Edit on GitHub

SnapshotEvaluator

A snapshot evaluator is responsible for evaluating a snapshot given some runtime arguments, e.g. start and end timestamps.

Evaluation

Snapshot evaluation involves determining the queries necessary to evaluate a snapshot and using sqlmesh.core.engine_adapter to execute the queries. Schemas, tables, and views are created if they don't exist and data is inserted when applicable.

A snapshot evaluator also promotes and demotes snapshots to a given environment.

Audits

A snapshot evaluator can also run the audits for a snapshot's node. This is often done after a snapshot has been evaluated to check for data quality issues.

For more information about audits, see sqlmesh.core.audit.

   1"""
   2# SnapshotEvaluator
   3
   4A snapshot evaluator is responsible for evaluating a snapshot given some runtime arguments, e.g. start
   5and end timestamps.
   6
   7# Evaluation
   8
   9Snapshot evaluation involves determining the queries necessary to evaluate a snapshot and using
  10`sqlmesh.core.engine_adapter` to execute the queries. Schemas, tables, and views are created if
  11they don't exist and data is inserted when applicable.
  12
  13A snapshot evaluator also promotes and demotes snapshots to a given environment.
  14
  15# Audits
  16
  17A snapshot evaluator can also run the audits for a snapshot's node. This is often done after a snapshot
  18has been evaluated to check for data quality issues.
  19
  20For more information about audits, see `sqlmesh.core.audit`.
  21"""
  22
  23from __future__ import annotations
  24
  25import abc
  26import logging
  27import typing as t
  28import sys
  29from collections import defaultdict
  30from contextlib import contextmanager
  31from functools import reduce
  32
  33from sqlglot import exp, select
  34from sqlglot.executor import execute
  35from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_not_exception_type
  36
  37from sqlmesh.core import constants as c
  38from sqlmesh.core import dialect as d
  39from sqlmesh.core.audit import Audit, StandaloneAudit
  40from sqlmesh.core.dialect import schema_
  41from sqlmesh.core.engine_adapter.shared import InsertOverwriteStrategy, DataObjectType, DataObject
  42from sqlmesh.core.model.meta import GrantsTargetLayer
  43from sqlmesh.core.macros import RuntimeStage
  44from sqlmesh.core.model import (
  45    AuditResult,
  46    IncrementalUnmanagedKind,
  47    Model,
  48    SeedModel,
  49    SCDType2ByColumnKind,
  50    SCDType2ByTimeKind,
  51    ViewKind,
  52    CustomKind,
  53)
  54from sqlmesh.core.model.kind import _Incremental, DbtCustomKind
  55from sqlmesh.utils import CompletionStatus, columns_to_types_all_known
  56from sqlmesh.core.schema_diff import (
  57    has_drop_alteration,
  58    TableAlterOperation,
  59    has_additive_alteration,
  60)
  61from sqlmesh.core.snapshot import (
  62    DeployabilityIndex,
  63    Intervals,
  64    Snapshot,
  65    SnapshotId,
  66    SnapshotIdBatch,
  67    SnapshotInfoLike,
  68    SnapshotTableCleanupTask,
  69)
  70from sqlmesh.core.snapshot.execution_tracker import QueryExecutionTracker
  71from sqlmesh.utils import random_id, CorrelationId, AttributeDict
  72from sqlmesh.utils.concurrency import (
  73    concurrent_apply_to_snapshots,
  74    concurrent_apply_to_values,
  75    NodeExecutionFailedError,
  76)
  77from sqlmesh.utils.date import TimeLike, now, time_like_to_str
  78from sqlmesh.utils.errors import (
  79    ConfigError,
  80    DestructiveChangeError,
  81    MigrationNotSupportedError,
  82    SQLMeshError,
  83    format_destructive_change_msg,
  84    format_additive_change_msg,
  85    AdditiveChangeError,
  86)
  87from sqlmesh.utils.jinja import MacroReturnVal
  88
  89if sys.version_info >= (3, 12):
  90    from importlib import metadata
  91else:
  92    import importlib_metadata as metadata  # type: ignore
  93
  94if t.TYPE_CHECKING:
  95    from sqlmesh.core.engine_adapter._typing import DF, QueryOrDF
  96    from sqlmesh.core.engine_adapter.base import EngineAdapter
  97    from sqlmesh.core.environment import EnvironmentNamingInfo
  98
  99logger = logging.getLogger(__name__)
 100
 101
 102class SnapshotCreationFailedError(SQLMeshError):
 103    def __init__(
 104        self, errors: t.List[NodeExecutionFailedError[SnapshotId]], skipped: t.List[SnapshotId]
 105    ):
 106        messages = "\n\n".join(f"{error}\n  {error.__cause__}" for error in errors)
 107        super().__init__(f"Physical table creation failed:\n\n{messages}")
 108        self.errors = errors
 109        self.skipped = skipped
 110
 111
 112class SnapshotEvaluator:
 113    """Evaluates a snapshot given runtime arguments through an arbitrary EngineAdapter.
 114
 115    The SnapshotEvaluator contains the business logic to generically evaluate a snapshot.
 116    It is responsible for delegating queries to the EngineAdapter. The SnapshotEvaluator
 117    does not directly communicate with the underlying execution engine.
 118
 119    Args:
 120        adapters: A single EngineAdapter or a dictionary of EngineAdapters where
 121            the key is the gateway name. When a dictionary is provided, and not an
 122            explicit default gateway its first item is treated as the default
 123            adapter and used for the virtual layer.
 124        ddl_concurrent_tasks: The number of concurrent tasks used for DDL
 125            operations (table / view creation, deletion, etc). Default: 1.
 126    """
 127
 128    def __init__(
 129        self,
 130        adapters: EngineAdapter | t.Dict[str, EngineAdapter],
 131        ddl_concurrent_tasks: int = 1,
 132        selected_gateway: t.Optional[str] = None,
 133    ):
 134        self.adapters = (
 135            adapters if isinstance(adapters, t.Dict) else {selected_gateway or "": adapters}
 136        )
 137        self.execution_tracker = QueryExecutionTracker()
 138        self.adapters = {
 139            gateway: adapter.with_settings(query_execution_tracker=self.execution_tracker)
 140            for gateway, adapter in self.adapters.items()
 141        }
 142        self.adapter = (
 143            next(iter(self.adapters.values()))
 144            if not selected_gateway
 145            else self.adapters[selected_gateway]
 146        )
 147        self.selected_gateway = selected_gateway
 148        self.ddl_concurrent_tasks = ddl_concurrent_tasks
 149
 150    def evaluate(
 151        self,
 152        snapshot: Snapshot,
 153        *,
 154        start: TimeLike,
 155        end: TimeLike,
 156        execution_time: TimeLike,
 157        snapshots: t.Dict[str, Snapshot],
 158        allow_destructive_snapshots: t.Optional[t.Set[str]] = None,
 159        allow_additive_snapshots: t.Optional[t.Set[str]] = None,
 160        deployability_index: t.Optional[DeployabilityIndex] = None,
 161        batch_index: int = 0,
 162        target_table_exists: t.Optional[bool] = None,
 163        **kwargs: t.Any,
 164    ) -> t.Optional[str]:
 165        """Renders the snapshot's model, executes it and stores the result in the snapshot's physical table.
 166
 167        Args:
 168            snapshot: Snapshot to evaluate.
 169            start: The start datetime to render.
 170            end: The end datetime to render.
 171            execution_time: The date/time time reference to use for execution time.
 172            snapshots: All upstream snapshots (by name) to use for expansion and mapping of physical locations.
 173            allow_destructive_snapshots: Snapshots for which destructive schema changes are allowed.
 174            allow_additive_snapshots: Snapshots for which additive schema changes are allowed.
 175            deployability_index: Determines snapshots that are deployable in the context of this evaluation.
 176            batch_index: If the snapshot is part of a batch of related snapshots; which index in the batch is it
 177            target_table_exists: Whether the target table exists. If None, the table will be checked for existence.
 178            kwargs: Additional kwargs to pass to the renderer.
 179
 180        Returns:
 181            The WAP ID of this evaluation if supported, None otherwise.
 182        """
 183        with self.execution_tracker.track_execution(
 184            SnapshotIdBatch(snapshot_id=snapshot.snapshot_id, batch_id=batch_index)
 185        ):
 186            result = self._evaluate_snapshot(
 187                start=start,
 188                end=end,
 189                execution_time=execution_time,
 190                snapshot=snapshot,
 191                snapshots=snapshots,
 192                allow_destructive_snapshots=allow_destructive_snapshots or set(),
 193                allow_additive_snapshots=allow_additive_snapshots or set(),
 194                deployability_index=deployability_index,
 195                batch_index=batch_index,
 196                target_table_exists=target_table_exists,
 197                **kwargs,
 198            )
 199        if result is None or isinstance(result, str):
 200            return result
 201        raise SQLMeshError(
 202            f"Unexpected result {result} when evaluating snapshot {snapshot.snapshot_id}."
 203        )
 204
 205    def evaluate_and_fetch(
 206        self,
 207        snapshot: Snapshot,
 208        *,
 209        start: TimeLike,
 210        end: TimeLike,
 211        execution_time: TimeLike,
 212        snapshots: t.Dict[str, Snapshot],
 213        limit: int,
 214        deployability_index: t.Optional[DeployabilityIndex] = None,
 215        **kwargs: t.Any,
 216    ) -> DF:
 217        """Renders the snapshot's model, executes it and returns a dataframe with the result.
 218
 219        Args:
 220            snapshot: Snapshot to evaluate.
 221            start: The start datetime to render.
 222            end: The end datetime to render.
 223            execution_time: The date/time time reference to use for execution time.
 224            snapshots: All upstream snapshots (by name) to use for expansion and mapping of physical locations.
 225            limit: The maximum number of rows to fetch.
 226            deployability_index: Determines snapshots that are deployable in the context of this evaluation.
 227            kwargs: Additional kwargs to pass to the renderer.
 228
 229        Returns:
 230            The result of the evaluation as a dataframe.
 231        """
 232        import pandas as pd
 233
 234        adapter = self.get_adapter(snapshot.model.gateway)
 235        render_kwargs = dict(
 236            start=start,
 237            end=end,
 238            execution_time=execution_time,
 239            snapshot=snapshot,
 240            runtime_stage=RuntimeStage.EVALUATING,
 241            **kwargs,
 242        )
 243        queries_or_dfs = self._render_snapshot_for_evaluation(
 244            snapshot,
 245            snapshots,
 246            deployability_index or DeployabilityIndex.all_deployable(),
 247            render_kwargs,
 248        )
 249        query_or_df = next(queries_or_dfs)
 250        if isinstance(query_or_df, pd.DataFrame):
 251            return query_or_df.head(limit)
 252        if not isinstance(query_or_df, exp.Expr):
 253            # We assume that if this branch is reached, `query_or_df` is a pyspark / snowpark / bigframe dataframe,
 254            # so we use `limit` instead of `head` to get back a dataframe instead of List[Row]
 255            # https://spark.apache.org/docs/3.1.1/api/python/reference/api/pyspark.sql.DataFrame.head.html#pyspark.sql.DataFrame.head
 256            return query_or_df.limit(limit)
 257
 258        assert isinstance(query_or_df, exp.Query)
 259
 260        existing_limit = query_or_df.args.get("limit")
 261        if existing_limit:
 262            limit = min(limit, execute(exp.select(existing_limit.expression)).rows[0][0])
 263            assert limit is not None
 264
 265        return adapter._fetch_native_df(query_or_df.limit(limit))
 266
 267    def promote(
 268        self,
 269        target_snapshots: t.Iterable[Snapshot],
 270        environment_naming_info: EnvironmentNamingInfo,
 271        deployability_index: t.Optional[DeployabilityIndex] = None,
 272        start: t.Optional[TimeLike] = None,
 273        end: t.Optional[TimeLike] = None,
 274        execution_time: t.Optional[TimeLike] = None,
 275        snapshots: t.Optional[t.Dict[SnapshotId, Snapshot]] = None,
 276        table_mapping: t.Optional[t.Dict[str, str]] = None,
 277        on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]] = None,
 278    ) -> None:
 279        """Promotes the given collection of snapshots in the target environment by replacing a corresponding
 280        view with a physical table associated with the given snapshot.
 281
 282        Args:
 283            target_snapshots: Snapshots to promote.
 284            environment_naming_info: Naming information for the target environment.
 285            deployability_index: Determines snapshots that are deployable in the context of this promotion.
 286            on_complete: A callback to call on each successfully promoted snapshot.
 287        """
 288
 289        tables_by_gateway: t.Dict[t.Union[str, None], t.List[exp.Table]] = defaultdict(list)
 290        for snapshot in target_snapshots:
 291            if snapshot.is_model and not snapshot.is_symbolic:
 292                gateway = (
 293                    snapshot.model_gateway if environment_naming_info.gateway_managed else None
 294                )
 295                adapter = self.get_adapter(gateway)
 296                table = snapshot.qualified_view_name.table_for_environment(
 297                    environment_naming_info, dialect=adapter.dialect
 298                )
 299                tables_by_gateway[gateway].append(table)
 300
 301        # A schema can be shared across multiple engines, so we need to group by gateway
 302        for gateway, tables in tables_by_gateway.items():
 303            if environment_naming_info.suffix_target.is_catalog:
 304                self._create_catalogs(tables=tables, gateway=gateway)
 305
 306        gateway_table_pairs = [
 307            (gateway, table) for gateway, tables in tables_by_gateway.items() for table in tables
 308        ]
 309        self._create_schemas(gateway_table_pairs=gateway_table_pairs)
 310
 311        # Fetch the view data objects for the promoted snapshots to get them cached
 312        self._get_virtual_data_objects(target_snapshots, environment_naming_info)
 313
 314        deployability_index = deployability_index or DeployabilityIndex.all_deployable()
 315        with self.concurrent_context():
 316            concurrent_apply_to_snapshots(
 317                target_snapshots,
 318                lambda s: self._promote_snapshot(
 319                    s,
 320                    start=start,
 321                    end=end,
 322                    execution_time=execution_time,
 323                    snapshots=snapshots,
 324                    table_mapping=table_mapping,
 325                    environment_naming_info=environment_naming_info,
 326                    deployability_index=deployability_index,  # type: ignore
 327                    on_complete=on_complete,
 328                ),
 329                self.ddl_concurrent_tasks,
 330            )
 331
 332    def demote(
 333        self,
 334        target_snapshots: t.Iterable[Snapshot],
 335        environment_naming_info: EnvironmentNamingInfo,
 336        table_mapping: t.Optional[t.Dict[str, str]] = None,
 337        deployability_index: t.Optional[DeployabilityIndex] = None,
 338        on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]] = None,
 339    ) -> None:
 340        """Demotes the given collection of snapshots in the target environment by removing its view.
 341
 342        Args:
 343            target_snapshots: Snapshots to demote.
 344            environment_naming_info: Naming info for the target environment.
 345            on_complete: A callback to call on each successfully demoted snapshot.
 346        """
 347        with self.concurrent_context():
 348            concurrent_apply_to_snapshots(
 349                target_snapshots,
 350                lambda s: self._demote_snapshot(
 351                    s,
 352                    environment_naming_info,
 353                    deployability_index=deployability_index,
 354                    on_complete=on_complete,
 355                    table_mapping=table_mapping,
 356                ),
 357                self.ddl_concurrent_tasks,
 358            )
 359
 360    def create(
 361        self,
 362        target_snapshots: t.Iterable[Snapshot],
 363        snapshots: t.Dict[SnapshotId, Snapshot],
 364        deployability_index: t.Optional[DeployabilityIndex] = None,
 365        on_start: t.Optional[t.Callable] = None,
 366        on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]] = None,
 367        allow_destructive_snapshots: t.Optional[t.Set[str]] = None,
 368        allow_additive_snapshots: t.Optional[t.Set[str]] = None,
 369    ) -> CompletionStatus:
 370        """Creates a physical snapshot schema and table for the given collection of snapshots.
 371
 372        Args:
 373            target_snapshots: Target snapshots.
 374            snapshots: Mapping of snapshot ID to snapshot.
 375            deployability_index: Determines snapshots that are deployable in the context of this creation.
 376            on_start: A callback to initialize the snapshot creation progress bar.
 377            on_complete: A callback to call on each successfully created snapshot.
 378            allow_destructive_snapshots: Set of snapshots that are allowed to have destructive schema changes.
 379            allow_additive_snapshots: Set of snapshots that are allowed to have additive schema changes.
 380
 381        Returns:
 382            CompletionStatus: The status of the creation operation (success, failure, nothing to do).
 383        """
 384        deployability_index = deployability_index or DeployabilityIndex.all_deployable()
 385
 386        snapshots_to_create = self.get_snapshots_to_create(target_snapshots, deployability_index)
 387        if not snapshots_to_create:
 388            return CompletionStatus.NOTHING_TO_DO
 389        if on_start:
 390            on_start(snapshots_to_create)
 391
 392        self._create_snapshots(
 393            snapshots_to_create=snapshots_to_create,
 394            snapshots={s.name: s for s in snapshots.values()},
 395            deployability_index=deployability_index,
 396            on_complete=on_complete,
 397            allow_destructive_snapshots=allow_destructive_snapshots or set(),
 398            allow_additive_snapshots=allow_additive_snapshots or set(),
 399        )
 400        return CompletionStatus.SUCCESS
 401
 402    def create_physical_schemas(
 403        self, snapshots: t.Iterable[Snapshot], deployability_index: DeployabilityIndex
 404    ) -> None:
 405        """Creates the physical schemas for the given snapshots.
 406
 407        Args:
 408            snapshots: Snapshots to create physical schemas for.
 409            deployability_index: Determines snapshots that are deployable in the context of this creation.
 410        """
 411        tables_by_gateway: t.Dict[t.Optional[str], t.List[str]] = defaultdict(list)
 412        for snapshot in snapshots:
 413            if snapshot.is_model and not snapshot.is_symbolic:
 414                tables_by_gateway[snapshot.model_gateway].append(
 415                    snapshot.table_name(is_deployable=deployability_index.is_deployable(snapshot))
 416                )
 417
 418        gateway_table_pairs = [
 419            (gateway, table) for gateway, tables in tables_by_gateway.items() for table in tables
 420        ]
 421        self._create_schemas(gateway_table_pairs=gateway_table_pairs)
 422
 423    def get_snapshots_to_create(
 424        self, target_snapshots: t.Iterable[Snapshot], deployability_index: DeployabilityIndex
 425    ) -> t.List[Snapshot]:
 426        """Returns a list of snapshots that need to have their physical tables created.
 427
 428        Args:
 429            target_snapshots: Target snapshots.
 430            deployability_index: Determines snapshots that are deployable / representative in the context of this creation.
 431        """
 432        existing_data_objects = self._get_physical_data_objects(
 433            target_snapshots, deployability_index
 434        )
 435        snapshots_to_create = []
 436        for snapshot in target_snapshots:
 437            if not snapshot.is_model or snapshot.is_symbolic:
 438                continue
 439            if snapshot.snapshot_id not in existing_data_objects or (
 440                snapshot.is_seed and not snapshot.intervals
 441            ):
 442                snapshots_to_create.append(snapshot)
 443
 444        return snapshots_to_create
 445
 446    def _create_snapshots(
 447        self,
 448        snapshots_to_create: t.Iterable[Snapshot],
 449        snapshots: t.Dict[str, Snapshot],
 450        deployability_index: DeployabilityIndex,
 451        on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]],
 452        allow_destructive_snapshots: t.Set[str],
 453        allow_additive_snapshots: t.Set[str],
 454    ) -> None:
 455        """Internal method to create tables in parallel."""
 456        with self.concurrent_context():
 457            errors, skipped = concurrent_apply_to_snapshots(
 458                snapshots_to_create,
 459                lambda s: self.create_snapshot(
 460                    s,
 461                    snapshots=snapshots,
 462                    deployability_index=deployability_index,
 463                    allow_destructive_snapshots=allow_destructive_snapshots,
 464                    allow_additive_snapshots=allow_additive_snapshots,
 465                    on_complete=on_complete,
 466                ),
 467                self.ddl_concurrent_tasks,
 468                raise_on_error=False,
 469            )
 470            if errors:
 471                raise SnapshotCreationFailedError(errors, skipped)
 472
 473    def migrate(
 474        self,
 475        target_snapshots: t.Iterable[Snapshot],
 476        snapshots: t.Dict[SnapshotId, Snapshot],
 477        allow_destructive_snapshots: t.Optional[t.Set[str]] = None,
 478        allow_additive_snapshots: t.Optional[t.Set[str]] = None,
 479        deployability_index: t.Optional[DeployabilityIndex] = None,
 480    ) -> None:
 481        """Alters a physical snapshot table to match its snapshot's schema for the given collection of snapshots.
 482
 483        Args:
 484            target_snapshots: Target snapshots.
 485            snapshots: Mapping of snapshot ID to snapshot.
 486            allow_destructive_snapshots: Set of snapshots that are allowed to have destructive schema changes.
 487            allow_additive_snapshots: Set of snapshots that are allowed to have additive schema changes.
 488            deployability_index: Determines snapshots that are deployable in the context of this evaluation.
 489        """
 490        deployability_index = deployability_index or DeployabilityIndex.all_deployable()
 491        target_data_objects = self._get_physical_data_objects(target_snapshots, deployability_index)
 492        if not target_data_objects:
 493            return
 494
 495        if not snapshots:
 496            snapshots = {s.snapshot_id: s for s in target_snapshots}
 497
 498        allow_destructive_snapshots = allow_destructive_snapshots or set()
 499        allow_additive_snapshots = allow_additive_snapshots or set()
 500        snapshots_by_name = {s.name: s for s in snapshots.values()}
 501        with self.concurrent_context():
 502            # Only migrate snapshots for which there's an existing data object
 503            concurrent_apply_to_snapshots(
 504                target_snapshots,
 505                lambda s: self._migrate_snapshot(
 506                    s,
 507                    snapshots_by_name,
 508                    target_data_objects.get(s.snapshot_id),
 509                    allow_destructive_snapshots,
 510                    allow_additive_snapshots,
 511                    self.get_adapter(s.model_gateway),
 512                    deployability_index,
 513                ),
 514                self.ddl_concurrent_tasks,
 515            )
 516
 517    def cleanup(
 518        self,
 519        target_snapshots: t.Iterable[SnapshotTableCleanupTask],
 520        on_complete: t.Optional[t.Callable[[str], None]] = None,
 521    ) -> None:
 522        """Cleans up the given snapshots by removing its table
 523
 524        Args:
 525            target_snapshots: Snapshots to cleanup.
 526            on_complete: A callback to call on each successfully deleted database object.
 527        """
 528        target_snapshots = [
 529            t for t in target_snapshots if t.snapshot.is_model and not t.snapshot.is_symbolic
 530        ]
 531        available_gateways = set(self.adapters.keys())
 532        skipped = []
 533        filtered_targets = []
 534        for t in target_snapshots:
 535            gw = t.snapshot.model_gateway
 536            if gw and gw not in available_gateways:
 537                skipped.append((t.snapshot.snapshot_id, gw))
 538            else:
 539                filtered_targets.append(t)
 540        if skipped:
 541            logger.warning(
 542                "Skipping cleanup of %d snapshot(s) with unavailable gateway(s): %s",
 543                len(skipped),
 544                ", ".join(f"{sid} (gateway={gw})" for sid, gw in skipped),
 545            )
 546        snapshots_to_dev_table_only = {
 547            t.snapshot.snapshot_id: t.dev_table_only for t in filtered_targets
 548        }
 549        with self.concurrent_context():
 550            errors, _ = concurrent_apply_to_snapshots(
 551                [t.snapshot for t in filtered_targets],
 552                lambda s: self._cleanup_snapshot(
 553                    s,
 554                    snapshots_to_dev_table_only[s.snapshot_id],
 555                    self.get_adapter(s.model_gateway),
 556                    on_complete,
 557                ),
 558                self.ddl_concurrent_tasks,
 559                reverse_order=True,
 560                raise_on_error=False,
 561            )
 562        if errors:
 563            errored_snapshots = "\n".join(f"  {e.node.name}: {e.__cause__}" for e in errors)
 564            raise SQLMeshError(f"\n{errored_snapshots}")
 565
 566    def audit(
 567        self,
 568        snapshot: Snapshot,
 569        *,
 570        snapshots: t.Dict[str, Snapshot],
 571        start: t.Optional[TimeLike] = None,
 572        end: t.Optional[TimeLike] = None,
 573        execution_time: t.Optional[TimeLike] = None,
 574        deployability_index: t.Optional[DeployabilityIndex] = None,
 575        wap_id: t.Optional[str] = None,
 576        **kwargs: t.Any,
 577    ) -> t.List[AuditResult]:
 578        """Execute a snapshot's node's audit queries.
 579
 580        Args:
 581            snapshot: Snapshot to evaluate.
 582            snapshots: All upstream snapshots (by name) to use for expansion and mapping of physical locations.
 583            start: The start datetime to audit. Defaults to epoch start.
 584            end: The end datetime to audit. Defaults to epoch start.
 585            execution_time: The date/time time reference to use for execution time.
 586            deployability_index: Determines snapshots that are deployable in the context of this evaluation.
 587            wap_id: The WAP ID if applicable, None otherwise.
 588            kwargs: Additional kwargs to pass to the renderer.
 589        """
 590        deployability_index = deployability_index or DeployabilityIndex.all_deployable()
 591        adapter = self.get_adapter(snapshot.model_gateway)
 592
 593        if not snapshot.version:
 594            raise ConfigError(
 595                f"Cannot audit '{snapshot.name}' because it has not been versioned yet. Apply a plan first."
 596            )
 597
 598        if wap_id is not None:
 599            deployability_index = deployability_index or DeployabilityIndex.all_deployable()
 600            original_table_name = snapshot.table_name(
 601                is_deployable=deployability_index.is_deployable(snapshot)
 602            )
 603            wap_table_name = adapter.wap_table_name(original_table_name, wap_id)
 604            logger.info(
 605                "Auditing WAP table '%s', snapshot %s",
 606                wap_table_name,
 607                snapshot.snapshot_id,
 608            )
 609
 610            table_mapping = kwargs.get("table_mapping") or {}
 611            table_mapping[snapshot.name] = wap_table_name
 612            kwargs["table_mapping"] = table_mapping
 613            kwargs["this_model"] = exp.to_table(wap_table_name, dialect=adapter.dialect)
 614
 615        results = []
 616
 617        audits_with_args = snapshot.node.audits_with_args
 618
 619        force_non_blocking = False
 620
 621        if audits_with_args:
 622            logger.info("Auditing snapshot %s", snapshot.snapshot_id)
 623
 624            if not deployability_index.is_deployable(snapshot) and not adapter.SUPPORTS_CLONING:
 625                # For dev preview tables that aren't based on clones of the production table, only a subset of the data is typically available
 626                # However, users still expect audits to run anwyay. Some audits (such as row count) are practically guaranteed to fail
 627                # when run on only a subset of data, so we switch all audits to non blocking and the user can decide if they still want to proceed
 628                force_non_blocking = True
 629
 630        for audit, audit_args in audits_with_args:
 631            if force_non_blocking:
 632                # remove any blocking indicator on the model itself
 633                audit_args.pop("blocking", None)
 634                # so that we can fall back to the audit's setting, which we override to blocking: False
 635                audit = audit.model_copy(update={"blocking": False})
 636
 637            results.append(
 638                self._audit(
 639                    audit=audit,
 640                    audit_args=audit_args,
 641                    snapshot=snapshot,
 642                    snapshots=snapshots,
 643                    start=start,
 644                    end=end,
 645                    execution_time=execution_time,
 646                    deployability_index=deployability_index,
 647                    **kwargs,
 648                )
 649            )
 650
 651        if wap_id is not None:
 652            logger.info(
 653                "Publishing evaluation results for snapshot %s, WAP ID '%s'",
 654                snapshot.snapshot_id,
 655                wap_id,
 656            )
 657            self.wap_publish_snapshot(snapshot, wap_id, deployability_index)
 658
 659        return results
 660
 661    @contextmanager
 662    def concurrent_context(self) -> t.Iterator[None]:
 663        try:
 664            yield
 665        finally:
 666            self.recycle()
 667
 668    def recycle(self) -> None:
 669        """Closes all open connections and releases all allocated resources associated with any thread
 670        except the calling one."""
 671        try:
 672            for adapter in self.adapters.values():
 673                adapter.recycle()
 674
 675        except Exception:
 676            logger.exception("Failed to recycle Snapshot Evaluator")
 677
 678    def close(self) -> None:
 679        """Closes all open connections and releases all allocated resources."""
 680        try:
 681            for adapter in self.adapters.values():
 682                adapter.close()
 683        except Exception:
 684            logger.exception("Failed to close Snapshot Evaluator")
 685
 686    def set_correlation_id(self, correlation_id: CorrelationId) -> SnapshotEvaluator:
 687        return SnapshotEvaluator(
 688            {
 689                gateway: adapter.with_settings(correlation_id=correlation_id)
 690                for gateway, adapter in self.adapters.items()
 691            },
 692            self.ddl_concurrent_tasks,
 693            self.selected_gateway,
 694        )
 695
 696    def _evaluate_snapshot(
 697        self,
 698        start: TimeLike,
 699        end: TimeLike,
 700        execution_time: TimeLike,
 701        snapshot: Snapshot,
 702        snapshots: t.Dict[str, Snapshot],
 703        allow_destructive_snapshots: t.Set[str],
 704        allow_additive_snapshots: t.Set[str],
 705        deployability_index: t.Optional[DeployabilityIndex],
 706        batch_index: int,
 707        target_table_exists: t.Optional[bool],
 708        **kwargs: t.Any,
 709    ) -> t.Optional[str]:
 710        """Renders the snapshot's model and executes it. The return value depends on whether the limit was specified.
 711
 712        Args:
 713            snapshot: Snapshot to evaluate.
 714            start: The start datetime to render.
 715            end: The end datetime to render.
 716            execution_time: The date/time time reference to use for execution time.
 717            snapshots: All upstream snapshots to use for expansion and mapping of physical locations.
 718            allow_destructive_snapshots: Snapshots for which destructive schema changes are allowed.
 719            allow_additive_snapshots: Snapshots for which additive schema changes are allowed.
 720            deployability_index: Determines snapshots that are deployable in the context of this evaluation.
 721            batch_index: If the snapshot is part of a batch of related snapshots; which index in the batch is it
 722            target_table_exists: Whether the target table exists. If None, the table will be checked for existence.
 723            kwargs: Additional kwargs to pass to the renderer.
 724        """
 725        if not snapshot.is_model:
 726            return None
 727
 728        model = snapshot.model
 729
 730        logger.info("Evaluating snapshot %s", snapshot.snapshot_id)
 731
 732        adapter = self.get_adapter(model.gateway)
 733        deployability_index = deployability_index or DeployabilityIndex.all_deployable()
 734        is_snapshot_deployable = deployability_index.is_deployable(snapshot)
 735        target_table_name = snapshot.table_name(is_deployable=is_snapshot_deployable)
 736        # https://github.com/SQLMesh/sqlmesh/issues/2609
 737        # If there are no existing intervals yet; only consider this a first insert for the first snapshot in the batch
 738        if target_table_exists is None:
 739            target_table_exists = adapter.table_exists(target_table_name)
 740        is_first_insert = (
 741            not _intervals(snapshot, deployability_index) or not target_table_exists
 742        ) and batch_index == 0
 743
 744        # Use the 'creating' stage if the table doesn't exist yet to preserve backwards compatibility with existing projects
 745        # that depend on a separate physical table creation stage.
 746        runtime_stage = RuntimeStage.EVALUATING if target_table_exists else RuntimeStage.CREATING
 747        common_render_kwargs = dict(
 748            start=start,
 749            end=end,
 750            execution_time=execution_time,
 751            snapshot=snapshot,
 752            runtime_stage=runtime_stage,
 753            **kwargs,
 754        )
 755        create_render_kwargs = dict(
 756            engine_adapter=adapter,
 757            snapshots=snapshots,
 758            deployability_index=deployability_index,
 759            **common_render_kwargs,
 760        )
 761        create_render_kwargs["runtime_stage"] = RuntimeStage.CREATING
 762        render_statements_kwargs = dict(
 763            engine_adapter=adapter,
 764            snapshots=snapshots,
 765            deployability_index=deployability_index,
 766            **common_render_kwargs,
 767        )
 768        rendered_physical_properties = snapshot.model.render_physical_properties(
 769            **render_statements_kwargs
 770        )
 771
 772        evaluation_strategy = _evaluation_strategy(snapshot, adapter)
 773        evaluation_strategy.run_pre_statements(
 774            snapshot=snapshot,
 775            render_kwargs={**render_statements_kwargs, "inside_transaction": False},
 776        )
 777
 778        with (
 779            adapter.transaction(),
 780            adapter.session(snapshot.model.render_session_properties(**render_statements_kwargs)),
 781        ):
 782            evaluation_strategy.run_pre_statements(
 783                snapshot=snapshot,
 784                render_kwargs={**render_statements_kwargs, "inside_transaction": True},
 785            )
 786
 787            if not target_table_exists or (model.is_seed and not snapshot.intervals):
 788                # Only create the empty table if the columns were provided explicitly by the user
 789                should_create_empty_table = (
 790                    model.kind.is_materialized
 791                    and model.columns_to_types_
 792                    and columns_to_types_all_known(model.columns_to_types_)
 793                )
 794                if not should_create_empty_table:
 795                    # Or if the model is self-referential and its query is fully annotated with types
 796                    should_create_empty_table = model.depends_on_self and model.annotated
 797                if self._can_clone(snapshot, deployability_index):
 798                    self._clone_snapshot_in_dev(
 799                        snapshot=snapshot,
 800                        snapshots=snapshots,
 801                        deployability_index=deployability_index,
 802                        render_kwargs=create_render_kwargs,
 803                        rendered_physical_properties=rendered_physical_properties.copy(),
 804                        allow_destructive_snapshots=allow_destructive_snapshots,
 805                        allow_additive_snapshots=allow_additive_snapshots,
 806                    )
 807                    runtime_stage = RuntimeStage.EVALUATING
 808                    target_table_exists = True
 809                elif should_create_empty_table or model.is_seed or model.kind.is_scd_type_2:
 810                    self._execute_create(
 811                        snapshot=snapshot,
 812                        table_name=target_table_name,
 813                        is_table_deployable=is_snapshot_deployable,
 814                        deployability_index=deployability_index,
 815                        create_render_kwargs=create_render_kwargs,
 816                        rendered_physical_properties=rendered_physical_properties.copy(),
 817                        dry_run=False,
 818                        run_pre_post_statements=False,
 819                    )
 820                    runtime_stage = RuntimeStage.EVALUATING
 821                    target_table_exists = True
 822
 823            evaluate_render_kwargs = {
 824                **common_render_kwargs,
 825                "runtime_stage": runtime_stage,
 826                "snapshot_table_exists": target_table_exists,
 827            }
 828
 829            wap_id: t.Optional[str] = None
 830            if (
 831                snapshot.is_materialized
 832                and target_table_exists
 833                and adapter.wap_enabled
 834                and (model.wap_supported or adapter.wap_supported(target_table_name))
 835            ):
 836                wap_id = random_id()[0:8]
 837                logger.info("Using WAP ID '%s' for snapshot %s", wap_id, snapshot.snapshot_id)
 838                target_table_name = adapter.wap_prepare(target_table_name, wap_id)
 839
 840            self._render_and_insert_snapshot(
 841                start=start,
 842                end=end,
 843                execution_time=execution_time,
 844                snapshot=snapshot,
 845                snapshots=snapshots,
 846                render_kwargs=evaluate_render_kwargs,
 847                create_render_kwargs=create_render_kwargs,
 848                rendered_physical_properties=rendered_physical_properties,
 849                deployability_index=deployability_index,
 850                target_table_name=target_table_name,
 851                is_first_insert=is_first_insert,
 852                batch_index=batch_index,
 853            )
 854
 855            evaluation_strategy.run_post_statements(
 856                snapshot=snapshot,
 857                render_kwargs={**render_statements_kwargs, "inside_transaction": True},
 858            )
 859
 860        evaluation_strategy.run_post_statements(
 861            snapshot=snapshot,
 862            render_kwargs={**render_statements_kwargs, "inside_transaction": False},
 863        )
 864
 865        return wap_id
 866
 867    def create_snapshot(
 868        self,
 869        snapshot: Snapshot,
 870        snapshots: t.Dict[str, Snapshot],
 871        deployability_index: DeployabilityIndex,
 872        allow_destructive_snapshots: t.Set[str],
 873        allow_additive_snapshots: t.Set[str],
 874        on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]] = None,
 875    ) -> None:
 876        """Creates a physical table for the given snapshot.
 877
 878        Args:
 879            snapshot: Snapshot to create.
 880            snapshots: All upstream snapshots to use for expansion and mapping of physical locations.
 881            deployability_index: Determines snapshots that are deployable in the context of this creation.
 882            on_complete: A callback to call on each successfully created database object.
 883            allow_destructive_snapshots: Snapshots for which destructive schema changes are allowed.
 884            allow_additive_snapshots: Snapshots for which additive schema changes are allowed.
 885        """
 886        if not snapshot.is_model:
 887            return
 888
 889        logger.info("Creating a physical table for snapshot %s", snapshot.snapshot_id)
 890
 891        adapter = self.get_adapter(snapshot.model.gateway)
 892        create_render_kwargs: t.Dict[str, t.Any] = dict(
 893            engine_adapter=adapter,
 894            snapshots=snapshots,
 895            runtime_stage=RuntimeStage.CREATING,
 896            deployability_index=deployability_index,
 897        )
 898
 899        evaluation_strategy = _evaluation_strategy(snapshot, adapter)
 900        evaluation_strategy.run_pre_statements(
 901            snapshot=snapshot, render_kwargs={**create_render_kwargs, "inside_transaction": False}
 902        )
 903
 904        with (
 905            adapter.transaction(),
 906            adapter.session(snapshot.model.render_session_properties(**create_render_kwargs)),
 907        ):
 908            rendered_physical_properties = snapshot.model.render_physical_properties(
 909                **create_render_kwargs
 910            )
 911
 912            if self._can_clone(snapshot, deployability_index):
 913                self._clone_snapshot_in_dev(
 914                    snapshot=snapshot,
 915                    snapshots=snapshots,
 916                    deployability_index=deployability_index,
 917                    render_kwargs=create_render_kwargs,
 918                    rendered_physical_properties=rendered_physical_properties,
 919                    allow_destructive_snapshots=allow_destructive_snapshots,
 920                    allow_additive_snapshots=allow_additive_snapshots,
 921                    run_pre_post_statements=True,
 922                )
 923            else:
 924                is_table_deployable = deployability_index.is_deployable(snapshot)
 925                self._execute_create(
 926                    snapshot=snapshot,
 927                    table_name=snapshot.table_name(is_deployable=is_table_deployable),
 928                    is_table_deployable=is_table_deployable,
 929                    deployability_index=deployability_index,
 930                    create_render_kwargs=create_render_kwargs,
 931                    rendered_physical_properties=rendered_physical_properties,
 932                    dry_run=True,
 933                )
 934
 935        evaluation_strategy.run_post_statements(
 936            snapshot=snapshot, render_kwargs={**create_render_kwargs, "inside_transaction": False}
 937        )
 938
 939        if on_complete is not None:
 940            on_complete(snapshot)
 941
 942    def wap_publish_snapshot(
 943        self,
 944        snapshot: Snapshot,
 945        wap_id: str,
 946        deployability_index: t.Optional[DeployabilityIndex],
 947    ) -> None:
 948        deployability_index = deployability_index or DeployabilityIndex.all_deployable()
 949        table_name = snapshot.table_name(is_deployable=deployability_index.is_deployable(snapshot))
 950        adapter = self.get_adapter(snapshot.model_gateway)
 951        adapter.wap_publish(table_name, wap_id)
 952
 953    def _render_and_insert_snapshot(
 954        self,
 955        start: TimeLike,
 956        end: TimeLike,
 957        execution_time: TimeLike,
 958        snapshot: Snapshot,
 959        snapshots: t.Dict[str, Snapshot],
 960        render_kwargs: t.Dict[str, t.Any],
 961        create_render_kwargs: t.Dict[str, t.Any],
 962        rendered_physical_properties: t.Dict[str, exp.Expr],
 963        deployability_index: DeployabilityIndex,
 964        target_table_name: str,
 965        is_first_insert: bool,
 966        batch_index: int,
 967    ) -> None:
 968        if not snapshot.is_model or snapshot.is_seed:
 969            return
 970
 971        logger.info("Inserting data for snapshot %s", snapshot.snapshot_id)
 972
 973        model = snapshot.model
 974        adapter = self.get_adapter(model.gateway)
 975        evaluation_strategy = _evaluation_strategy(snapshot, adapter)
 976        is_snapshot_deployable = deployability_index.is_deployable(snapshot)
 977
 978        queries_or_dfs = self._render_snapshot_for_evaluation(
 979            snapshot,
 980            snapshots,
 981            deployability_index,
 982            render_kwargs,
 983        )
 984
 985        def apply(query_or_df: QueryOrDF, index: int = 0) -> None:
 986            if index > 0:
 987                evaluation_strategy.append(
 988                    table_name=target_table_name,
 989                    query_or_df=query_or_df,
 990                    model=snapshot.model,
 991                    snapshot=snapshot,
 992                    snapshots=snapshots,
 993                    deployability_index=deployability_index,
 994                    batch_index=batch_index,
 995                    start=start,
 996                    end=end,
 997                    execution_time=execution_time,
 998                    physical_properties=rendered_physical_properties,
 999                    render_kwargs=create_render_kwargs,
1000                    is_snapshot_deployable=is_snapshot_deployable,
1001                )
1002            else:
1003                logger.info(
1004                    "Inserting batch (%s, %s) into %s'",
1005                    time_like_to_str(start),
1006                    time_like_to_str(end),
1007                    target_table_name,
1008                )
1009                evaluation_strategy.insert(
1010                    table_name=target_table_name,
1011                    query_or_df=query_or_df,
1012                    is_first_insert=is_first_insert,
1013                    model=snapshot.model,
1014                    snapshot=snapshot,
1015                    snapshots=snapshots,
1016                    deployability_index=deployability_index,
1017                    batch_index=batch_index,
1018                    start=start,
1019                    end=end,
1020                    execution_time=execution_time,
1021                    physical_properties=rendered_physical_properties,
1022                    render_kwargs=create_render_kwargs,
1023                    is_snapshot_deployable=is_snapshot_deployable,
1024                )
1025
1026        # DataFrames, unlike SQL expressions, can provide partial results by yielding dataframes. As a result,
1027        # if the engine supports INSERT OVERWRITE or REPLACE WHERE and the snapshot is incremental by time range, we risk
1028        # having a partial result since each dataframe write can re-truncate partitions. To avoid this, we
1029        # union all the dataframes together before writing. For pandas this could result in OOM and a potential
1030        # workaround for that would be to serialize pandas to disk and then read it back with Spark.
1031        # Note: We assume that if multiple things are yielded from `queries_or_dfs` that they are dataframes
1032        # and not SQL expressions.
1033        if (
1034            adapter.INSERT_OVERWRITE_STRATEGY
1035            in (
1036                InsertOverwriteStrategy.INSERT_OVERWRITE,
1037                InsertOverwriteStrategy.REPLACE_WHERE,
1038            )
1039            and snapshot.is_incremental_by_time_range
1040        ):
1041            import pandas as pd
1042
1043            try:
1044                first_query_or_df = next(queries_or_dfs)
1045            except StopIteration:
1046                return
1047
1048            query_or_df = reduce(
1049                lambda a, b: (
1050                    pd.concat([a, b], ignore_index=True)  # type: ignore
1051                    if isinstance(a, pd.DataFrame)
1052                    else a.union_all(b)  # type: ignore
1053                ),  # type: ignore
1054                queries_or_dfs,
1055                first_query_or_df,
1056            )
1057            apply(query_or_df, index=0)
1058        else:
1059            for index, query_or_df in enumerate(queries_or_dfs):
1060                apply(query_or_df, index)
1061
1062    def _render_snapshot_for_evaluation(
1063        self,
1064        snapshot: Snapshot,
1065        snapshots: t.Dict[str, Snapshot],
1066        deployability_index: DeployabilityIndex,
1067        render_kwargs: t.Dict[str, t.Any],
1068    ) -> t.Iterator[QueryOrDF]:
1069        from sqlmesh.core.context import ExecutionContext
1070
1071        model = snapshot.model
1072        adapter = self.get_adapter(model.gateway)
1073
1074        return model.render(
1075            context=ExecutionContext(
1076                adapter,
1077                snapshots,
1078                deployability_index,
1079                default_dialect=model.dialect,
1080                default_catalog=model.default_catalog,
1081            ),
1082            **render_kwargs,
1083        )
1084
1085    def _clone_snapshot_in_dev(
1086        self,
1087        snapshot: Snapshot,
1088        snapshots: t.Dict[str, Snapshot],
1089        deployability_index: DeployabilityIndex,
1090        render_kwargs: t.Dict[str, t.Any],
1091        rendered_physical_properties: t.Dict[str, exp.Expr],
1092        allow_destructive_snapshots: t.Set[str],
1093        allow_additive_snapshots: t.Set[str],
1094        run_pre_post_statements: bool = False,
1095    ) -> None:
1096        adapter = self.get_adapter(snapshot.model.gateway)
1097
1098        target_table_name = snapshot.table_name(is_deployable=False)
1099        source_table_name = snapshot.table_name()
1100
1101        try:
1102            logger.info(f"Cloning table '{source_table_name}' into '{target_table_name}'")
1103            adapter.clone_table(
1104                target_table_name,
1105                snapshot.table_name(),
1106                rendered_physical_properties=rendered_physical_properties,
1107            )
1108            self._migrate_target_table(
1109                target_table_name=target_table_name,
1110                snapshot=snapshot,
1111                snapshots=snapshots,
1112                deployability_index=deployability_index,
1113                render_kwargs=render_kwargs,
1114                rendered_physical_properties=rendered_physical_properties,
1115                allow_destructive_snapshots=allow_destructive_snapshots,
1116                allow_additive_snapshots=allow_additive_snapshots,
1117                run_pre_post_statements=run_pre_post_statements,
1118            )
1119
1120        except Exception:
1121            adapter.drop_table(target_table_name)
1122            raise
1123
1124    def _migrate_snapshot(
1125        self,
1126        snapshot: Snapshot,
1127        snapshots: t.Dict[str, Snapshot],
1128        target_data_object: t.Optional[DataObject],
1129        allow_destructive_snapshots: t.Set[str],
1130        allow_additive_snapshots: t.Set[str],
1131        adapter: EngineAdapter,
1132        deployability_index: DeployabilityIndex,
1133    ) -> None:
1134        if not snapshot.is_model or snapshot.is_symbolic:
1135            return
1136
1137        deployability_index = DeployabilityIndex.all_deployable()
1138        render_kwargs: t.Dict[str, t.Any] = dict(
1139            engine_adapter=adapter,
1140            snapshots=snapshots,
1141            runtime_stage=RuntimeStage.CREATING,
1142            deployability_index=deployability_index,
1143        )
1144        target_table_name = snapshot.table_name()
1145
1146        evaluation_strategy = _evaluation_strategy(snapshot, adapter)
1147        evaluation_strategy.run_pre_statements(
1148            snapshot=snapshot, render_kwargs={**render_kwargs, "inside_transaction": False}
1149        )
1150
1151        with (
1152            adapter.transaction(),
1153            adapter.session(snapshot.model.render_session_properties(**render_kwargs)),
1154        ):
1155            table_exists = target_data_object is not None
1156            if adapter.drop_data_object_on_type_mismatch(
1157                target_data_object, _snapshot_to_data_object_type(snapshot)
1158            ):
1159                table_exists = False
1160
1161            rendered_physical_properties = snapshot.model.render_physical_properties(
1162                **render_kwargs
1163            )
1164
1165            if table_exists:
1166                self._migrate_target_table(
1167                    target_table_name=target_table_name,
1168                    snapshot=snapshot,
1169                    snapshots=snapshots,
1170                    deployability_index=deployability_index,
1171                    render_kwargs=render_kwargs,
1172                    rendered_physical_properties=rendered_physical_properties,
1173                    allow_destructive_snapshots=allow_destructive_snapshots,
1174                    allow_additive_snapshots=allow_additive_snapshots,
1175                    run_pre_post_statements=True,
1176                )
1177            else:
1178                self._execute_create(
1179                    snapshot=snapshot,
1180                    table_name=snapshot.table_name(is_deployable=True),
1181                    is_table_deployable=True,
1182                    deployability_index=deployability_index,
1183                    create_render_kwargs=render_kwargs,
1184                    rendered_physical_properties=rendered_physical_properties,
1185                    dry_run=True,
1186                )
1187
1188        evaluation_strategy.run_post_statements(
1189            snapshot=snapshot, render_kwargs={**render_kwargs, "inside_transaction": False}
1190        )
1191
1192    # Retry in case when the table is migrated concurrently from another plan application
1193    @retry(
1194        reraise=True,
1195        stop=stop_after_attempt(5),
1196        wait=wait_exponential(min=1, max=16),
1197        retry=retry_if_not_exception_type(
1198            (DestructiveChangeError, AdditiveChangeError, MigrationNotSupportedError)
1199        ),
1200    )
1201    def _migrate_target_table(
1202        self,
1203        target_table_name: str,
1204        snapshot: Snapshot,
1205        snapshots: t.Dict[str, Snapshot],
1206        deployability_index: DeployabilityIndex,
1207        render_kwargs: t.Dict[str, t.Any],
1208        rendered_physical_properties: t.Dict[str, exp.Expr],
1209        allow_destructive_snapshots: t.Set[str],
1210        allow_additive_snapshots: t.Set[str],
1211        run_pre_post_statements: bool = False,
1212    ) -> None:
1213        adapter = self.get_adapter(snapshot.model.gateway)
1214
1215        tmp_table = exp.to_table(target_table_name)
1216        tmp_table.this.set("this", f"{tmp_table.name}_schema_tmp")
1217        tmp_table_name = tmp_table.sql()
1218
1219        if snapshot.is_materialized:
1220            self._execute_create(
1221                snapshot=snapshot,
1222                table_name=tmp_table_name,
1223                is_table_deployable=False,
1224                deployability_index=deployability_index,
1225                create_render_kwargs=render_kwargs,
1226                rendered_physical_properties=rendered_physical_properties,
1227                dry_run=False,
1228                run_pre_post_statements=run_pre_post_statements,
1229                skip_grants=True,  # skip grants for tmp table
1230            )
1231        try:
1232            evaluation_strategy = _evaluation_strategy(snapshot, adapter)
1233            logger.info(
1234                "Migrating table schema from '%s' to '%s'",
1235                tmp_table_name,
1236                target_table_name,
1237            )
1238            evaluation_strategy.migrate(
1239                target_table_name=target_table_name,
1240                source_table_name=tmp_table_name,
1241                snapshot=snapshot,
1242                snapshots=snapshots,
1243                allow_destructive_snapshots=allow_destructive_snapshots,
1244                allow_additive_snapshots=allow_additive_snapshots,
1245                ignore_destructive=snapshot.model.on_destructive_change.is_ignore,
1246                ignore_additive=snapshot.model.on_additive_change.is_ignore,
1247                deployability_index=deployability_index,
1248            )
1249        finally:
1250            if snapshot.is_materialized:
1251                adapter.drop_table(tmp_table_name)
1252
1253    def _promote_snapshot(
1254        self,
1255        snapshot: Snapshot,
1256        environment_naming_info: EnvironmentNamingInfo,
1257        deployability_index: DeployabilityIndex,
1258        on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]],
1259        start: t.Optional[TimeLike] = None,
1260        end: t.Optional[TimeLike] = None,
1261        execution_time: t.Optional[TimeLike] = None,
1262        snapshots: t.Optional[t.Dict[SnapshotId, Snapshot]] = None,
1263        table_mapping: t.Optional[t.Dict[str, str]] = None,
1264    ) -> None:
1265        if not snapshot.is_model:
1266            return
1267
1268        adapter = (
1269            self.get_adapter(snapshot.model_gateway)
1270            if environment_naming_info.gateway_managed
1271            else self.adapter
1272        )
1273        table_name = snapshot.table_name(deployability_index.is_representative(snapshot))
1274        view_name = snapshot.qualified_view_name.for_environment(
1275            environment_naming_info, dialect=adapter.dialect
1276        )
1277        render_kwargs: t.Dict[str, t.Any] = dict(
1278            start=start,
1279            end=end,
1280            execution_time=execution_time,
1281            engine_adapter=adapter,
1282            deployability_index=deployability_index,
1283            table_mapping=table_mapping,
1284            runtime_stage=RuntimeStage.PROMOTING,
1285        )
1286
1287        with (
1288            adapter.transaction(),
1289            adapter.session(snapshot.model.render_session_properties(**render_kwargs)),
1290        ):
1291            _evaluation_strategy(snapshot, adapter).promote(
1292                table_name=table_name,
1293                view_name=view_name,
1294                model=snapshot.model,
1295                environment=environment_naming_info.name,
1296                snapshots=snapshots,
1297                snapshot=snapshot,
1298                **render_kwargs,
1299            )
1300
1301            snapshot_by_name = {s.name: s for s in (snapshots or {}).values()}
1302            render_kwargs["snapshots"] = snapshot_by_name
1303            adapter.execute(snapshot.model.render_on_virtual_update(**render_kwargs))
1304
1305        if on_complete is not None:
1306            on_complete(snapshot)
1307
1308    def _demote_snapshot(
1309        self,
1310        snapshot: Snapshot,
1311        environment_naming_info: EnvironmentNamingInfo,
1312        deployability_index: t.Optional[DeployabilityIndex],
1313        on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]],
1314        table_mapping: t.Optional[t.Dict[str, str]] = None,
1315    ) -> None:
1316        if not snapshot.is_model:
1317            return
1318
1319        adapter = (
1320            self.get_adapter(snapshot.model_gateway)
1321            if environment_naming_info.gateway_managed
1322            else self.adapter
1323        )
1324        view_name = snapshot.qualified_view_name.for_environment(
1325            environment_naming_info, dialect=adapter.dialect
1326        )
1327        with (
1328            adapter.transaction(),
1329            adapter.session(
1330                snapshot.model.render_session_properties(
1331                    engine_adapter=adapter,
1332                    deployability_index=deployability_index,
1333                    table_mapping=table_mapping,
1334                    runtime_stage=RuntimeStage.DEMOTING,
1335                )
1336            ),
1337        ):
1338            _evaluation_strategy(snapshot, adapter).demote(view_name)
1339
1340        if on_complete is not None:
1341            on_complete(snapshot)
1342
1343    def _cleanup_snapshot(
1344        self,
1345        snapshot: SnapshotInfoLike,
1346        dev_table_only: bool,
1347        adapter: EngineAdapter,
1348        on_complete: t.Optional[t.Callable[[str], None]],
1349    ) -> None:
1350        snapshot = snapshot.table_info
1351
1352        table_names = [(False, snapshot.table_name(is_deployable=False))]
1353        if not dev_table_only:
1354            table_names.append((True, snapshot.table_name(is_deployable=True)))
1355
1356        evaluation_strategy = _evaluation_strategy(snapshot, adapter)
1357        for is_table_deployable, table_name in table_names:
1358            try:
1359                evaluation_strategy.delete(
1360                    table_name,
1361                    is_table_deployable=is_table_deployable,
1362                    physical_schema=snapshot.physical_schema,
1363                    # we need to set cascade=true or we will get a 'cant drop because other objects depend on it'-style
1364                    # error on engines that enforce referential integrity, such as Postgres
1365                    # this situation can happen when a snapshot expires but downstream view snapshots that reference it have not yet expired
1366                    cascade=True,
1367                )
1368            except Exception:
1369                # Use `get_data_object` to check if the table exists instead of `table_exists` since the former
1370                # is based on `INFORMATION_SCHEMA` and avoids touching the table directly.
1371                # This is important when the table name is malformed for some reason and running any statement
1372                # that touches the table would result in an error.
1373                if adapter.get_data_object(table_name) is not None:
1374                    raise
1375                logger.warning(
1376                    "Skipping cleanup of table '%s' because it does not exist", table_name
1377                )
1378
1379            if on_complete is not None:
1380                on_complete(table_name)
1381
1382    def _audit(
1383        self,
1384        audit: Audit,
1385        audit_args: t.Dict[t.Any, t.Any],
1386        snapshot: Snapshot,
1387        snapshots: t.Dict[str, Snapshot],
1388        start: t.Optional[TimeLike],
1389        end: t.Optional[TimeLike],
1390        execution_time: t.Optional[TimeLike],
1391        deployability_index: t.Optional[DeployabilityIndex],
1392        **kwargs: t.Any,
1393    ) -> AuditResult:
1394        if audit.skip:
1395            return AuditResult(
1396                audit=audit,
1397                audit_args=audit_args,
1398                model=snapshot.model_or_none,
1399                skipped=True,
1400            )
1401
1402        # Model's "blocking" argument takes precedence over the audit's default setting
1403        blocking = audit_args.pop("blocking", None)
1404        blocking = blocking == exp.true() if blocking else audit.blocking
1405
1406        adapter = self.get_adapter(snapshot.model_gateway)
1407
1408        kwargs = {
1409            "start": start,
1410            "end": end,
1411            "execution_time": execution_time,
1412            "snapshots": snapshots,
1413            "deployability_index": deployability_index,
1414            "engine_adapter": adapter,
1415            "runtime_stage": RuntimeStage.AUDITING,
1416            **audit_args,
1417            **kwargs,
1418        }
1419
1420        if snapshot.is_model:
1421            query = snapshot.model.render_audit_query(audit, **kwargs)
1422        elif isinstance(audit, StandaloneAudit):
1423            query = audit.render_audit_query(**kwargs)
1424        else:
1425            raise SQLMeshError("Expected model or standalone audit. {snapshot}: {audit}")
1426
1427        count, *_ = adapter.fetchone(
1428            select("COUNT(*)").from_(query.subquery("audit")),
1429            quote_identifiers=True,
1430        )  # type: ignore
1431
1432        return AuditResult(
1433            audit=audit,
1434            audit_args=audit_args,
1435            model=snapshot.model_or_none,
1436            count=count,
1437            query=query,
1438            blocking=blocking,
1439        )
1440
1441    def _create_catalogs(
1442        self,
1443        tables: t.Iterable[t.Union[exp.Table, str]],
1444        gateway: t.Optional[str] = None,
1445    ) -> None:
1446        # attempt to create catalogs for the virtual layer if possible
1447        adapter = self.get_adapter(gateway)
1448        if adapter.SUPPORTS_CREATE_DROP_CATALOG:
1449            unique_catalogs = {t.catalog for t in [exp.to_table(maybe_t) for maybe_t in tables]}
1450            for catalog_name in unique_catalogs:
1451                adapter.create_catalog(catalog_name)
1452
1453    def _create_schemas(
1454        self,
1455        gateway_table_pairs: t.Iterable[t.Tuple[t.Optional[str], t.Union[exp.Table, str]]],
1456    ) -> None:
1457        table_exprs = [(gateway, exp.to_table(t)) for gateway, t in gateway_table_pairs]
1458        unique_schemas = {
1459            (gateway, t.args["db"], t.args.get("catalog"))
1460            for gateway, t in table_exprs
1461            if t and t.db
1462        }
1463
1464        def _create_schema(
1465            gateway: t.Optional[str], schema_name: str, catalog: t.Optional[str]
1466        ) -> None:
1467            schema = schema_(schema_name, catalog)
1468            logger.info("Creating schema '%s'", schema)
1469            adapter = self.get_adapter(gateway)
1470            adapter.create_schema(schema)
1471
1472        with self.concurrent_context():
1473            concurrent_apply_to_values(
1474                list(unique_schemas),
1475                lambda item: _create_schema(item[0], item[1], item[2]),
1476                self.ddl_concurrent_tasks,
1477            )
1478
1479    def get_adapter(self, gateway: t.Optional[str] = None) -> EngineAdapter:
1480        """Returns the adapter for the specified gateway or the default adapter if none is provided."""
1481        if gateway:
1482            if adapter := self.adapters.get(gateway):
1483                return adapter
1484            raise SQLMeshError(f"Gateway '{gateway}' not found in the available engine adapters.")
1485        return self.adapter
1486
1487    def _execute_create(
1488        self,
1489        snapshot: Snapshot,
1490        table_name: str,
1491        is_table_deployable: bool,
1492        deployability_index: DeployabilityIndex,
1493        create_render_kwargs: t.Dict[str, t.Any],
1494        rendered_physical_properties: t.Dict[str, exp.Expr],
1495        dry_run: bool,
1496        run_pre_post_statements: bool = True,
1497        skip_grants: bool = False,
1498    ) -> None:
1499        adapter = self.get_adapter(snapshot.model.gateway)
1500        evaluation_strategy = _evaluation_strategy(snapshot, adapter)
1501
1502        # It can still be useful for some strategies to know if the snapshot was actually deployable
1503        is_snapshot_deployable = deployability_index.is_deployable(snapshot)
1504        is_snapshot_representative = deployability_index.is_representative(snapshot)
1505
1506        create_render_kwargs = {
1507            **create_render_kwargs,
1508            "table_mapping": {snapshot.name: table_name},
1509        }
1510        if run_pre_post_statements:
1511            evaluation_strategy.run_pre_statements(
1512                snapshot=snapshot,
1513                render_kwargs={**create_render_kwargs, "inside_transaction": True},
1514            )
1515        evaluation_strategy.create(
1516            table_name=table_name,
1517            model=snapshot.model,
1518            is_table_deployable=is_table_deployable,
1519            skip_grants=skip_grants,
1520            render_kwargs=create_render_kwargs,
1521            is_snapshot_deployable=is_snapshot_deployable,
1522            is_snapshot_representative=is_snapshot_representative,
1523            dry_run=dry_run,
1524            physical_properties=rendered_physical_properties,
1525            snapshot=snapshot,
1526            deployability_index=deployability_index,
1527        )
1528        if run_pre_post_statements:
1529            evaluation_strategy.run_post_statements(
1530                snapshot=snapshot,
1531                render_kwargs={**create_render_kwargs, "inside_transaction": True},
1532            )
1533
1534    def _can_clone(self, snapshot: Snapshot, deployability_index: DeployabilityIndex) -> bool:
1535        adapter = self.get_adapter(snapshot.model.gateway)
1536        return (
1537            snapshot.is_forward_only
1538            and snapshot.is_materialized
1539            and bool(snapshot.previous_versions)
1540            and adapter.SUPPORTS_CLONING
1541            # managed models cannot have their schema mutated because they're based on queries, so clone + alter won't work
1542            and not snapshot.is_managed
1543            and not snapshot.is_dbt_custom
1544            and not deployability_index.is_deployable(snapshot)
1545            # If the deployable table is missing we can't clone it
1546            and adapter.table_exists(snapshot.table_name())
1547        )
1548
1549    def _get_physical_data_objects(
1550        self,
1551        target_snapshots: t.Iterable[Snapshot],
1552        deployability_index: DeployabilityIndex,
1553    ) -> t.Dict[SnapshotId, DataObject]:
1554        """Returns a dictionary of snapshot IDs to existing data objects of their physical tables.
1555
1556        Args:
1557            target_snapshots: Target snapshots.
1558            deployability_index: The deployability index to determine whether to look for a deployable or
1559                a non-deployable physical table.
1560
1561        Returns:
1562            A dictionary of snapshot IDs to existing data objects of their physical tables. If the data object
1563            for a snapshot is not found, it will not be included in the dictionary.
1564        """
1565        return self._get_data_objects(
1566            target_snapshots,
1567            lambda s: exp.to_table(
1568                s.table_name(deployability_index.is_deployable(s)), dialect=s.model.dialect
1569            ),
1570        )
1571
1572    def _get_virtual_data_objects(
1573        self,
1574        target_snapshots: t.Iterable[Snapshot],
1575        environment_naming_info: EnvironmentNamingInfo,
1576    ) -> t.Dict[SnapshotId, DataObject]:
1577        """Returns a dictionary of snapshot IDs to existing data objects of their virtual views.
1578
1579        Args:
1580            target_snapshots: Target snapshots.
1581             environment_naming_info: The environment naming info of the target virtual environment.
1582
1583        Returns:
1584            A dictionary of snapshot IDs to existing data objects of their virtual views. If the data object
1585            for a snapshot is not found, it will not be included in the dictionary.
1586        """
1587
1588        def _get_view_name(s: Snapshot) -> exp.Table:
1589            adapter = (
1590                self.get_adapter(s.model_gateway)
1591                if environment_naming_info.gateway_managed
1592                else self.adapter
1593            )
1594            return exp.to_table(
1595                s.qualified_view_name.for_environment(
1596                    environment_naming_info, dialect=adapter.dialect
1597                ),
1598                dialect=adapter.dialect,
1599            )
1600
1601        return self._get_data_objects(target_snapshots, _get_view_name)
1602
1603    def _get_data_objects(
1604        self,
1605        target_snapshots: t.Iterable[Snapshot],
1606        table_name_callable: t.Callable[[Snapshot], exp.Table],
1607    ) -> t.Dict[SnapshotId, DataObject]:
1608        """Returns a dictionary of snapshot IDs to existing data objects.
1609
1610        Args:
1611            target_snapshots: Target snapshots.
1612            table_name_callable: A function that takes a snapshot and returns the table to look for.
1613
1614        Returns:
1615            A dictionary of snapshot IDs to existing data objects. If the data object for a snapshot is not found,
1616            it will not be included in the dictionary.
1617        """
1618        tables_by_gateway_and_schema: t.Dict[t.Union[str, None], t.Dict[exp.Table, set[str]]] = (
1619            defaultdict(lambda: defaultdict(set))
1620        )
1621        snapshots_by_table_name: t.Dict[exp.Table, t.Dict[str, Snapshot]] = defaultdict(dict)
1622        for snapshot in target_snapshots:
1623            if not snapshot.is_model or snapshot.is_symbolic:
1624                continue
1625            table = table_name_callable(snapshot)
1626            table_schema = d.schema_(table.db, catalog=table.catalog)
1627            tables_by_gateway_and_schema[snapshot.model_gateway][table_schema].add(table.name)
1628            snapshots_by_table_name[table_schema][table.name] = snapshot
1629
1630        def _get_data_objects_in_schema(
1631            schema: exp.Table,
1632            object_names: t.Optional[t.Set[str]] = None,
1633            gateway: t.Optional[str] = None,
1634        ) -> t.List[DataObject]:
1635            logger.info("Listing data objects in schema %s", schema.sql())
1636            return self.get_adapter(gateway).get_data_objects(
1637                schema, object_names, safe_to_cache=True
1638            )
1639
1640        with self.concurrent_context():
1641            snapshot_id_to_obj: t.Dict[SnapshotId, DataObject] = {}
1642            # A schema can be shared across multiple engines, so we need to group tables by both gateway and schema
1643            for gateway, tables_by_schema in tables_by_gateway_and_schema.items():
1644                schema_list = list(tables_by_schema.keys())
1645                results = concurrent_apply_to_values(
1646                    schema_list,
1647                    lambda s: _get_data_objects_in_schema(
1648                        schema=s, object_names=tables_by_schema.get(s), gateway=gateway
1649                    ),
1650                    self.ddl_concurrent_tasks,
1651                )
1652
1653                for schema, objs in zip(schema_list, results):
1654                    snapshots_by_name = snapshots_by_table_name.get(schema, {})
1655                    for obj in objs:
1656                        if obj.name in snapshots_by_name:
1657                            snapshot_id_to_obj[snapshots_by_name[obj.name].snapshot_id] = obj
1658
1659        return snapshot_id_to_obj
1660
1661
1662def _evaluation_strategy(snapshot: SnapshotInfoLike, adapter: EngineAdapter) -> EvaluationStrategy:
1663    klass: t.Type
1664    if snapshot.is_embedded:
1665        klass = EmbeddedStrategy
1666    elif snapshot.is_symbolic or snapshot.is_audit:
1667        klass = SymbolicStrategy
1668    elif snapshot.is_full:
1669        klass = FullRefreshStrategy
1670    elif snapshot.is_seed:
1671        klass = SeedStrategy
1672    elif snapshot.is_incremental_by_time_range:
1673        klass = IncrementalByTimeRangeStrategy
1674    elif snapshot.is_incremental_by_unique_key:
1675        klass = IncrementalByUniqueKeyStrategy
1676    elif snapshot.is_incremental_by_partition:
1677        klass = IncrementalByPartitionStrategy
1678    elif snapshot.is_incremental_unmanaged:
1679        klass = IncrementalUnmanagedStrategy
1680    elif snapshot.is_view:
1681        klass = ViewStrategy
1682    elif snapshot.is_scd_type_2:
1683        klass = SCDType2Strategy
1684    elif snapshot.is_dbt_custom:
1685        if hasattr(snapshot, "model") and isinstance(
1686            (model_kind := snapshot.model.kind), DbtCustomKind
1687        ):
1688            return DbtCustomMaterializationStrategy(
1689                adapter=adapter,
1690                materialization_name=model_kind.materialization,
1691                materialization_template=model_kind.definition,
1692            )
1693
1694        raise SQLMeshError(
1695            f"Expected DbtCustomKind for dbt custom materialization in model '{snapshot.name}'"
1696        )
1697    elif snapshot.is_custom:
1698        if snapshot.custom_materialization is None:
1699            raise SQLMeshError(
1700                f"Missing the name of a custom evaluation strategy in model '{snapshot.name}'."
1701            )
1702        _, klass = get_custom_materialization_type_or_raise(snapshot.custom_materialization)
1703        return klass(adapter)
1704    elif snapshot.is_managed:
1705        klass = EngineManagedStrategy
1706    else:
1707        raise SQLMeshError(f"Unexpected snapshot: {snapshot}")
1708
1709    return klass(adapter)
1710
1711
1712class EvaluationStrategy(abc.ABC):
1713    def __init__(self, adapter: EngineAdapter):
1714        self.adapter = adapter
1715
1716    @abc.abstractmethod
1717    def insert(
1718        self,
1719        table_name: str,
1720        query_or_df: QueryOrDF,
1721        model: Model,
1722        is_first_insert: bool,
1723        render_kwargs: t.Dict[str, t.Any],
1724        **kwargs: t.Any,
1725    ) -> None:
1726        """Inserts the given query or a DataFrame into the target table or a view.
1727
1728        Args:
1729            table_name: The name of the target table or view.
1730            query_or_df: A query or a DataFrame to insert.
1731            model: The target model.
1732            is_first_insert: Whether this is the first insert for this version of a model. This value is set to True
1733                if no data has been previously inserted into the target table, or when the entire history of the target model has
1734                been restated. Note that in the latter case, the table might contain data from previous executions, and it is the
1735                responsibility of a specific evaluation strategy to handle the truncation of the table if necessary.
1736            render_kwargs: Additional key-value arguments to pass when rendering the model's query.
1737        """
1738
1739    @abc.abstractmethod
1740    def append(
1741        self,
1742        table_name: str,
1743        query_or_df: QueryOrDF,
1744        model: Model,
1745        render_kwargs: t.Dict[str, t.Any],
1746        **kwargs: t.Any,
1747    ) -> None:
1748        """Appends the given query or a DataFrame to the existing table.
1749
1750        Args:
1751            table_name: The target table name.
1752            query_or_df: A query or a DataFrame to insert.
1753            model: The target model.
1754            render_kwargs: Additional key-value arguments to pass when rendering the model's query.
1755        """
1756
1757    @abc.abstractmethod
1758    def create(
1759        self,
1760        table_name: str,
1761        model: Model,
1762        is_table_deployable: bool,
1763        render_kwargs: t.Dict[str, t.Any],
1764        skip_grants: bool,
1765        **kwargs: t.Any,
1766    ) -> None:
1767        """Creates the target table or view.
1768
1769        Note that the intention here is to just create the table structure, data is loaded in insert() and append()
1770
1771        Args:
1772            table_name: The name of a table or a view.
1773            model: The target model.
1774            is_table_deployable: True if this creation request is for the "main" table that *might* be deployed to a production environment.
1775                False if this creation request is for the "dev preview" table. Note that this flag is not related to the DeployabilityIndex
1776                which determines if the snapshot is deployable to production or not
1777            render_kwargs: Additional key-value arguments to pass when rendering the model's query.
1778        """
1779
1780    @abc.abstractmethod
1781    def migrate(
1782        self,
1783        target_table_name: str,
1784        source_table_name: str,
1785        snapshot: Snapshot,
1786        *,
1787        ignore_destructive: bool,
1788        ignore_additive: bool,
1789        **kwargs: t.Any,
1790    ) -> None:
1791        """Migrates the target table schema so that it corresponds to the source table schema.
1792
1793        Args:
1794            target_table_name: The target table name.
1795            source_table_name: The source table name.
1796            snapshot: The target snapshot.
1797            ignore_destructive: If True, destructive changes are not created when migrating.
1798                This is used for forward-only models that are being migrated to a new version.
1799            ignore_additive: If True, additive changes are not created when migrating.
1800                This is used for forward-only models that are being migrated to a new version.
1801        """
1802
1803    @abc.abstractmethod
1804    def delete(self, name: str, **kwargs: t.Any) -> None:
1805        """Deletes a target table or a view.
1806
1807        Args:
1808            name: The name of a table or a view.
1809        """
1810
1811    @abc.abstractmethod
1812    def promote(
1813        self,
1814        table_name: str,
1815        view_name: str,
1816        model: Model,
1817        environment: str,
1818        **kwargs: t.Any,
1819    ) -> None:
1820        """Updates the target view to point to the target table.
1821
1822        Args:
1823            table_name: The name of a table in the physical layer that is being promoted.
1824            view_name: The name of the target view in the virtual layer.
1825            model: The model that is being promoted.
1826            environment: The name of the target environment.
1827        """
1828
1829    @abc.abstractmethod
1830    def demote(self, view_name: str, **kwargs: t.Any) -> None:
1831        """Deletes the target view in the virtual layer.
1832
1833        Args:
1834            view_name: The name of the target view in the virtual layer.
1835        """
1836
1837    @abc.abstractmethod
1838    def run_pre_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
1839        """Executes the snapshot's pre statements.
1840
1841        Args:
1842            snapshot: The target snapshot.
1843            render_kwargs: Additional key-value arguments to pass when rendering the statements.
1844        """
1845
1846    @abc.abstractmethod
1847    def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
1848        """Executes the snapshot's post statements.
1849
1850        Args:
1851            snapshot: The target snapshot.
1852            render_kwargs: Additional key-value arguments to pass when rendering the statements.
1853        """
1854
1855    def _apply_grants(
1856        self,
1857        model: Model,
1858        table_name: str,
1859        target_layer: GrantsTargetLayer,
1860        is_snapshot_deployable: bool = False,
1861    ) -> None:
1862        """Apply grants for a model if grants are configured.
1863
1864        This method provides consistent grants application across all evaluation strategies.
1865        It ensures that whenever a physical database object (table, view, materialized view)
1866        is created or modified, the appropriate grants are applied.
1867
1868        Args:
1869            model: The SQLMesh model containing grants configuration
1870            table_name: The target table/view name to apply grants to
1871            target_layer: The grants application layer (physical or virtual)
1872            is_snapshot_deployable: Whether the snapshot is deployable (targeting production)
1873        """
1874        grants_config = model.grants
1875        if grants_config is None:
1876            return
1877
1878        if not self.adapter.SUPPORTS_GRANTS:
1879            logger.warning(
1880                f"Engine {self.adapter.__class__.__name__} does not support grants. "
1881                f"Skipping grants application for model {model.name}"
1882            )
1883            return
1884
1885        model_grants_target_layer = model.grants_target_layer
1886        deployable_vde_dev_only = (
1887            is_snapshot_deployable and model.virtual_environment_mode.is_dev_only
1888        )
1889
1890        # table_type is always a VIEW in the virtual layer unless model is deployable and VDE is dev_only
1891        # in which case we fall back to the model's model_grants_table_type
1892        if target_layer == GrantsTargetLayer.VIRTUAL and not deployable_vde_dev_only:
1893            model_grants_table_type = DataObjectType.VIEW
1894        else:
1895            model_grants_table_type = model.grants_table_type
1896
1897        if (
1898            model_grants_target_layer.is_all
1899            or model_grants_target_layer == target_layer
1900            # Always apply grants in production when VDE is dev_only regardless of target_layer
1901            # since only physical tables are created in production
1902            or deployable_vde_dev_only
1903        ):
1904            logger.info(f"Applying grants for model {model.name} to table {table_name}")
1905            self.adapter.sync_grants_config(
1906                exp.to_table(table_name, dialect=self.adapter.dialect),
1907                grants_config,
1908                model_grants_table_type,
1909            )
1910        else:
1911            logger.debug(
1912                f"Skipping grants application for model {model.name} in {target_layer} layer"
1913            )
1914
1915
1916class SymbolicStrategy(EvaluationStrategy):
1917    def insert(
1918        self,
1919        table_name: str,
1920        query_or_df: QueryOrDF,
1921        model: Model,
1922        is_first_insert: bool,
1923        render_kwargs: t.Dict[str, t.Any],
1924        **kwargs: t.Any,
1925    ) -> None:
1926        pass
1927
1928    def append(
1929        self,
1930        table_name: str,
1931        query_or_df: QueryOrDF,
1932        model: Model,
1933        render_kwargs: t.Dict[str, t.Any],
1934        **kwargs: t.Any,
1935    ) -> None:
1936        pass
1937
1938    def create(
1939        self,
1940        table_name: str,
1941        model: Model,
1942        is_table_deployable: bool,
1943        render_kwargs: t.Dict[str, t.Any],
1944        skip_grants: bool,
1945        **kwargs: t.Any,
1946    ) -> None:
1947        pass
1948
1949    def migrate(
1950        self,
1951        target_table_name: str,
1952        source_table_name: str,
1953        snapshot: Snapshot,
1954        *,
1955        ignore_destructive: bool,
1956        ignore_additive: bool,
1957        **kwarg: t.Any,
1958    ) -> None:
1959        pass
1960
1961    def delete(self, name: str, **kwargs: t.Any) -> None:
1962        pass
1963
1964    def promote(
1965        self,
1966        table_name: str,
1967        view_name: str,
1968        model: Model,
1969        environment: str,
1970        **kwargs: t.Any,
1971    ) -> None:
1972        pass
1973
1974    def demote(self, view_name: str, **kwargs: t.Any) -> None:
1975        pass
1976
1977    def run_pre_statements(self, snapshot: Snapshot, render_kwargs: t.Dict[str, t.Any]) -> None:
1978        pass
1979
1980    def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Dict[str, t.Any]) -> None:
1981        pass
1982
1983
1984class EmbeddedStrategy(SymbolicStrategy):
1985    def promote(
1986        self,
1987        table_name: str,
1988        view_name: str,
1989        model: Model,
1990        environment: str,
1991        **kwargs: t.Any,
1992    ) -> None:
1993        logger.info("Dropping view '%s' for non-materialized table", view_name)
1994        self.adapter.drop_view(view_name, cascade=False)
1995
1996
1997class PromotableStrategy(EvaluationStrategy, abc.ABC):
1998    def promote(
1999        self,
2000        table_name: str,
2001        view_name: str,
2002        model: Model,
2003        environment: str,
2004        **kwargs: t.Any,
2005    ) -> None:
2006        is_prod = environment == c.PROD
2007        logger.info("Updating view '%s' to point at table '%s'", view_name, table_name)
2008        render_kwargs: t.Dict[str, t.Any] = dict(
2009            start=kwargs.get("start"),
2010            end=kwargs.get("end"),
2011            execution_time=kwargs.get("execution_time"),
2012            engine_adapter=kwargs.get("engine_adapter"),
2013            snapshots=kwargs.get("snapshots"),
2014            deployability_index=kwargs.get("deployability_index"),
2015            table_mapping=kwargs.get("table_mapping"),
2016            runtime_stage=kwargs.get("runtime_stage"),
2017        )
2018        self.adapter.create_view(
2019            view_name,
2020            exp.select("*").from_(table_name, dialect=self.adapter.dialect),
2021            table_description=model.description if is_prod else None,
2022            column_descriptions=model.column_descriptions if is_prod else None,
2023            view_properties=model.render_virtual_properties(**render_kwargs),
2024        )
2025
2026        snapshot = kwargs.get("snapshot")
2027        deployability_index = kwargs.get("deployability_index")
2028        is_snapshot_deployable = (
2029            deployability_index.is_deployable(snapshot)
2030            if snapshot and deployability_index
2031            else False
2032        )
2033
2034        # Apply grants to the virtual layer (view) after promotion
2035        self._apply_grants(model, view_name, GrantsTargetLayer.VIRTUAL, is_snapshot_deployable)
2036
2037    def demote(self, view_name: str, **kwargs: t.Any) -> None:
2038        logger.info("Dropping view '%s'", view_name)
2039        self.adapter.drop_view(view_name, cascade=False)
2040
2041    def run_pre_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
2042        self.adapter.execute(snapshot.model.render_pre_statements(**render_kwargs))
2043
2044    def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
2045        self.adapter.execute(snapshot.model.render_post_statements(**render_kwargs))
2046
2047
2048def _adjust_physical_properties_for_engine(
2049    adapter: EngineAdapter,
2050    model: Model,
2051    physical_properties: t.Optional[t.Dict[str, t.Any]],
2052) -> t.Dict[str, t.Any]:
2053    """Let the target engine adjust/validate physical properties for an incremental model.
2054
2055    The generic responsibility here is to determine, from the model kind, whether the table will
2056    be the target of DELETE/MERGE statements (vs. append-only INSERTs) and whether its unique_key
2057    may be promoted to an engine-specific key. The engine adapter decides what, if anything, to do
2058    with that information (see ``EngineAdapter.adjust_physical_properties_for_incremental``).
2059    """
2060    kind = model.kind
2061
2062    # Only incremental kinds that issue DELETE/MERGE need a delete-capable table. Append-only
2063    # INCREMENTAL_UNMANAGED (insert_overwrite=False) only does INSERT, so it does not.
2064    requires_delete_capable_table = (
2065        kind.is_incremental_by_time_range
2066        or kind.is_incremental_by_unique_key
2067        or kind.is_incremental_by_partition
2068        or kind.is_scd_type_2
2069        or (isinstance(kind, IncrementalUnmanagedKind) and kind.insert_overwrite)
2070    )
2071
2072    return adapter.adjust_physical_properties_for_incremental(
2073        dict(physical_properties or {}),
2074        requires_delete_capable_table=requires_delete_capable_table,
2075        unique_key=model.unique_key if kind.is_incremental_by_unique_key else None,
2076        model_name=model.name,
2077    )
2078
2079
2080class MaterializableStrategy(PromotableStrategy, abc.ABC):
2081    def create(
2082        self,
2083        table_name: str,
2084        model: Model,
2085        is_table_deployable: bool,
2086        render_kwargs: t.Dict[str, t.Any],
2087        skip_grants: bool,
2088        **kwargs: t.Any,
2089    ) -> None:
2090        ctas_query = model.ctas_query(**render_kwargs)
2091        physical_properties = _adjust_physical_properties_for_engine(
2092            self.adapter, model, kwargs.get("physical_properties", model.physical_properties)
2093        )
2094
2095        logger.info("Creating table '%s'", table_name)
2096        if model.annotated:
2097            self.adapter.create_table(
2098                table_name,
2099                target_columns_to_types=model.columns_to_types_or_raise,
2100                table_format=model.table_format,
2101                storage_format=model.storage_format,
2102                partitioned_by=model.partitioned_by,
2103                partition_interval_unit=model.partition_interval_unit,
2104                clustered_by=model.clustered_by,
2105                table_properties=physical_properties,
2106                table_description=model.description if is_table_deployable else None,
2107                column_descriptions=model.column_descriptions if is_table_deployable else None,
2108            )
2109
2110            # If we create both temp and prod tables, we need to make sure that we dry run once.
2111            dry_run = kwargs.get("dry_run", True) or not is_table_deployable
2112
2113            # Only sql models have queries that can be tested.
2114            # We also need to make sure that we don't dry run on Redshift because its planner / optimizer sometimes
2115            # breaks on our CTAS queries due to us relying on the WHERE FALSE LIMIT 0 combo.
2116            if model.is_sql and dry_run and self.adapter.dialect != "redshift":
2117                logger.info("Dry running model '%s'", model.name)
2118                self.adapter.fetchall(ctas_query)
2119        else:
2120            self.adapter.ctas(
2121                table_name,
2122                ctas_query,
2123                model.columns_to_types,
2124                table_format=model.table_format,
2125                storage_format=model.storage_format,
2126                partitioned_by=model.partitioned_by,
2127                partition_interval_unit=model.partition_interval_unit,
2128                clustered_by=model.clustered_by,
2129                table_properties=physical_properties,
2130                table_description=model.description if is_table_deployable else None,
2131                column_descriptions=model.column_descriptions if is_table_deployable else None,
2132            )
2133
2134        # Apply grants after table creation (unless explicitly skipped by caller)
2135        if not skip_grants:
2136            is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2137            self._apply_grants(
2138                model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2139            )
2140
2141    def migrate(
2142        self,
2143        target_table_name: str,
2144        source_table_name: str,
2145        snapshot: Snapshot,
2146        *,
2147        ignore_destructive: bool,
2148        ignore_additive: bool,
2149        **kwargs: t.Any,
2150    ) -> None:
2151        logger.info(f"Altering table '{target_table_name}'")
2152        alter_operations = self.adapter.get_alter_operations(
2153            target_table_name,
2154            source_table_name,
2155            ignore_destructive=ignore_destructive,
2156            ignore_additive=ignore_additive,
2157        )
2158        _check_destructive_schema_change(
2159            snapshot, alter_operations, kwargs["allow_destructive_snapshots"]
2160        )
2161        _check_additive_schema_change(
2162            snapshot, alter_operations, kwargs["allow_additive_snapshots"]
2163        )
2164        self.adapter.alter_table(alter_operations)
2165
2166        # Apply grants after schema migration
2167        deployability_index = kwargs.get("deployability_index")
2168        is_snapshot_deployable = (
2169            deployability_index.is_deployable(snapshot) if deployability_index else False
2170        )
2171        self._apply_grants(
2172            snapshot.model, target_table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2173        )
2174
2175    def delete(self, name: str, **kwargs: t.Any) -> None:
2176        _check_table_db_is_physical_schema(name, kwargs["physical_schema"])
2177        self.adapter.drop_table(name, cascade=kwargs.pop("cascade", False))
2178        logger.info("Dropped table '%s'", name)
2179
2180    def _replace_query_for_model(
2181        self,
2182        model: Model,
2183        name: str,
2184        query_or_df: QueryOrDF,
2185        render_kwargs: t.Dict[str, t.Any],
2186        skip_grants: bool = False,
2187        **kwargs: t.Any,
2188    ) -> None:
2189        """Replaces the table for the given model.
2190
2191        Args:
2192            model: The target model.
2193            name: The name of the target table.
2194            query_or_df: The query or DataFrame to replace the target table with.
2195        """
2196        if (model.is_seed or model.kind.is_full) and model.annotated:
2197            columns_to_types = model.columns_to_types_or_raise
2198            source_columns: t.Optional[t.List[str]] = list(columns_to_types)
2199        else:
2200            try:
2201                # Source columns from the underlying table to prevent unintentional table schema changes during restatement of incremental models.
2202                columns_to_types, source_columns = self._get_target_and_source_columns(
2203                    model, name, render_kwargs, force_get_columns_from_target=True
2204                )
2205            except Exception:
2206                columns_to_types, source_columns = None, None
2207
2208        physical_properties = _adjust_physical_properties_for_engine(
2209            self.adapter, model, kwargs.get("physical_properties", model.physical_properties)
2210        )
2211        self.adapter.replace_query(
2212            name,
2213            query_or_df,
2214            table_format=model.table_format,
2215            storage_format=model.storage_format,
2216            partitioned_by=model.partitioned_by,
2217            partition_interval_unit=model.partition_interval_unit,
2218            clustered_by=model.clustered_by,
2219            table_properties=physical_properties,
2220            table_description=model.description,
2221            column_descriptions=model.column_descriptions,
2222            target_columns_to_types=columns_to_types,
2223            source_columns=source_columns,
2224        )
2225
2226        # Apply grants after table replacement (unless explicitly skipped by caller)
2227        if not skip_grants:
2228            is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2229            self._apply_grants(model, name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable)
2230
2231    def _get_target_and_source_columns(
2232        self,
2233        model: Model,
2234        table_name: str,
2235        render_kwargs: t.Dict[str, t.Any],
2236        force_get_columns_from_target: bool = False,
2237    ) -> t.Tuple[t.Dict[str, exp.DataType], t.Optional[t.List[str]]]:
2238        if force_get_columns_from_target:
2239            target_column_to_types = self.adapter.columns(table_name)
2240        else:
2241            target_column_to_types = (
2242                model.columns_to_types  # type: ignore
2243                if model.annotated
2244                and not model.on_destructive_change.is_ignore
2245                and not model.on_additive_change.is_ignore
2246                else self.adapter.columns(table_name)
2247            )
2248        assert target_column_to_types is not None
2249        if model.on_destructive_change.is_ignore or model.on_additive_change.is_ignore:
2250            # We need to identify the columns that are only in the source so we create an empty table with
2251            # the user query to determine that
2252            temp_table_name = exp.table_(
2253                "diff",
2254                db=model.physical_schema,
2255            )
2256            with self.adapter.temp_table(
2257                model.ctas_query(**render_kwargs), name=temp_table_name
2258            ) as temp_table:
2259                source_columns = list(self.adapter.columns(temp_table))
2260        else:
2261            source_columns = None
2262        return target_column_to_types, source_columns
2263
2264
2265class IncrementalStrategy(MaterializableStrategy, abc.ABC):
2266    def append(
2267        self,
2268        table_name: str,
2269        query_or_df: QueryOrDF,
2270        model: Model,
2271        render_kwargs: t.Dict[str, t.Any],
2272        **kwargs: t.Any,
2273    ) -> None:
2274        columns_to_types, source_columns = self._get_target_and_source_columns(
2275            model, table_name, render_kwargs=render_kwargs
2276        )
2277        self.adapter.insert_append(
2278            table_name,
2279            query_or_df,
2280            target_columns_to_types=columns_to_types,
2281            source_columns=source_columns,
2282        )
2283
2284
2285class IncrementalByPartitionStrategy(IncrementalStrategy):
2286    def insert(
2287        self,
2288        table_name: str,
2289        query_or_df: QueryOrDF,
2290        model: Model,
2291        is_first_insert: bool,
2292        render_kwargs: t.Dict[str, t.Any],
2293        **kwargs: t.Any,
2294    ) -> None:
2295        if is_first_insert:
2296            self._replace_query_for_model(model, table_name, query_or_df, render_kwargs, **kwargs)
2297        else:
2298            columns_to_types, source_columns = self._get_target_and_source_columns(
2299                model, table_name, render_kwargs=render_kwargs
2300            )
2301            self.adapter.insert_overwrite_by_partition(
2302                table_name,
2303                query_or_df,
2304                partitioned_by=model.partitioned_by,
2305                target_columns_to_types=columns_to_types,
2306                source_columns=source_columns,
2307            )
2308
2309
2310class IncrementalByTimeRangeStrategy(IncrementalStrategy):
2311    def insert(
2312        self,
2313        table_name: str,
2314        query_or_df: QueryOrDF,
2315        model: Model,
2316        is_first_insert: bool,
2317        render_kwargs: t.Dict[str, t.Any],
2318        **kwargs: t.Any,
2319    ) -> None:
2320        assert model.time_column
2321        columns_to_types, source_columns = self._get_target_and_source_columns(
2322            model, table_name, render_kwargs=render_kwargs
2323        )
2324        self.adapter.insert_overwrite_by_time_partition(
2325            table_name,
2326            query_or_df,
2327            time_formatter=model.convert_to_time_column,
2328            time_column=model.time_column,
2329            target_columns_to_types=columns_to_types,
2330            source_columns=source_columns,
2331            **kwargs,
2332        )
2333
2334
2335class IncrementalByUniqueKeyStrategy(IncrementalStrategy):
2336    def insert(
2337        self,
2338        table_name: str,
2339        query_or_df: QueryOrDF,
2340        model: Model,
2341        is_first_insert: bool,
2342        render_kwargs: t.Dict[str, t.Any],
2343        **kwargs: t.Any,
2344    ) -> None:
2345        if is_first_insert:
2346            self._replace_query_for_model(model, table_name, query_or_df, render_kwargs, **kwargs)
2347        else:
2348            columns_to_types, source_columns = self._get_target_and_source_columns(
2349                model,
2350                table_name,
2351                render_kwargs=render_kwargs,
2352            )
2353            physical_properties = _adjust_physical_properties_for_engine(
2354                self.adapter, model, kwargs.get("physical_properties", model.physical_properties)
2355            )
2356            self.adapter.merge(
2357                table_name,
2358                query_or_df,
2359                target_columns_to_types=columns_to_types,
2360                unique_key=model.unique_key,
2361                when_matched=model.when_matched,
2362                merge_filter=model.render_merge_filter(
2363                    start=kwargs.get("start"),
2364                    end=kwargs.get("end"),
2365                    execution_time=kwargs.get("execution_time"),
2366                ),
2367                physical_properties=physical_properties,
2368                source_columns=source_columns,
2369            )
2370
2371    def append(
2372        self,
2373        table_name: str,
2374        query_or_df: QueryOrDF,
2375        model: Model,
2376        render_kwargs: t.Dict[str, t.Any],
2377        **kwargs: t.Any,
2378    ) -> None:
2379        columns_to_types, source_columns = self._get_target_and_source_columns(
2380            model, table_name, render_kwargs=render_kwargs
2381        )
2382        physical_properties = _adjust_physical_properties_for_engine(
2383            self.adapter, model, kwargs.get("physical_properties", model.physical_properties)
2384        )
2385        self.adapter.merge(
2386            table_name,
2387            query_or_df,
2388            target_columns_to_types=columns_to_types,
2389            unique_key=model.unique_key,
2390            when_matched=model.when_matched,
2391            merge_filter=model.render_merge_filter(
2392                start=kwargs.get("start"),
2393                end=kwargs.get("end"),
2394                execution_time=kwargs.get("execution_time"),
2395            ),
2396            physical_properties=physical_properties,
2397            source_columns=source_columns,
2398        )
2399
2400
2401class IncrementalUnmanagedStrategy(IncrementalStrategy):
2402    def append(
2403        self,
2404        table_name: str,
2405        query_or_df: QueryOrDF,
2406        model: Model,
2407        render_kwargs: t.Dict[str, t.Any],
2408        **kwargs: t.Any,
2409    ) -> None:
2410        columns_to_types, source_columns = self._get_target_and_source_columns(
2411            model, table_name, render_kwargs=render_kwargs
2412        )
2413        self.adapter.insert_append(
2414            table_name,
2415            query_or_df,
2416            target_columns_to_types=columns_to_types,
2417            source_columns=source_columns,
2418        )
2419
2420    def insert(
2421        self,
2422        table_name: str,
2423        query_or_df: QueryOrDF,
2424        model: Model,
2425        is_first_insert: bool,
2426        render_kwargs: t.Dict[str, t.Any],
2427        **kwargs: t.Any,
2428    ) -> None:
2429        if is_first_insert:
2430            return self._replace_query_for_model(
2431                model, table_name, query_or_df, render_kwargs, **kwargs
2432            )
2433        if isinstance(model.kind, IncrementalUnmanagedKind) and model.kind.insert_overwrite:
2434            columns_to_types, source_columns = self._get_target_and_source_columns(
2435                model,
2436                table_name,
2437                render_kwargs=render_kwargs,
2438            )
2439
2440            return self.adapter.insert_overwrite_by_partition(
2441                table_name,
2442                query_or_df,
2443                model.partitioned_by,
2444                target_columns_to_types=columns_to_types,
2445                source_columns=source_columns,
2446            )
2447        return self.append(
2448            table_name,
2449            query_or_df,
2450            model,
2451            render_kwargs=render_kwargs,
2452            **kwargs,
2453        )
2454
2455
2456class FullRefreshStrategy(MaterializableStrategy):
2457    def append(
2458        self,
2459        table_name: str,
2460        query_or_df: QueryOrDF,
2461        model: Model,
2462        render_kwargs: t.Dict[str, t.Any],
2463        **kwargs: t.Any,
2464    ) -> None:
2465        self.adapter.insert_append(
2466            table_name,
2467            query_or_df,
2468            target_columns_to_types=model.columns_to_types,
2469        )
2470
2471    def insert(
2472        self,
2473        table_name: str,
2474        query_or_df: QueryOrDF,
2475        model: Model,
2476        is_first_insert: bool,
2477        render_kwargs: t.Dict[str, t.Any],
2478        **kwargs: t.Any,
2479    ) -> None:
2480        self._replace_query_for_model(model, table_name, query_or_df, render_kwargs, **kwargs)
2481
2482
2483class SeedStrategy(MaterializableStrategy):
2484    def create(
2485        self,
2486        table_name: str,
2487        model: Model,
2488        is_table_deployable: bool,
2489        render_kwargs: t.Dict[str, t.Any],
2490        skip_grants: bool,
2491        **kwargs: t.Any,
2492    ) -> None:
2493        model = t.cast(SeedModel, model)
2494        if not model.is_hydrated and self.adapter.table_exists(table_name):
2495            # This likely means that the table was created and populated previously, but the evaluation stage
2496            # failed before the interval could be added for this model.
2497            logger.warning(
2498                "Seed model '%s' is not hydrated, but the table '%s' exists. Skipping creation",
2499                model.name,
2500                table_name,
2501            )
2502            return
2503
2504        super().create(
2505            table_name,
2506            model,
2507            is_table_deployable,
2508            render_kwargs,
2509            skip_grants=True,  # Skip grants; they're applied after data insertion
2510            **kwargs,
2511        )
2512        # For seeds we insert data at the time of table creation.
2513        try:
2514            for index, df in enumerate(model.render_seed()):
2515                if index == 0:
2516                    self._replace_query_for_model(
2517                        model,
2518                        table_name,
2519                        df,
2520                        render_kwargs,
2521                        skip_grants=True,  # Skip grants; they're applied after data insertion
2522                        **kwargs,
2523                    )
2524                else:
2525                    self.adapter.insert_append(
2526                        table_name, df, target_columns_to_types=model.columns_to_types
2527                    )
2528
2529            if not skip_grants:
2530                # Apply grants after seed table creation and data insertion
2531                is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2532                self._apply_grants(
2533                    model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2534                )
2535        except Exception:
2536            self.adapter.drop_table(table_name)
2537            raise
2538
2539    def migrate(
2540        self,
2541        target_table_name: str,
2542        source_table_name: str,
2543        snapshot: Snapshot,
2544        *,
2545        ignore_destructive: bool,
2546        ignore_additive: bool,
2547        **kwargs: t.Any,
2548    ) -> None:
2549        raise NotImplementedError("Seeds do not support migrations.")
2550
2551    def insert(
2552        self,
2553        table_name: str,
2554        query_or_df: QueryOrDF,
2555        model: Model,
2556        is_first_insert: bool,
2557        render_kwargs: t.Dict[str, t.Any],
2558        **kwargs: t.Any,
2559    ) -> None:
2560        # Data has already been inserted at the time of table creation.
2561        pass
2562
2563    def append(
2564        self,
2565        table_name: str,
2566        query_or_df: QueryOrDF,
2567        model: Model,
2568        render_kwargs: t.Dict[str, t.Any],
2569        **kwargs: t.Any,
2570    ) -> None:
2571        # Data has already been inserted at the time of table creation.
2572        pass
2573
2574
2575class SCDType2Strategy(IncrementalStrategy):
2576    def create(
2577        self,
2578        table_name: str,
2579        model: Model,
2580        is_table_deployable: bool,
2581        render_kwargs: t.Dict[str, t.Any],
2582        skip_grants: bool,
2583        **kwargs: t.Any,
2584    ) -> None:
2585        assert isinstance(model.kind, (SCDType2ByTimeKind, SCDType2ByColumnKind))
2586        if model.annotated:
2587            logger.info("Creating table '%s'", table_name)
2588            columns_to_types = model.columns_to_types_or_raise
2589            if isinstance(model.kind, SCDType2ByTimeKind):
2590                columns_to_types[model.kind.updated_at_name.name] = model.kind.time_data_type
2591            physical_properties = _adjust_physical_properties_for_engine(
2592                self.adapter, model, kwargs.get("physical_properties", model.physical_properties)
2593            )
2594            self.adapter.create_table(
2595                table_name,
2596                target_columns_to_types=columns_to_types,
2597                table_format=model.table_format,
2598                storage_format=model.storage_format,
2599                partitioned_by=model.partitioned_by,
2600                partition_interval_unit=model.partition_interval_unit,
2601                clustered_by=model.clustered_by,
2602                table_properties=physical_properties,
2603                table_description=model.description if is_table_deployable else None,
2604                column_descriptions=model.column_descriptions if is_table_deployable else None,
2605            )
2606        else:
2607            # We assume that the data type for `updated_at_name` matches the data type that is defined for
2608            # `time_data_type`. If that isn't the case, then the user might get an error about not being able
2609            # to do comparisons across different data types
2610            super().create(
2611                table_name,
2612                model,
2613                is_table_deployable,
2614                render_kwargs,
2615                skip_grants,
2616                **kwargs,
2617            )
2618
2619        if not skip_grants:
2620            # Apply grants after SCD Type 2 table creation
2621            is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2622            self._apply_grants(
2623                model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2624            )
2625
2626    def insert(
2627        self,
2628        table_name: str,
2629        query_or_df: QueryOrDF,
2630        model: Model,
2631        is_first_insert: bool,
2632        render_kwargs: t.Dict[str, t.Any],
2633        **kwargs: t.Any,
2634    ) -> None:
2635        # Source columns from the underlying table to prevent unintentional table schema changes during restatement of incremental models.
2636        columns_to_types, source_columns = self._get_target_and_source_columns(
2637            model,
2638            table_name,
2639            render_kwargs=render_kwargs,
2640            force_get_columns_from_target=True,
2641        )
2642        if isinstance(model.kind, SCDType2ByTimeKind):
2643            self.adapter.scd_type_2_by_time(
2644                target_table=table_name,
2645                source_table=query_or_df,
2646                unique_key=model.unique_key,
2647                valid_from_col=model.kind.valid_from_name,
2648                valid_to_col=model.kind.valid_to_name,
2649                execution_time=kwargs["execution_time"],
2650                updated_at_col=model.kind.updated_at_name,
2651                invalidate_hard_deletes=model.kind.invalidate_hard_deletes,
2652                updated_at_as_valid_from=model.kind.updated_at_as_valid_from,
2653                target_columns_to_types=columns_to_types,
2654                table_format=model.table_format,
2655                table_description=model.description,
2656                column_descriptions=model.column_descriptions,
2657                truncate=is_first_insert,
2658                source_columns=source_columns,
2659                storage_format=model.storage_format,
2660                partitioned_by=model.partitioned_by,
2661                partition_interval_unit=model.partition_interval_unit,
2662                clustered_by=model.clustered_by,
2663                table_properties=kwargs.get("physical_properties", model.physical_properties),
2664            )
2665        elif isinstance(model.kind, SCDType2ByColumnKind):
2666            self.adapter.scd_type_2_by_column(
2667                target_table=table_name,
2668                source_table=query_or_df,
2669                unique_key=model.unique_key,
2670                valid_from_col=model.kind.valid_from_name,
2671                valid_to_col=model.kind.valid_to_name,
2672                execution_time=model.kind.updated_at_name or kwargs["execution_time"],
2673                check_columns=model.kind.columns,
2674                invalidate_hard_deletes=model.kind.invalidate_hard_deletes,
2675                execution_time_as_valid_from=model.kind.execution_time_as_valid_from,
2676                target_columns_to_types=columns_to_types,
2677                table_format=model.table_format,
2678                table_description=model.description,
2679                column_descriptions=model.column_descriptions,
2680                truncate=is_first_insert,
2681                source_columns=source_columns,
2682                storage_format=model.storage_format,
2683                partitioned_by=model.partitioned_by,
2684                partition_interval_unit=model.partition_interval_unit,
2685                clustered_by=model.clustered_by,
2686                table_properties=kwargs.get("physical_properties", model.physical_properties),
2687            )
2688        else:
2689            raise SQLMeshError(
2690                f"Unexpected SCD Type 2 kind: {model.kind}. This is not expected and please report this as a bug."
2691            )
2692
2693        # Apply grants after SCD Type 2 table recreation
2694        is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2695        self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable)
2696
2697    def append(
2698        self,
2699        table_name: str,
2700        query_or_df: QueryOrDF,
2701        model: Model,
2702        render_kwargs: t.Dict[str, t.Any],
2703        **kwargs: t.Any,
2704    ) -> None:
2705        return self.insert(
2706            table_name,
2707            query_or_df,
2708            model,
2709            is_first_insert=False,
2710            render_kwargs=render_kwargs,
2711            **kwargs,
2712        )
2713
2714
2715class ViewStrategy(PromotableStrategy):
2716    def insert(
2717        self,
2718        table_name: str,
2719        query_or_df: QueryOrDF,
2720        model: Model,
2721        is_first_insert: bool,
2722        render_kwargs: t.Dict[str, t.Any],
2723        **kwargs: t.Any,
2724    ) -> None:
2725        # We should recreate MVs across supported engines (Snowflake, BigQuery etc) because
2726        # if upstream tables were recreated (e.g FULL models), the MVs would be silently invalidated.
2727        # The only exception to that rule is RisingWave which doesn't support CREATE OR REPLACE, so upstream
2728        # models don't recreate their physical tables for the MVs to be invalidated.
2729        # However, even for RW we still want to recreate MVs to avoid stale references, as is the case with normal views.
2730        # The flag is_first_insert is used for that matter as a signal to recreate the MV if the snapshot's intervals
2731        # have been cleared by `should_force_rebuild`
2732        is_materialized_view = self._is_materialized_view(model)
2733        must_recreate_view = not self.adapter.HAS_VIEW_BINDING or (
2734            is_materialized_view and is_first_insert
2735        )
2736
2737        # Some engines (e.g. StarRocks) maintain materialized views automatically (via auto/scheduled
2738        # REFRESH) and can only recreate them via a destructive DROP + CREATE, which deletes the
2739        # materialized data and forces a full rebuild. For those, an existing MV must not be recreated
2740        # on routine evaluation (e.g. every `sqlmesh run`); only build it on the first insert (a new
2741        # version) or when a rebuild is explicitly forced (intervals cleared by `should_force_rebuild`,
2742        # which sets `is_first_insert`).
2743        if (
2744            is_materialized_view
2745            and not is_first_insert
2746            and not self.adapter.RECREATE_MATERIALIZED_VIEW_ON_EVALUATION
2747        ):
2748            must_recreate_view = False
2749
2750        if self.adapter.table_exists(table_name) and not must_recreate_view:
2751            logger.info("Skipping creation of the view '%s'", table_name)
2752            return
2753
2754        logger.info("Replacing view '%s'", table_name)
2755        materialized_properties = None
2756        if is_materialized_view:
2757            materialized_properties = {
2758                "partitioned_by": model.partitioned_by,
2759                "partition_interval_unit": model.partition_interval_unit,
2760                "clustered_by": model.clustered_by,
2761                "has_audits": bool(model.audits_with_args),
2762            }
2763        self.adapter.create_view(
2764            table_name,
2765            query_or_df,
2766            model.columns_to_types,
2767            replace=must_recreate_view,
2768            materialized=is_materialized_view,
2769            materialized_properties=materialized_properties,
2770            view_properties=kwargs.get("physical_properties", model.physical_properties),
2771            table_description=model.description,
2772            column_descriptions=model.column_descriptions,
2773        )
2774
2775        # Apply grants after view creation / replacement
2776        is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2777        self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable)
2778
2779    def append(
2780        self,
2781        table_name: str,
2782        query_or_df: QueryOrDF,
2783        model: Model,
2784        render_kwargs: t.Dict[str, t.Any],
2785        **kwargs: t.Any,
2786    ) -> None:
2787        raise ConfigError(f"Cannot append to a view '{table_name}'.")
2788
2789    def create(
2790        self,
2791        table_name: str,
2792        model: Model,
2793        is_table_deployable: bool,
2794        render_kwargs: t.Dict[str, t.Any],
2795        skip_grants: bool,
2796        **kwargs: t.Any,
2797    ) -> None:
2798        is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2799
2800        if self.adapter.table_exists(table_name):
2801            # Make sure we don't recreate the view to prevent deletion of downstream views in engines with no late
2802            # binding support (because of DROP CASCADE).
2803            logger.info("View '%s' already exists", table_name)
2804
2805            if not skip_grants:
2806                # Always apply grants when present, even if view exists, to handle grants updates
2807                self._apply_grants(
2808                    model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2809                )
2810            return
2811
2812        logger.info("Creating view '%s'", table_name)
2813        materialized = self._is_materialized_view(model)
2814        materialized_properties = None
2815        if materialized:
2816            materialized_properties = {
2817                "partitioned_by": model.partitioned_by,
2818                "clustered_by": model.clustered_by,
2819                "partition_interval_unit": model.partition_interval_unit,
2820                "has_audits": bool(model.audits_with_args),
2821            }
2822        self.adapter.create_view(
2823            table_name,
2824            model.render_query_or_raise(**render_kwargs),
2825            # Make sure we never replace the view during creation to avoid race conditions in engines with no late binding support.
2826            replace=False,
2827            materialized=self._is_materialized_view(model),
2828            materialized_properties=materialized_properties,
2829            view_properties=kwargs.get("physical_properties", model.physical_properties),
2830            table_description=model.description if is_table_deployable else None,
2831            column_descriptions=model.column_descriptions if is_table_deployable else None,
2832        )
2833
2834        if not skip_grants:
2835            # Apply grants after view creation
2836            self._apply_grants(
2837                model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2838            )
2839
2840    def migrate(
2841        self,
2842        target_table_name: str,
2843        source_table_name: str,
2844        snapshot: Snapshot,
2845        *,
2846        ignore_destructive: bool,
2847        ignore_additive: bool,
2848        **kwargs: t.Any,
2849    ) -> None:
2850        logger.info("Migrating view '%s'", target_table_name)
2851        model = snapshot.model
2852        render_kwargs = dict(
2853            execution_time=now(), snapshots=kwargs["snapshots"], engine_adapter=self.adapter
2854        )
2855
2856        is_materialized_view = self._is_materialized_view(model)
2857        materialized_properties = None
2858        if is_materialized_view:
2859            materialized_properties = {
2860                "partitioned_by": model.partitioned_by,
2861                "clustered_by": model.clustered_by,
2862                "partition_interval_unit": model.partition_interval_unit,
2863                "has_audits": bool(model.audits_with_args),
2864            }
2865
2866        self.adapter.create_view(
2867            target_table_name,
2868            model.render_query_or_raise(**render_kwargs),
2869            model.columns_to_types,
2870            materialized=is_materialized_view,
2871            materialized_properties=materialized_properties,
2872            view_properties=model.render_physical_properties(**render_kwargs),
2873            table_description=model.description,
2874            column_descriptions=model.column_descriptions,
2875        )
2876
2877        # Apply grants after view migration
2878        deployability_index = kwargs.get("deployability_index")
2879        is_snapshot_deployable = (
2880            deployability_index.is_deployable(snapshot) if deployability_index else False
2881        )
2882        self._apply_grants(
2883            snapshot.model, target_table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2884        )
2885
2886    def delete(self, name: str, **kwargs: t.Any) -> None:
2887        cascade = kwargs.pop("cascade", False)
2888        try:
2889            # Some engines (e.g., RisingWave) don’t fail when dropping a materialized view with a DROP VIEW statement,
2890            # because views and materialized views don’t share the same namespace. Therefore, we should not ignore if the
2891            # view doesn't exist and let the exception handler attempt to drop the materialized view.
2892            self.adapter.drop_view(name, cascade=cascade, ignore_if_not_exists=False)
2893        except Exception:
2894            logger.debug(
2895                "Failed to drop view '%s'. Trying to drop the materialized view instead",
2896                name,
2897                exc_info=True,
2898            )
2899            self.adapter.drop_view(
2900                name, materialized=True, cascade=cascade, ignore_if_not_exists=True
2901            )
2902        logger.info("Dropped view '%s'", name)
2903
2904    def _is_materialized_view(self, model: Model) -> bool:
2905        return isinstance(model.kind, ViewKind) and model.kind.materialized
2906
2907
2908C = t.TypeVar("C", bound=CustomKind)
2909
2910
2911class CustomMaterialization(IncrementalStrategy, t.Generic[C]):
2912    """Base class for custom materializations."""
2913
2914    def insert(
2915        self,
2916        table_name: str,
2917        query_or_df: QueryOrDF,
2918        model: Model,
2919        is_first_insert: bool,
2920        render_kwargs: t.Dict[str, t.Any],
2921        **kwargs: t.Any,
2922    ) -> None:
2923        """Inserts the given query or a DataFrame into the target table or a view.
2924
2925        Args:
2926            table_name: The name of the target table or view.
2927            query_or_df: A query or a DataFrame to insert.
2928            model: The target model.
2929            is_first_insert: Whether this is the first insert for this version of a model. This value is set to True
2930                if no data has been previously inserted into the target table, or when the entire history of the target model has
2931                been restated. Note that in the latter case, the table might contain data from previous executions, and it is the
2932                responsibility of a specific evaluation strategy to handle the truncation of the table if necessary.
2933            render_kwargs: Additional key-value arguments to pass when rendering the model's query.
2934        """
2935        raise NotImplementedError(
2936            "Custom materialization strategies must implement the 'insert' method."
2937        )
2938
2939
2940_custom_materialization_type_cache: t.Optional[
2941    t.Dict[str, t.Tuple[t.Type[CustomKind], t.Type[CustomMaterialization]]]
2942] = None
2943
2944
2945def get_custom_materialization_kind_type(st: t.Type[CustomMaterialization]) -> t.Type[CustomKind]:
2946    # try to read if there is a custom 'kind' type in use by inspecting the type signature
2947    # eg try to read 'MyCustomKind' from:
2948    # >>>> class MyCustomMaterialization(CustomMaterialization[MyCustomKind])
2949    # and fall back to base CustomKind if there is no generic type declared
2950    if hasattr(st, "__orig_bases__"):
2951        for base in st.__orig_bases__:
2952            if hasattr(base, "__origin__") and base.__origin__ == CustomMaterialization:
2953                for generic_arg in t.get_args(base):
2954                    if not issubclass(generic_arg, CustomKind):
2955                        raise SQLMeshError(
2956                            f"Custom materialization kind '{generic_arg.__name__}' must be a subclass of CustomKind"
2957                        )
2958
2959                    return generic_arg
2960
2961    return CustomKind
2962
2963
2964def get_custom_materialization_type(
2965    name: str, raise_errors: bool = True
2966) -> t.Optional[t.Tuple[t.Type[CustomKind], t.Type[CustomMaterialization]]]:
2967    global _custom_materialization_type_cache
2968
2969    strategy_key = name.lower()
2970
2971    try:
2972        if (
2973            _custom_materialization_type_cache is None
2974            or strategy_key not in _custom_materialization_type_cache
2975        ):
2976            strategy_types = list(CustomMaterialization.__subclasses__())
2977
2978            entry_points = metadata.entry_points(group="sqlmesh.materializations")
2979            for entry_point in entry_points:
2980                strategy_type = entry_point.load()
2981                if not issubclass(strategy_type, CustomMaterialization):
2982                    raise SQLMeshError(
2983                        f"Custom materialization entry point '{entry_point.name}' must be a subclass of CustomMaterialization."
2984                    )
2985                strategy_types.append(strategy_type)
2986
2987            _custom_materialization_type_cache = {
2988                getattr(strategy_type, "NAME", strategy_type.__name__).lower(): (
2989                    get_custom_materialization_kind_type(strategy_type),
2990                    strategy_type,
2991                )
2992                for strategy_type in strategy_types
2993            }
2994
2995        if strategy_key not in _custom_materialization_type_cache:
2996            raise ConfigError(f"Materialization strategy with name '{name}' was not found.")
2997    except (SQLMeshError, ConfigError) as e:
2998        if raise_errors:
2999            raise e
3000
3001        from sqlmesh.core.console import get_console
3002
3003        get_console().log_warning(str(e))
3004        return None
3005
3006    strategy_kind_type, strategy_type = _custom_materialization_type_cache[strategy_key]
3007    logger.debug(
3008        "Resolved custom materialization '%s' to '%s' (%s)", name, strategy_type, strategy_kind_type
3009    )
3010
3011    return strategy_kind_type, strategy_type
3012
3013
3014def get_custom_materialization_type_or_raise(
3015    name: str,
3016) -> t.Tuple[t.Type[CustomKind], t.Type[CustomMaterialization]]:
3017    types = get_custom_materialization_type(name, raise_errors=True)
3018    if types is not None:
3019        return types[0], types[1]
3020
3021    # Shouldnt get here as get_custom_materialization_type() has raise_errors=True, but just in case...
3022    raise SQLMeshError(f"Custom materialization '{name}' not present in the Python environment")
3023
3024
3025class DbtCustomMaterializationStrategy(MaterializableStrategy):
3026    def __init__(
3027        self,
3028        adapter: EngineAdapter,
3029        materialization_name: str,
3030        materialization_template: str,
3031    ):
3032        super().__init__(adapter)
3033        self.materialization_name = materialization_name
3034        self.materialization_template = materialization_template
3035
3036    def create(
3037        self,
3038        table_name: str,
3039        model: Model,
3040        is_table_deployable: bool,
3041        render_kwargs: t.Dict[str, t.Any],
3042        skip_grants: bool,
3043        **kwargs: t.Any,
3044    ) -> None:
3045        original_query = model.render_query_or_raise(**render_kwargs)
3046        self._execute_materialization(
3047            table_name=table_name,
3048            query_or_df=original_query.limit(0),
3049            model=model,
3050            is_first_insert=True,
3051            render_kwargs=render_kwargs,
3052            create_only=True,
3053            **kwargs,
3054        )
3055
3056        # Apply grants after dbt custom materialization table creation
3057        if not skip_grants:
3058            is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
3059            self._apply_grants(
3060                model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
3061            )
3062
3063    def insert(
3064        self,
3065        table_name: str,
3066        query_or_df: QueryOrDF,
3067        model: Model,
3068        is_first_insert: bool,
3069        render_kwargs: t.Dict[str, t.Any],
3070        **kwargs: t.Any,
3071    ) -> None:
3072        self._execute_materialization(
3073            table_name=table_name,
3074            query_or_df=query_or_df,
3075            model=model,
3076            is_first_insert=is_first_insert,
3077            render_kwargs=render_kwargs,
3078            **kwargs,
3079        )
3080
3081        # Apply grants after custom materialization insert (only on first insert)
3082        if is_first_insert:
3083            is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
3084            self._apply_grants(
3085                model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
3086            )
3087
3088    def append(
3089        self,
3090        table_name: str,
3091        query_or_df: QueryOrDF,
3092        model: Model,
3093        render_kwargs: t.Dict[str, t.Any],
3094        **kwargs: t.Any,
3095    ) -> None:
3096        return self.insert(
3097            table_name,
3098            query_or_df,
3099            model,
3100            is_first_insert=False,
3101            render_kwargs=render_kwargs,
3102            **kwargs,
3103        )
3104
3105    def run_pre_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
3106        # in dbt custom materialisations it's up to the user to run the pre hooks inside the transaction
3107        if not render_kwargs.get("inside_transaction", True):
3108            super().run_pre_statements(
3109                snapshot=snapshot,
3110                render_kwargs=render_kwargs,
3111            )
3112
3113    def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
3114        # in dbt custom materialisations it's up to the user to run the post hooks inside the transaction
3115        if not render_kwargs.get("inside_transaction", True):
3116            super().run_post_statements(
3117                snapshot=snapshot,
3118                render_kwargs=render_kwargs,
3119            )
3120
3121    def _execute_materialization(
3122        self,
3123        table_name: str,
3124        query_or_df: QueryOrDF,
3125        model: Model,
3126        is_first_insert: bool,
3127        render_kwargs: t.Dict[str, t.Any],
3128        create_only: bool = False,
3129        **kwargs: t.Any,
3130    ) -> None:
3131        jinja_macros = model.jinja_macros
3132
3133        # For vdes we need to use the table, since we don't know the schema/table at parse time
3134        parts = exp.to_table(table_name, dialect=self.adapter.dialect)
3135
3136        existing_globals = jinja_macros.global_objs
3137        relation_info = existing_globals.get("this")
3138        if isinstance(relation_info, dict):
3139            relation_info["database"] = parts.catalog
3140            relation_info["identifier"] = parts.name
3141            relation_info["name"] = parts.name
3142
3143        jinja_globals = {
3144            **existing_globals,
3145            "this": relation_info,
3146            "database": parts.catalog,
3147            "schema": parts.db,
3148            "identifier": parts.name,
3149            "target": existing_globals.get("target", {"type": self.adapter.dialect}),
3150            "execution_dt": kwargs.get("execution_time"),
3151            "engine_adapter": self.adapter,
3152            "sql": str(query_or_df),
3153            "is_first_insert": is_first_insert,
3154            "create_only": create_only,
3155            "pre_hooks": [
3156                AttributeDict({"sql": s.this.this, "transaction": transaction})
3157                for s in model.pre_statements
3158                if (transaction := s.args.get("transaction", True))
3159            ],
3160            "post_hooks": [
3161                AttributeDict({"sql": s.this.this, "transaction": transaction})
3162                for s in model.post_statements
3163                if (transaction := s.args.get("transaction", True))
3164            ],
3165            "model_instance": model,
3166            **kwargs,
3167        }
3168
3169        try:
3170            jinja_env = jinja_macros.build_environment(**jinja_globals)
3171            template = jinja_env.from_string(self.materialization_template)
3172
3173            try:
3174                template.render()
3175            except MacroReturnVal as ret:
3176                # this is a successful return from a macro call (dbt uses this list of Relations to update their relation cache)
3177                returned_relations = ret.value.get("relations", [])
3178                logger.info(
3179                    f"Materialization {self.materialization_name} returned relations: {returned_relations}"
3180                )
3181
3182        except Exception as e:
3183            raise SQLMeshError(
3184                f"Failed to execute dbt materialization '{self.materialization_name}': {e}"
3185            ) from e
3186
3187
3188class EngineManagedStrategy(MaterializableStrategy):
3189    def create(
3190        self,
3191        table_name: str,
3192        model: Model,
3193        is_table_deployable: bool,
3194        render_kwargs: t.Dict[str, t.Any],
3195        skip_grants: bool,
3196        **kwargs: t.Any,
3197    ) -> None:
3198        is_snapshot_deployable: bool = kwargs["is_snapshot_deployable"]
3199
3200        if is_table_deployable and is_snapshot_deployable:
3201            # We could deploy this to prod; create a proper managed table
3202            logger.info("Creating managed table: %s", table_name)
3203            physical_properties = _adjust_physical_properties_for_engine(
3204                self.adapter, model, kwargs.get("physical_properties", model.physical_properties)
3205            )
3206            self.adapter.create_managed_table(
3207                table_name=table_name,
3208                query=model.render_query_or_raise(**render_kwargs),
3209                target_columns_to_types=model.columns_to_types,
3210                partitioned_by=model.partitioned_by,
3211                clustered_by=model.clustered_by,  # type: ignore[arg-type]
3212                table_properties=physical_properties,
3213                table_description=model.description,
3214                column_descriptions=model.column_descriptions,
3215                table_format=model.table_format,
3216            )
3217
3218            # Apply grants after managed table creation
3219            if not skip_grants:
3220                self._apply_grants(
3221                    model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
3222                )
3223
3224        elif not is_table_deployable:
3225            # Only create the dev preview table as a normal table.
3226            # For the main table, if the snapshot is cant be deployed to prod (eg upstream is forward-only) do nothing.
3227            # Any downstream models that reference it will be updated to point to the dev preview table.
3228            # If the user eventually tries to deploy it, the logic in insert() will see it doesnt exist and create it
3229            super().create(
3230                table_name=table_name,
3231                model=model,
3232                is_table_deployable=is_table_deployable,
3233                render_kwargs=render_kwargs,
3234                skip_grants=skip_grants,
3235                **kwargs,
3236            )
3237
3238    def insert(
3239        self,
3240        table_name: str,
3241        query_or_df: QueryOrDF,
3242        model: Model,
3243        is_first_insert: bool,
3244        render_kwargs: t.Dict[str, t.Any],
3245        **kwargs: t.Any,
3246    ) -> None:
3247        deployability_index: DeployabilityIndex = kwargs["deployability_index"]
3248        snapshot: Snapshot = kwargs["snapshot"]
3249        is_snapshot_deployable = deployability_index.is_deployable(snapshot)
3250        if is_first_insert and is_snapshot_deployable and not self.adapter.table_exists(table_name):
3251            self.adapter.create_managed_table(
3252                table_name=table_name,
3253                query=query_or_df,  # type: ignore
3254                target_columns_to_types=model.columns_to_types,
3255                partitioned_by=model.partitioned_by,
3256                clustered_by=model.clustered_by,  # type: ignore[arg-type]
3257                table_properties=kwargs.get("physical_properties", model.physical_properties),
3258                table_description=model.description,
3259                column_descriptions=model.column_descriptions,
3260                table_format=model.table_format,
3261            )
3262            self._apply_grants(
3263                model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
3264            )
3265        elif not is_snapshot_deployable:
3266            # Snapshot isnt deployable; update the preview table instead
3267            # If the snapshot was deployable, then data would have already been loaded in create() because a managed table would have been created
3268            logger.info(
3269                "Updating preview table: %s (for managed model: %s)",
3270                table_name,
3271                model.name,
3272            )
3273            self._replace_query_for_model(
3274                model=model,
3275                name=table_name,
3276                query_or_df=query_or_df,
3277                render_kwargs=render_kwargs,
3278                **kwargs,
3279            )
3280
3281    def append(
3282        self,
3283        table_name: str,
3284        query_or_df: QueryOrDF,
3285        model: Model,
3286        render_kwargs: t.Dict[str, t.Any],
3287        **kwargs: t.Any,
3288    ) -> None:
3289        raise ConfigError(f"Cannot append to a managed table '{table_name}'.")
3290
3291    def migrate(
3292        self,
3293        target_table_name: str,
3294        source_table_name: str,
3295        snapshot: Snapshot,
3296        *,
3297        ignore_destructive: bool,
3298        ignore_additive: bool,
3299        **kwargs: t.Any,
3300    ) -> None:
3301        potential_alter_operations = self.adapter.get_alter_operations(
3302            target_table_name,
3303            source_table_name,
3304            ignore_destructive=ignore_destructive,
3305            ignore_additive=ignore_additive,
3306        )
3307        if len(potential_alter_operations) > 0:
3308            # this can happen if a user changes a managed model and deliberately overrides a plan to be forward only, eg `sqlmesh plan --forward-only`
3309            raise MigrationNotSupportedError(
3310                f"The schema of the managed model '{target_table_name}' cannot be updated in a forward-only fashion."
3311            )
3312
3313        # Apply grants after verifying no schema changes
3314        deployability_index = kwargs.get("deployability_index")
3315        is_snapshot_deployable = (
3316            deployability_index.is_deployable(snapshot) if deployability_index else False
3317        )
3318        self._apply_grants(
3319            snapshot.model, target_table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
3320        )
3321
3322    def delete(self, name: str, **kwargs: t.Any) -> None:
3323        # a dev preview table is created as a normal table, so it needs to be dropped as a normal table
3324        _check_table_db_is_physical_schema(name, kwargs["physical_schema"])
3325        if kwargs["is_table_deployable"]:
3326            self.adapter.drop_managed_table(name)
3327            logger.info("Dropped managed table '%s'", name)
3328        else:
3329            self.adapter.drop_table(name)
3330            logger.info("Dropped dev preview for managed table '%s'", name)
3331
3332
3333def _intervals(snapshot: Snapshot, deployability_index: DeployabilityIndex) -> Intervals:
3334    return (
3335        snapshot.intervals
3336        if deployability_index.is_deployable(snapshot)
3337        else snapshot.dev_intervals
3338    )
3339
3340
3341def _check_destructive_schema_change(
3342    snapshot: Snapshot,
3343    alter_operations: t.List[TableAlterOperation],
3344    allow_destructive_snapshots: t.Set[str],
3345) -> None:
3346    if (
3347        snapshot.is_no_rebuild
3348        and snapshot.needs_destructive_check(allow_destructive_snapshots)
3349        and has_drop_alteration(alter_operations)
3350    ):
3351        snapshot_name = snapshot.name
3352        model_dialect = snapshot.model.dialect
3353
3354        if snapshot.model.on_destructive_change.is_warn:
3355            logger.warning(
3356                format_destructive_change_msg(
3357                    snapshot_name,
3358                    alter_operations,
3359                    model_dialect,
3360                    error=False,
3361                )
3362            )
3363            return
3364        raise DestructiveChangeError(
3365            format_destructive_change_msg(snapshot_name, alter_operations, model_dialect)
3366        )
3367
3368
3369def _check_additive_schema_change(
3370    snapshot: Snapshot,
3371    alter_operations: t.List[TableAlterOperation],
3372    allow_additive_snapshots: t.Set[str],
3373) -> None:
3374    # Only check additive changes for incremental models that have the on_additive_change property
3375    if not isinstance(snapshot.model.kind, _Incremental):
3376        return
3377
3378    if snapshot.needs_additive_check(allow_additive_snapshots) and has_additive_alteration(
3379        alter_operations
3380    ):
3381        # Note: IGNORE filtering is applied before this function is called
3382        # so if we reach here, additive changes are not being ignored
3383        snapshot_name = snapshot.name
3384        model_dialect = snapshot.model.dialect
3385
3386        if snapshot.model.on_additive_change.is_warn:
3387            logger.warning(
3388                format_additive_change_msg(
3389                    snapshot_name,
3390                    alter_operations,
3391                    model_dialect,
3392                    error=False,
3393                )
3394            )
3395            return
3396        if snapshot.model.on_additive_change.is_error:
3397            raise AdditiveChangeError(
3398                format_additive_change_msg(snapshot_name, alter_operations, model_dialect)
3399            )
3400
3401
3402def _check_table_db_is_physical_schema(table_name: str, physical_schema: str) -> None:
3403    table = exp.to_table(table_name)
3404    if table.db != physical_schema:
3405        raise SQLMeshError(
3406            f"Table '{table_name}' is not a part of the physical schema '{physical_schema}' and so can't be dropped."
3407        )
3408
3409
3410def _snapshot_to_data_object_type(snapshot: Snapshot) -> DataObjectType:
3411    if snapshot.is_managed:
3412        return DataObjectType.MANAGED_TABLE
3413    if snapshot.is_materialized_view:
3414        return DataObjectType.MATERIALIZED_VIEW
3415    if snapshot.is_view:
3416        return DataObjectType.VIEW
3417    if snapshot.is_materialized:
3418        return DataObjectType.TABLE
3419    return DataObjectType.UNKNOWN
logger = <Logger sqlmesh.core.snapshot.evaluator (WARNING)>
class SnapshotCreationFailedError(sqlmesh.utils.errors.SQLMeshError):
103class SnapshotCreationFailedError(SQLMeshError):
104    def __init__(
105        self, errors: t.List[NodeExecutionFailedError[SnapshotId]], skipped: t.List[SnapshotId]
106    ):
107        messages = "\n\n".join(f"{error}\n  {error.__cause__}" for error in errors)
108        super().__init__(f"Physical table creation failed:\n\n{messages}")
109        self.errors = errors
110        self.skipped = skipped

Common base class for all non-exit exceptions.

104    def __init__(
105        self, errors: t.List[NodeExecutionFailedError[SnapshotId]], skipped: t.List[SnapshotId]
106    ):
107        messages = "\n\n".join(f"{error}\n  {error.__cause__}" for error in errors)
108        super().__init__(f"Physical table creation failed:\n\n{messages}")
109        self.errors = errors
110        self.skipped = skipped
errors
skipped
Inherited Members
builtins.BaseException
with_traceback
args
class SnapshotEvaluator:
 113class SnapshotEvaluator:
 114    """Evaluates a snapshot given runtime arguments through an arbitrary EngineAdapter.
 115
 116    The SnapshotEvaluator contains the business logic to generically evaluate a snapshot.
 117    It is responsible for delegating queries to the EngineAdapter. The SnapshotEvaluator
 118    does not directly communicate with the underlying execution engine.
 119
 120    Args:
 121        adapters: A single EngineAdapter or a dictionary of EngineAdapters where
 122            the key is the gateway name. When a dictionary is provided, and not an
 123            explicit default gateway its first item is treated as the default
 124            adapter and used for the virtual layer.
 125        ddl_concurrent_tasks: The number of concurrent tasks used for DDL
 126            operations (table / view creation, deletion, etc). Default: 1.
 127    """
 128
 129    def __init__(
 130        self,
 131        adapters: EngineAdapter | t.Dict[str, EngineAdapter],
 132        ddl_concurrent_tasks: int = 1,
 133        selected_gateway: t.Optional[str] = None,
 134    ):
 135        self.adapters = (
 136            adapters if isinstance(adapters, t.Dict) else {selected_gateway or "": adapters}
 137        )
 138        self.execution_tracker = QueryExecutionTracker()
 139        self.adapters = {
 140            gateway: adapter.with_settings(query_execution_tracker=self.execution_tracker)
 141            for gateway, adapter in self.adapters.items()
 142        }
 143        self.adapter = (
 144            next(iter(self.adapters.values()))
 145            if not selected_gateway
 146            else self.adapters[selected_gateway]
 147        )
 148        self.selected_gateway = selected_gateway
 149        self.ddl_concurrent_tasks = ddl_concurrent_tasks
 150
 151    def evaluate(
 152        self,
 153        snapshot: Snapshot,
 154        *,
 155        start: TimeLike,
 156        end: TimeLike,
 157        execution_time: TimeLike,
 158        snapshots: t.Dict[str, Snapshot],
 159        allow_destructive_snapshots: t.Optional[t.Set[str]] = None,
 160        allow_additive_snapshots: t.Optional[t.Set[str]] = None,
 161        deployability_index: t.Optional[DeployabilityIndex] = None,
 162        batch_index: int = 0,
 163        target_table_exists: t.Optional[bool] = None,
 164        **kwargs: t.Any,
 165    ) -> t.Optional[str]:
 166        """Renders the snapshot's model, executes it and stores the result in the snapshot's physical table.
 167
 168        Args:
 169            snapshot: Snapshot to evaluate.
 170            start: The start datetime to render.
 171            end: The end datetime to render.
 172            execution_time: The date/time time reference to use for execution time.
 173            snapshots: All upstream snapshots (by name) to use for expansion and mapping of physical locations.
 174            allow_destructive_snapshots: Snapshots for which destructive schema changes are allowed.
 175            allow_additive_snapshots: Snapshots for which additive schema changes are allowed.
 176            deployability_index: Determines snapshots that are deployable in the context of this evaluation.
 177            batch_index: If the snapshot is part of a batch of related snapshots; which index in the batch is it
 178            target_table_exists: Whether the target table exists. If None, the table will be checked for existence.
 179            kwargs: Additional kwargs to pass to the renderer.
 180
 181        Returns:
 182            The WAP ID of this evaluation if supported, None otherwise.
 183        """
 184        with self.execution_tracker.track_execution(
 185            SnapshotIdBatch(snapshot_id=snapshot.snapshot_id, batch_id=batch_index)
 186        ):
 187            result = self._evaluate_snapshot(
 188                start=start,
 189                end=end,
 190                execution_time=execution_time,
 191                snapshot=snapshot,
 192                snapshots=snapshots,
 193                allow_destructive_snapshots=allow_destructive_snapshots or set(),
 194                allow_additive_snapshots=allow_additive_snapshots or set(),
 195                deployability_index=deployability_index,
 196                batch_index=batch_index,
 197                target_table_exists=target_table_exists,
 198                **kwargs,
 199            )
 200        if result is None or isinstance(result, str):
 201            return result
 202        raise SQLMeshError(
 203            f"Unexpected result {result} when evaluating snapshot {snapshot.snapshot_id}."
 204        )
 205
 206    def evaluate_and_fetch(
 207        self,
 208        snapshot: Snapshot,
 209        *,
 210        start: TimeLike,
 211        end: TimeLike,
 212        execution_time: TimeLike,
 213        snapshots: t.Dict[str, Snapshot],
 214        limit: int,
 215        deployability_index: t.Optional[DeployabilityIndex] = None,
 216        **kwargs: t.Any,
 217    ) -> DF:
 218        """Renders the snapshot's model, executes it and returns a dataframe with the result.
 219
 220        Args:
 221            snapshot: Snapshot to evaluate.
 222            start: The start datetime to render.
 223            end: The end datetime to render.
 224            execution_time: The date/time time reference to use for execution time.
 225            snapshots: All upstream snapshots (by name) to use for expansion and mapping of physical locations.
 226            limit: The maximum number of rows to fetch.
 227            deployability_index: Determines snapshots that are deployable in the context of this evaluation.
 228            kwargs: Additional kwargs to pass to the renderer.
 229
 230        Returns:
 231            The result of the evaluation as a dataframe.
 232        """
 233        import pandas as pd
 234
 235        adapter = self.get_adapter(snapshot.model.gateway)
 236        render_kwargs = dict(
 237            start=start,
 238            end=end,
 239            execution_time=execution_time,
 240            snapshot=snapshot,
 241            runtime_stage=RuntimeStage.EVALUATING,
 242            **kwargs,
 243        )
 244        queries_or_dfs = self._render_snapshot_for_evaluation(
 245            snapshot,
 246            snapshots,
 247            deployability_index or DeployabilityIndex.all_deployable(),
 248            render_kwargs,
 249        )
 250        query_or_df = next(queries_or_dfs)
 251        if isinstance(query_or_df, pd.DataFrame):
 252            return query_or_df.head(limit)
 253        if not isinstance(query_or_df, exp.Expr):
 254            # We assume that if this branch is reached, `query_or_df` is a pyspark / snowpark / bigframe dataframe,
 255            # so we use `limit` instead of `head` to get back a dataframe instead of List[Row]
 256            # https://spark.apache.org/docs/3.1.1/api/python/reference/api/pyspark.sql.DataFrame.head.html#pyspark.sql.DataFrame.head
 257            return query_or_df.limit(limit)
 258
 259        assert isinstance(query_or_df, exp.Query)
 260
 261        existing_limit = query_or_df.args.get("limit")
 262        if existing_limit:
 263            limit = min(limit, execute(exp.select(existing_limit.expression)).rows[0][0])
 264            assert limit is not None
 265
 266        return adapter._fetch_native_df(query_or_df.limit(limit))
 267
 268    def promote(
 269        self,
 270        target_snapshots: t.Iterable[Snapshot],
 271        environment_naming_info: EnvironmentNamingInfo,
 272        deployability_index: t.Optional[DeployabilityIndex] = None,
 273        start: t.Optional[TimeLike] = None,
 274        end: t.Optional[TimeLike] = None,
 275        execution_time: t.Optional[TimeLike] = None,
 276        snapshots: t.Optional[t.Dict[SnapshotId, Snapshot]] = None,
 277        table_mapping: t.Optional[t.Dict[str, str]] = None,
 278        on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]] = None,
 279    ) -> None:
 280        """Promotes the given collection of snapshots in the target environment by replacing a corresponding
 281        view with a physical table associated with the given snapshot.
 282
 283        Args:
 284            target_snapshots: Snapshots to promote.
 285            environment_naming_info: Naming information for the target environment.
 286            deployability_index: Determines snapshots that are deployable in the context of this promotion.
 287            on_complete: A callback to call on each successfully promoted snapshot.
 288        """
 289
 290        tables_by_gateway: t.Dict[t.Union[str, None], t.List[exp.Table]] = defaultdict(list)
 291        for snapshot in target_snapshots:
 292            if snapshot.is_model and not snapshot.is_symbolic:
 293                gateway = (
 294                    snapshot.model_gateway if environment_naming_info.gateway_managed else None
 295                )
 296                adapter = self.get_adapter(gateway)
 297                table = snapshot.qualified_view_name.table_for_environment(
 298                    environment_naming_info, dialect=adapter.dialect
 299                )
 300                tables_by_gateway[gateway].append(table)
 301
 302        # A schema can be shared across multiple engines, so we need to group by gateway
 303        for gateway, tables in tables_by_gateway.items():
 304            if environment_naming_info.suffix_target.is_catalog:
 305                self._create_catalogs(tables=tables, gateway=gateway)
 306
 307        gateway_table_pairs = [
 308            (gateway, table) for gateway, tables in tables_by_gateway.items() for table in tables
 309        ]
 310        self._create_schemas(gateway_table_pairs=gateway_table_pairs)
 311
 312        # Fetch the view data objects for the promoted snapshots to get them cached
 313        self._get_virtual_data_objects(target_snapshots, environment_naming_info)
 314
 315        deployability_index = deployability_index or DeployabilityIndex.all_deployable()
 316        with self.concurrent_context():
 317            concurrent_apply_to_snapshots(
 318                target_snapshots,
 319                lambda s: self._promote_snapshot(
 320                    s,
 321                    start=start,
 322                    end=end,
 323                    execution_time=execution_time,
 324                    snapshots=snapshots,
 325                    table_mapping=table_mapping,
 326                    environment_naming_info=environment_naming_info,
 327                    deployability_index=deployability_index,  # type: ignore
 328                    on_complete=on_complete,
 329                ),
 330                self.ddl_concurrent_tasks,
 331            )
 332
 333    def demote(
 334        self,
 335        target_snapshots: t.Iterable[Snapshot],
 336        environment_naming_info: EnvironmentNamingInfo,
 337        table_mapping: t.Optional[t.Dict[str, str]] = None,
 338        deployability_index: t.Optional[DeployabilityIndex] = None,
 339        on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]] = None,
 340    ) -> None:
 341        """Demotes the given collection of snapshots in the target environment by removing its view.
 342
 343        Args:
 344            target_snapshots: Snapshots to demote.
 345            environment_naming_info: Naming info for the target environment.
 346            on_complete: A callback to call on each successfully demoted snapshot.
 347        """
 348        with self.concurrent_context():
 349            concurrent_apply_to_snapshots(
 350                target_snapshots,
 351                lambda s: self._demote_snapshot(
 352                    s,
 353                    environment_naming_info,
 354                    deployability_index=deployability_index,
 355                    on_complete=on_complete,
 356                    table_mapping=table_mapping,
 357                ),
 358                self.ddl_concurrent_tasks,
 359            )
 360
 361    def create(
 362        self,
 363        target_snapshots: t.Iterable[Snapshot],
 364        snapshots: t.Dict[SnapshotId, Snapshot],
 365        deployability_index: t.Optional[DeployabilityIndex] = None,
 366        on_start: t.Optional[t.Callable] = None,
 367        on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]] = None,
 368        allow_destructive_snapshots: t.Optional[t.Set[str]] = None,
 369        allow_additive_snapshots: t.Optional[t.Set[str]] = None,
 370    ) -> CompletionStatus:
 371        """Creates a physical snapshot schema and table for the given collection of snapshots.
 372
 373        Args:
 374            target_snapshots: Target snapshots.
 375            snapshots: Mapping of snapshot ID to snapshot.
 376            deployability_index: Determines snapshots that are deployable in the context of this creation.
 377            on_start: A callback to initialize the snapshot creation progress bar.
 378            on_complete: A callback to call on each successfully created snapshot.
 379            allow_destructive_snapshots: Set of snapshots that are allowed to have destructive schema changes.
 380            allow_additive_snapshots: Set of snapshots that are allowed to have additive schema changes.
 381
 382        Returns:
 383            CompletionStatus: The status of the creation operation (success, failure, nothing to do).
 384        """
 385        deployability_index = deployability_index or DeployabilityIndex.all_deployable()
 386
 387        snapshots_to_create = self.get_snapshots_to_create(target_snapshots, deployability_index)
 388        if not snapshots_to_create:
 389            return CompletionStatus.NOTHING_TO_DO
 390        if on_start:
 391            on_start(snapshots_to_create)
 392
 393        self._create_snapshots(
 394            snapshots_to_create=snapshots_to_create,
 395            snapshots={s.name: s for s in snapshots.values()},
 396            deployability_index=deployability_index,
 397            on_complete=on_complete,
 398            allow_destructive_snapshots=allow_destructive_snapshots or set(),
 399            allow_additive_snapshots=allow_additive_snapshots or set(),
 400        )
 401        return CompletionStatus.SUCCESS
 402
 403    def create_physical_schemas(
 404        self, snapshots: t.Iterable[Snapshot], deployability_index: DeployabilityIndex
 405    ) -> None:
 406        """Creates the physical schemas for the given snapshots.
 407
 408        Args:
 409            snapshots: Snapshots to create physical schemas for.
 410            deployability_index: Determines snapshots that are deployable in the context of this creation.
 411        """
 412        tables_by_gateway: t.Dict[t.Optional[str], t.List[str]] = defaultdict(list)
 413        for snapshot in snapshots:
 414            if snapshot.is_model and not snapshot.is_symbolic:
 415                tables_by_gateway[snapshot.model_gateway].append(
 416                    snapshot.table_name(is_deployable=deployability_index.is_deployable(snapshot))
 417                )
 418
 419        gateway_table_pairs = [
 420            (gateway, table) for gateway, tables in tables_by_gateway.items() for table in tables
 421        ]
 422        self._create_schemas(gateway_table_pairs=gateway_table_pairs)
 423
 424    def get_snapshots_to_create(
 425        self, target_snapshots: t.Iterable[Snapshot], deployability_index: DeployabilityIndex
 426    ) -> t.List[Snapshot]:
 427        """Returns a list of snapshots that need to have their physical tables created.
 428
 429        Args:
 430            target_snapshots: Target snapshots.
 431            deployability_index: Determines snapshots that are deployable / representative in the context of this creation.
 432        """
 433        existing_data_objects = self._get_physical_data_objects(
 434            target_snapshots, deployability_index
 435        )
 436        snapshots_to_create = []
 437        for snapshot in target_snapshots:
 438            if not snapshot.is_model or snapshot.is_symbolic:
 439                continue
 440            if snapshot.snapshot_id not in existing_data_objects or (
 441                snapshot.is_seed and not snapshot.intervals
 442            ):
 443                snapshots_to_create.append(snapshot)
 444
 445        return snapshots_to_create
 446
 447    def _create_snapshots(
 448        self,
 449        snapshots_to_create: t.Iterable[Snapshot],
 450        snapshots: t.Dict[str, Snapshot],
 451        deployability_index: DeployabilityIndex,
 452        on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]],
 453        allow_destructive_snapshots: t.Set[str],
 454        allow_additive_snapshots: t.Set[str],
 455    ) -> None:
 456        """Internal method to create tables in parallel."""
 457        with self.concurrent_context():
 458            errors, skipped = concurrent_apply_to_snapshots(
 459                snapshots_to_create,
 460                lambda s: self.create_snapshot(
 461                    s,
 462                    snapshots=snapshots,
 463                    deployability_index=deployability_index,
 464                    allow_destructive_snapshots=allow_destructive_snapshots,
 465                    allow_additive_snapshots=allow_additive_snapshots,
 466                    on_complete=on_complete,
 467                ),
 468                self.ddl_concurrent_tasks,
 469                raise_on_error=False,
 470            )
 471            if errors:
 472                raise SnapshotCreationFailedError(errors, skipped)
 473
 474    def migrate(
 475        self,
 476        target_snapshots: t.Iterable[Snapshot],
 477        snapshots: t.Dict[SnapshotId, Snapshot],
 478        allow_destructive_snapshots: t.Optional[t.Set[str]] = None,
 479        allow_additive_snapshots: t.Optional[t.Set[str]] = None,
 480        deployability_index: t.Optional[DeployabilityIndex] = None,
 481    ) -> None:
 482        """Alters a physical snapshot table to match its snapshot's schema for the given collection of snapshots.
 483
 484        Args:
 485            target_snapshots: Target snapshots.
 486            snapshots: Mapping of snapshot ID to snapshot.
 487            allow_destructive_snapshots: Set of snapshots that are allowed to have destructive schema changes.
 488            allow_additive_snapshots: Set of snapshots that are allowed to have additive schema changes.
 489            deployability_index: Determines snapshots that are deployable in the context of this evaluation.
 490        """
 491        deployability_index = deployability_index or DeployabilityIndex.all_deployable()
 492        target_data_objects = self._get_physical_data_objects(target_snapshots, deployability_index)
 493        if not target_data_objects:
 494            return
 495
 496        if not snapshots:
 497            snapshots = {s.snapshot_id: s for s in target_snapshots}
 498
 499        allow_destructive_snapshots = allow_destructive_snapshots or set()
 500        allow_additive_snapshots = allow_additive_snapshots or set()
 501        snapshots_by_name = {s.name: s for s in snapshots.values()}
 502        with self.concurrent_context():
 503            # Only migrate snapshots for which there's an existing data object
 504            concurrent_apply_to_snapshots(
 505                target_snapshots,
 506                lambda s: self._migrate_snapshot(
 507                    s,
 508                    snapshots_by_name,
 509                    target_data_objects.get(s.snapshot_id),
 510                    allow_destructive_snapshots,
 511                    allow_additive_snapshots,
 512                    self.get_adapter(s.model_gateway),
 513                    deployability_index,
 514                ),
 515                self.ddl_concurrent_tasks,
 516            )
 517
 518    def cleanup(
 519        self,
 520        target_snapshots: t.Iterable[SnapshotTableCleanupTask],
 521        on_complete: t.Optional[t.Callable[[str], None]] = None,
 522    ) -> None:
 523        """Cleans up the given snapshots by removing its table
 524
 525        Args:
 526            target_snapshots: Snapshots to cleanup.
 527            on_complete: A callback to call on each successfully deleted database object.
 528        """
 529        target_snapshots = [
 530            t for t in target_snapshots if t.snapshot.is_model and not t.snapshot.is_symbolic
 531        ]
 532        available_gateways = set(self.adapters.keys())
 533        skipped = []
 534        filtered_targets = []
 535        for t in target_snapshots:
 536            gw = t.snapshot.model_gateway
 537            if gw and gw not in available_gateways:
 538                skipped.append((t.snapshot.snapshot_id, gw))
 539            else:
 540                filtered_targets.append(t)
 541        if skipped:
 542            logger.warning(
 543                "Skipping cleanup of %d snapshot(s) with unavailable gateway(s): %s",
 544                len(skipped),
 545                ", ".join(f"{sid} (gateway={gw})" for sid, gw in skipped),
 546            )
 547        snapshots_to_dev_table_only = {
 548            t.snapshot.snapshot_id: t.dev_table_only for t in filtered_targets
 549        }
 550        with self.concurrent_context():
 551            errors, _ = concurrent_apply_to_snapshots(
 552                [t.snapshot for t in filtered_targets],
 553                lambda s: self._cleanup_snapshot(
 554                    s,
 555                    snapshots_to_dev_table_only[s.snapshot_id],
 556                    self.get_adapter(s.model_gateway),
 557                    on_complete,
 558                ),
 559                self.ddl_concurrent_tasks,
 560                reverse_order=True,
 561                raise_on_error=False,
 562            )
 563        if errors:
 564            errored_snapshots = "\n".join(f"  {e.node.name}: {e.__cause__}" for e in errors)
 565            raise SQLMeshError(f"\n{errored_snapshots}")
 566
 567    def audit(
 568        self,
 569        snapshot: Snapshot,
 570        *,
 571        snapshots: t.Dict[str, Snapshot],
 572        start: t.Optional[TimeLike] = None,
 573        end: t.Optional[TimeLike] = None,
 574        execution_time: t.Optional[TimeLike] = None,
 575        deployability_index: t.Optional[DeployabilityIndex] = None,
 576        wap_id: t.Optional[str] = None,
 577        **kwargs: t.Any,
 578    ) -> t.List[AuditResult]:
 579        """Execute a snapshot's node's audit queries.
 580
 581        Args:
 582            snapshot: Snapshot to evaluate.
 583            snapshots: All upstream snapshots (by name) to use for expansion and mapping of physical locations.
 584            start: The start datetime to audit. Defaults to epoch start.
 585            end: The end datetime to audit. Defaults to epoch start.
 586            execution_time: The date/time time reference to use for execution time.
 587            deployability_index: Determines snapshots that are deployable in the context of this evaluation.
 588            wap_id: The WAP ID if applicable, None otherwise.
 589            kwargs: Additional kwargs to pass to the renderer.
 590        """
 591        deployability_index = deployability_index or DeployabilityIndex.all_deployable()
 592        adapter = self.get_adapter(snapshot.model_gateway)
 593
 594        if not snapshot.version:
 595            raise ConfigError(
 596                f"Cannot audit '{snapshot.name}' because it has not been versioned yet. Apply a plan first."
 597            )
 598
 599        if wap_id is not None:
 600            deployability_index = deployability_index or DeployabilityIndex.all_deployable()
 601            original_table_name = snapshot.table_name(
 602                is_deployable=deployability_index.is_deployable(snapshot)
 603            )
 604            wap_table_name = adapter.wap_table_name(original_table_name, wap_id)
 605            logger.info(
 606                "Auditing WAP table '%s', snapshot %s",
 607                wap_table_name,
 608                snapshot.snapshot_id,
 609            )
 610
 611            table_mapping = kwargs.get("table_mapping") or {}
 612            table_mapping[snapshot.name] = wap_table_name
 613            kwargs["table_mapping"] = table_mapping
 614            kwargs["this_model"] = exp.to_table(wap_table_name, dialect=adapter.dialect)
 615
 616        results = []
 617
 618        audits_with_args = snapshot.node.audits_with_args
 619
 620        force_non_blocking = False
 621
 622        if audits_with_args:
 623            logger.info("Auditing snapshot %s", snapshot.snapshot_id)
 624
 625            if not deployability_index.is_deployable(snapshot) and not adapter.SUPPORTS_CLONING:
 626                # For dev preview tables that aren't based on clones of the production table, only a subset of the data is typically available
 627                # However, users still expect audits to run anwyay. Some audits (such as row count) are practically guaranteed to fail
 628                # when run on only a subset of data, so we switch all audits to non blocking and the user can decide if they still want to proceed
 629                force_non_blocking = True
 630
 631        for audit, audit_args in audits_with_args:
 632            if force_non_blocking:
 633                # remove any blocking indicator on the model itself
 634                audit_args.pop("blocking", None)
 635                # so that we can fall back to the audit's setting, which we override to blocking: False
 636                audit = audit.model_copy(update={"blocking": False})
 637
 638            results.append(
 639                self._audit(
 640                    audit=audit,
 641                    audit_args=audit_args,
 642                    snapshot=snapshot,
 643                    snapshots=snapshots,
 644                    start=start,
 645                    end=end,
 646                    execution_time=execution_time,
 647                    deployability_index=deployability_index,
 648                    **kwargs,
 649                )
 650            )
 651
 652        if wap_id is not None:
 653            logger.info(
 654                "Publishing evaluation results for snapshot %s, WAP ID '%s'",
 655                snapshot.snapshot_id,
 656                wap_id,
 657            )
 658            self.wap_publish_snapshot(snapshot, wap_id, deployability_index)
 659
 660        return results
 661
 662    @contextmanager
 663    def concurrent_context(self) -> t.Iterator[None]:
 664        try:
 665            yield
 666        finally:
 667            self.recycle()
 668
 669    def recycle(self) -> None:
 670        """Closes all open connections and releases all allocated resources associated with any thread
 671        except the calling one."""
 672        try:
 673            for adapter in self.adapters.values():
 674                adapter.recycle()
 675
 676        except Exception:
 677            logger.exception("Failed to recycle Snapshot Evaluator")
 678
 679    def close(self) -> None:
 680        """Closes all open connections and releases all allocated resources."""
 681        try:
 682            for adapter in self.adapters.values():
 683                adapter.close()
 684        except Exception:
 685            logger.exception("Failed to close Snapshot Evaluator")
 686
 687    def set_correlation_id(self, correlation_id: CorrelationId) -> SnapshotEvaluator:
 688        return SnapshotEvaluator(
 689            {
 690                gateway: adapter.with_settings(correlation_id=correlation_id)
 691                for gateway, adapter in self.adapters.items()
 692            },
 693            self.ddl_concurrent_tasks,
 694            self.selected_gateway,
 695        )
 696
 697    def _evaluate_snapshot(
 698        self,
 699        start: TimeLike,
 700        end: TimeLike,
 701        execution_time: TimeLike,
 702        snapshot: Snapshot,
 703        snapshots: t.Dict[str, Snapshot],
 704        allow_destructive_snapshots: t.Set[str],
 705        allow_additive_snapshots: t.Set[str],
 706        deployability_index: t.Optional[DeployabilityIndex],
 707        batch_index: int,
 708        target_table_exists: t.Optional[bool],
 709        **kwargs: t.Any,
 710    ) -> t.Optional[str]:
 711        """Renders the snapshot's model and executes it. The return value depends on whether the limit was specified.
 712
 713        Args:
 714            snapshot: Snapshot to evaluate.
 715            start: The start datetime to render.
 716            end: The end datetime to render.
 717            execution_time: The date/time time reference to use for execution time.
 718            snapshots: All upstream snapshots to use for expansion and mapping of physical locations.
 719            allow_destructive_snapshots: Snapshots for which destructive schema changes are allowed.
 720            allow_additive_snapshots: Snapshots for which additive schema changes are allowed.
 721            deployability_index: Determines snapshots that are deployable in the context of this evaluation.
 722            batch_index: If the snapshot is part of a batch of related snapshots; which index in the batch is it
 723            target_table_exists: Whether the target table exists. If None, the table will be checked for existence.
 724            kwargs: Additional kwargs to pass to the renderer.
 725        """
 726        if not snapshot.is_model:
 727            return None
 728
 729        model = snapshot.model
 730
 731        logger.info("Evaluating snapshot %s", snapshot.snapshot_id)
 732
 733        adapter = self.get_adapter(model.gateway)
 734        deployability_index = deployability_index or DeployabilityIndex.all_deployable()
 735        is_snapshot_deployable = deployability_index.is_deployable(snapshot)
 736        target_table_name = snapshot.table_name(is_deployable=is_snapshot_deployable)
 737        # https://github.com/SQLMesh/sqlmesh/issues/2609
 738        # If there are no existing intervals yet; only consider this a first insert for the first snapshot in the batch
 739        if target_table_exists is None:
 740            target_table_exists = adapter.table_exists(target_table_name)
 741        is_first_insert = (
 742            not _intervals(snapshot, deployability_index) or not target_table_exists
 743        ) and batch_index == 0
 744
 745        # Use the 'creating' stage if the table doesn't exist yet to preserve backwards compatibility with existing projects
 746        # that depend on a separate physical table creation stage.
 747        runtime_stage = RuntimeStage.EVALUATING if target_table_exists else RuntimeStage.CREATING
 748        common_render_kwargs = dict(
 749            start=start,
 750            end=end,
 751            execution_time=execution_time,
 752            snapshot=snapshot,
 753            runtime_stage=runtime_stage,
 754            **kwargs,
 755        )
 756        create_render_kwargs = dict(
 757            engine_adapter=adapter,
 758            snapshots=snapshots,
 759            deployability_index=deployability_index,
 760            **common_render_kwargs,
 761        )
 762        create_render_kwargs["runtime_stage"] = RuntimeStage.CREATING
 763        render_statements_kwargs = dict(
 764            engine_adapter=adapter,
 765            snapshots=snapshots,
 766            deployability_index=deployability_index,
 767            **common_render_kwargs,
 768        )
 769        rendered_physical_properties = snapshot.model.render_physical_properties(
 770            **render_statements_kwargs
 771        )
 772
 773        evaluation_strategy = _evaluation_strategy(snapshot, adapter)
 774        evaluation_strategy.run_pre_statements(
 775            snapshot=snapshot,
 776            render_kwargs={**render_statements_kwargs, "inside_transaction": False},
 777        )
 778
 779        with (
 780            adapter.transaction(),
 781            adapter.session(snapshot.model.render_session_properties(**render_statements_kwargs)),
 782        ):
 783            evaluation_strategy.run_pre_statements(
 784                snapshot=snapshot,
 785                render_kwargs={**render_statements_kwargs, "inside_transaction": True},
 786            )
 787
 788            if not target_table_exists or (model.is_seed and not snapshot.intervals):
 789                # Only create the empty table if the columns were provided explicitly by the user
 790                should_create_empty_table = (
 791                    model.kind.is_materialized
 792                    and model.columns_to_types_
 793                    and columns_to_types_all_known(model.columns_to_types_)
 794                )
 795                if not should_create_empty_table:
 796                    # Or if the model is self-referential and its query is fully annotated with types
 797                    should_create_empty_table = model.depends_on_self and model.annotated
 798                if self._can_clone(snapshot, deployability_index):
 799                    self._clone_snapshot_in_dev(
 800                        snapshot=snapshot,
 801                        snapshots=snapshots,
 802                        deployability_index=deployability_index,
 803                        render_kwargs=create_render_kwargs,
 804                        rendered_physical_properties=rendered_physical_properties.copy(),
 805                        allow_destructive_snapshots=allow_destructive_snapshots,
 806                        allow_additive_snapshots=allow_additive_snapshots,
 807                    )
 808                    runtime_stage = RuntimeStage.EVALUATING
 809                    target_table_exists = True
 810                elif should_create_empty_table or model.is_seed or model.kind.is_scd_type_2:
 811                    self._execute_create(
 812                        snapshot=snapshot,
 813                        table_name=target_table_name,
 814                        is_table_deployable=is_snapshot_deployable,
 815                        deployability_index=deployability_index,
 816                        create_render_kwargs=create_render_kwargs,
 817                        rendered_physical_properties=rendered_physical_properties.copy(),
 818                        dry_run=False,
 819                        run_pre_post_statements=False,
 820                    )
 821                    runtime_stage = RuntimeStage.EVALUATING
 822                    target_table_exists = True
 823
 824            evaluate_render_kwargs = {
 825                **common_render_kwargs,
 826                "runtime_stage": runtime_stage,
 827                "snapshot_table_exists": target_table_exists,
 828            }
 829
 830            wap_id: t.Optional[str] = None
 831            if (
 832                snapshot.is_materialized
 833                and target_table_exists
 834                and adapter.wap_enabled
 835                and (model.wap_supported or adapter.wap_supported(target_table_name))
 836            ):
 837                wap_id = random_id()[0:8]
 838                logger.info("Using WAP ID '%s' for snapshot %s", wap_id, snapshot.snapshot_id)
 839                target_table_name = adapter.wap_prepare(target_table_name, wap_id)
 840
 841            self._render_and_insert_snapshot(
 842                start=start,
 843                end=end,
 844                execution_time=execution_time,
 845                snapshot=snapshot,
 846                snapshots=snapshots,
 847                render_kwargs=evaluate_render_kwargs,
 848                create_render_kwargs=create_render_kwargs,
 849                rendered_physical_properties=rendered_physical_properties,
 850                deployability_index=deployability_index,
 851                target_table_name=target_table_name,
 852                is_first_insert=is_first_insert,
 853                batch_index=batch_index,
 854            )
 855
 856            evaluation_strategy.run_post_statements(
 857                snapshot=snapshot,
 858                render_kwargs={**render_statements_kwargs, "inside_transaction": True},
 859            )
 860
 861        evaluation_strategy.run_post_statements(
 862            snapshot=snapshot,
 863            render_kwargs={**render_statements_kwargs, "inside_transaction": False},
 864        )
 865
 866        return wap_id
 867
 868    def create_snapshot(
 869        self,
 870        snapshot: Snapshot,
 871        snapshots: t.Dict[str, Snapshot],
 872        deployability_index: DeployabilityIndex,
 873        allow_destructive_snapshots: t.Set[str],
 874        allow_additive_snapshots: t.Set[str],
 875        on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]] = None,
 876    ) -> None:
 877        """Creates a physical table for the given snapshot.
 878
 879        Args:
 880            snapshot: Snapshot to create.
 881            snapshots: All upstream snapshots to use for expansion and mapping of physical locations.
 882            deployability_index: Determines snapshots that are deployable in the context of this creation.
 883            on_complete: A callback to call on each successfully created database object.
 884            allow_destructive_snapshots: Snapshots for which destructive schema changes are allowed.
 885            allow_additive_snapshots: Snapshots for which additive schema changes are allowed.
 886        """
 887        if not snapshot.is_model:
 888            return
 889
 890        logger.info("Creating a physical table for snapshot %s", snapshot.snapshot_id)
 891
 892        adapter = self.get_adapter(snapshot.model.gateway)
 893        create_render_kwargs: t.Dict[str, t.Any] = dict(
 894            engine_adapter=adapter,
 895            snapshots=snapshots,
 896            runtime_stage=RuntimeStage.CREATING,
 897            deployability_index=deployability_index,
 898        )
 899
 900        evaluation_strategy = _evaluation_strategy(snapshot, adapter)
 901        evaluation_strategy.run_pre_statements(
 902            snapshot=snapshot, render_kwargs={**create_render_kwargs, "inside_transaction": False}
 903        )
 904
 905        with (
 906            adapter.transaction(),
 907            adapter.session(snapshot.model.render_session_properties(**create_render_kwargs)),
 908        ):
 909            rendered_physical_properties = snapshot.model.render_physical_properties(
 910                **create_render_kwargs
 911            )
 912
 913            if self._can_clone(snapshot, deployability_index):
 914                self._clone_snapshot_in_dev(
 915                    snapshot=snapshot,
 916                    snapshots=snapshots,
 917                    deployability_index=deployability_index,
 918                    render_kwargs=create_render_kwargs,
 919                    rendered_physical_properties=rendered_physical_properties,
 920                    allow_destructive_snapshots=allow_destructive_snapshots,
 921                    allow_additive_snapshots=allow_additive_snapshots,
 922                    run_pre_post_statements=True,
 923                )
 924            else:
 925                is_table_deployable = deployability_index.is_deployable(snapshot)
 926                self._execute_create(
 927                    snapshot=snapshot,
 928                    table_name=snapshot.table_name(is_deployable=is_table_deployable),
 929                    is_table_deployable=is_table_deployable,
 930                    deployability_index=deployability_index,
 931                    create_render_kwargs=create_render_kwargs,
 932                    rendered_physical_properties=rendered_physical_properties,
 933                    dry_run=True,
 934                )
 935
 936        evaluation_strategy.run_post_statements(
 937            snapshot=snapshot, render_kwargs={**create_render_kwargs, "inside_transaction": False}
 938        )
 939
 940        if on_complete is not None:
 941            on_complete(snapshot)
 942
 943    def wap_publish_snapshot(
 944        self,
 945        snapshot: Snapshot,
 946        wap_id: str,
 947        deployability_index: t.Optional[DeployabilityIndex],
 948    ) -> None:
 949        deployability_index = deployability_index or DeployabilityIndex.all_deployable()
 950        table_name = snapshot.table_name(is_deployable=deployability_index.is_deployable(snapshot))
 951        adapter = self.get_adapter(snapshot.model_gateway)
 952        adapter.wap_publish(table_name, wap_id)
 953
 954    def _render_and_insert_snapshot(
 955        self,
 956        start: TimeLike,
 957        end: TimeLike,
 958        execution_time: TimeLike,
 959        snapshot: Snapshot,
 960        snapshots: t.Dict[str, Snapshot],
 961        render_kwargs: t.Dict[str, t.Any],
 962        create_render_kwargs: t.Dict[str, t.Any],
 963        rendered_physical_properties: t.Dict[str, exp.Expr],
 964        deployability_index: DeployabilityIndex,
 965        target_table_name: str,
 966        is_first_insert: bool,
 967        batch_index: int,
 968    ) -> None:
 969        if not snapshot.is_model or snapshot.is_seed:
 970            return
 971
 972        logger.info("Inserting data for snapshot %s", snapshot.snapshot_id)
 973
 974        model = snapshot.model
 975        adapter = self.get_adapter(model.gateway)
 976        evaluation_strategy = _evaluation_strategy(snapshot, adapter)
 977        is_snapshot_deployable = deployability_index.is_deployable(snapshot)
 978
 979        queries_or_dfs = self._render_snapshot_for_evaluation(
 980            snapshot,
 981            snapshots,
 982            deployability_index,
 983            render_kwargs,
 984        )
 985
 986        def apply(query_or_df: QueryOrDF, index: int = 0) -> None:
 987            if index > 0:
 988                evaluation_strategy.append(
 989                    table_name=target_table_name,
 990                    query_or_df=query_or_df,
 991                    model=snapshot.model,
 992                    snapshot=snapshot,
 993                    snapshots=snapshots,
 994                    deployability_index=deployability_index,
 995                    batch_index=batch_index,
 996                    start=start,
 997                    end=end,
 998                    execution_time=execution_time,
 999                    physical_properties=rendered_physical_properties,
1000                    render_kwargs=create_render_kwargs,
1001                    is_snapshot_deployable=is_snapshot_deployable,
1002                )
1003            else:
1004                logger.info(
1005                    "Inserting batch (%s, %s) into %s'",
1006                    time_like_to_str(start),
1007                    time_like_to_str(end),
1008                    target_table_name,
1009                )
1010                evaluation_strategy.insert(
1011                    table_name=target_table_name,
1012                    query_or_df=query_or_df,
1013                    is_first_insert=is_first_insert,
1014                    model=snapshot.model,
1015                    snapshot=snapshot,
1016                    snapshots=snapshots,
1017                    deployability_index=deployability_index,
1018                    batch_index=batch_index,
1019                    start=start,
1020                    end=end,
1021                    execution_time=execution_time,
1022                    physical_properties=rendered_physical_properties,
1023                    render_kwargs=create_render_kwargs,
1024                    is_snapshot_deployable=is_snapshot_deployable,
1025                )
1026
1027        # DataFrames, unlike SQL expressions, can provide partial results by yielding dataframes. As a result,
1028        # if the engine supports INSERT OVERWRITE or REPLACE WHERE and the snapshot is incremental by time range, we risk
1029        # having a partial result since each dataframe write can re-truncate partitions. To avoid this, we
1030        # union all the dataframes together before writing. For pandas this could result in OOM and a potential
1031        # workaround for that would be to serialize pandas to disk and then read it back with Spark.
1032        # Note: We assume that if multiple things are yielded from `queries_or_dfs` that they are dataframes
1033        # and not SQL expressions.
1034        if (
1035            adapter.INSERT_OVERWRITE_STRATEGY
1036            in (
1037                InsertOverwriteStrategy.INSERT_OVERWRITE,
1038                InsertOverwriteStrategy.REPLACE_WHERE,
1039            )
1040            and snapshot.is_incremental_by_time_range
1041        ):
1042            import pandas as pd
1043
1044            try:
1045                first_query_or_df = next(queries_or_dfs)
1046            except StopIteration:
1047                return
1048
1049            query_or_df = reduce(
1050                lambda a, b: (
1051                    pd.concat([a, b], ignore_index=True)  # type: ignore
1052                    if isinstance(a, pd.DataFrame)
1053                    else a.union_all(b)  # type: ignore
1054                ),  # type: ignore
1055                queries_or_dfs,
1056                first_query_or_df,
1057            )
1058            apply(query_or_df, index=0)
1059        else:
1060            for index, query_or_df in enumerate(queries_or_dfs):
1061                apply(query_or_df, index)
1062
1063    def _render_snapshot_for_evaluation(
1064        self,
1065        snapshot: Snapshot,
1066        snapshots: t.Dict[str, Snapshot],
1067        deployability_index: DeployabilityIndex,
1068        render_kwargs: t.Dict[str, t.Any],
1069    ) -> t.Iterator[QueryOrDF]:
1070        from sqlmesh.core.context import ExecutionContext
1071
1072        model = snapshot.model
1073        adapter = self.get_adapter(model.gateway)
1074
1075        return model.render(
1076            context=ExecutionContext(
1077                adapter,
1078                snapshots,
1079                deployability_index,
1080                default_dialect=model.dialect,
1081                default_catalog=model.default_catalog,
1082            ),
1083            **render_kwargs,
1084        )
1085
1086    def _clone_snapshot_in_dev(
1087        self,
1088        snapshot: Snapshot,
1089        snapshots: t.Dict[str, Snapshot],
1090        deployability_index: DeployabilityIndex,
1091        render_kwargs: t.Dict[str, t.Any],
1092        rendered_physical_properties: t.Dict[str, exp.Expr],
1093        allow_destructive_snapshots: t.Set[str],
1094        allow_additive_snapshots: t.Set[str],
1095        run_pre_post_statements: bool = False,
1096    ) -> None:
1097        adapter = self.get_adapter(snapshot.model.gateway)
1098
1099        target_table_name = snapshot.table_name(is_deployable=False)
1100        source_table_name = snapshot.table_name()
1101
1102        try:
1103            logger.info(f"Cloning table '{source_table_name}' into '{target_table_name}'")
1104            adapter.clone_table(
1105                target_table_name,
1106                snapshot.table_name(),
1107                rendered_physical_properties=rendered_physical_properties,
1108            )
1109            self._migrate_target_table(
1110                target_table_name=target_table_name,
1111                snapshot=snapshot,
1112                snapshots=snapshots,
1113                deployability_index=deployability_index,
1114                render_kwargs=render_kwargs,
1115                rendered_physical_properties=rendered_physical_properties,
1116                allow_destructive_snapshots=allow_destructive_snapshots,
1117                allow_additive_snapshots=allow_additive_snapshots,
1118                run_pre_post_statements=run_pre_post_statements,
1119            )
1120
1121        except Exception:
1122            adapter.drop_table(target_table_name)
1123            raise
1124
1125    def _migrate_snapshot(
1126        self,
1127        snapshot: Snapshot,
1128        snapshots: t.Dict[str, Snapshot],
1129        target_data_object: t.Optional[DataObject],
1130        allow_destructive_snapshots: t.Set[str],
1131        allow_additive_snapshots: t.Set[str],
1132        adapter: EngineAdapter,
1133        deployability_index: DeployabilityIndex,
1134    ) -> None:
1135        if not snapshot.is_model or snapshot.is_symbolic:
1136            return
1137
1138        deployability_index = DeployabilityIndex.all_deployable()
1139        render_kwargs: t.Dict[str, t.Any] = dict(
1140            engine_adapter=adapter,
1141            snapshots=snapshots,
1142            runtime_stage=RuntimeStage.CREATING,
1143            deployability_index=deployability_index,
1144        )
1145        target_table_name = snapshot.table_name()
1146
1147        evaluation_strategy = _evaluation_strategy(snapshot, adapter)
1148        evaluation_strategy.run_pre_statements(
1149            snapshot=snapshot, render_kwargs={**render_kwargs, "inside_transaction": False}
1150        )
1151
1152        with (
1153            adapter.transaction(),
1154            adapter.session(snapshot.model.render_session_properties(**render_kwargs)),
1155        ):
1156            table_exists = target_data_object is not None
1157            if adapter.drop_data_object_on_type_mismatch(
1158                target_data_object, _snapshot_to_data_object_type(snapshot)
1159            ):
1160                table_exists = False
1161
1162            rendered_physical_properties = snapshot.model.render_physical_properties(
1163                **render_kwargs
1164            )
1165
1166            if table_exists:
1167                self._migrate_target_table(
1168                    target_table_name=target_table_name,
1169                    snapshot=snapshot,
1170                    snapshots=snapshots,
1171                    deployability_index=deployability_index,
1172                    render_kwargs=render_kwargs,
1173                    rendered_physical_properties=rendered_physical_properties,
1174                    allow_destructive_snapshots=allow_destructive_snapshots,
1175                    allow_additive_snapshots=allow_additive_snapshots,
1176                    run_pre_post_statements=True,
1177                )
1178            else:
1179                self._execute_create(
1180                    snapshot=snapshot,
1181                    table_name=snapshot.table_name(is_deployable=True),
1182                    is_table_deployable=True,
1183                    deployability_index=deployability_index,
1184                    create_render_kwargs=render_kwargs,
1185                    rendered_physical_properties=rendered_physical_properties,
1186                    dry_run=True,
1187                )
1188
1189        evaluation_strategy.run_post_statements(
1190            snapshot=snapshot, render_kwargs={**render_kwargs, "inside_transaction": False}
1191        )
1192
1193    # Retry in case when the table is migrated concurrently from another plan application
1194    @retry(
1195        reraise=True,
1196        stop=stop_after_attempt(5),
1197        wait=wait_exponential(min=1, max=16),
1198        retry=retry_if_not_exception_type(
1199            (DestructiveChangeError, AdditiveChangeError, MigrationNotSupportedError)
1200        ),
1201    )
1202    def _migrate_target_table(
1203        self,
1204        target_table_name: str,
1205        snapshot: Snapshot,
1206        snapshots: t.Dict[str, Snapshot],
1207        deployability_index: DeployabilityIndex,
1208        render_kwargs: t.Dict[str, t.Any],
1209        rendered_physical_properties: t.Dict[str, exp.Expr],
1210        allow_destructive_snapshots: t.Set[str],
1211        allow_additive_snapshots: t.Set[str],
1212        run_pre_post_statements: bool = False,
1213    ) -> None:
1214        adapter = self.get_adapter(snapshot.model.gateway)
1215
1216        tmp_table = exp.to_table(target_table_name)
1217        tmp_table.this.set("this", f"{tmp_table.name}_schema_tmp")
1218        tmp_table_name = tmp_table.sql()
1219
1220        if snapshot.is_materialized:
1221            self._execute_create(
1222                snapshot=snapshot,
1223                table_name=tmp_table_name,
1224                is_table_deployable=False,
1225                deployability_index=deployability_index,
1226                create_render_kwargs=render_kwargs,
1227                rendered_physical_properties=rendered_physical_properties,
1228                dry_run=False,
1229                run_pre_post_statements=run_pre_post_statements,
1230                skip_grants=True,  # skip grants for tmp table
1231            )
1232        try:
1233            evaluation_strategy = _evaluation_strategy(snapshot, adapter)
1234            logger.info(
1235                "Migrating table schema from '%s' to '%s'",
1236                tmp_table_name,
1237                target_table_name,
1238            )
1239            evaluation_strategy.migrate(
1240                target_table_name=target_table_name,
1241                source_table_name=tmp_table_name,
1242                snapshot=snapshot,
1243                snapshots=snapshots,
1244                allow_destructive_snapshots=allow_destructive_snapshots,
1245                allow_additive_snapshots=allow_additive_snapshots,
1246                ignore_destructive=snapshot.model.on_destructive_change.is_ignore,
1247                ignore_additive=snapshot.model.on_additive_change.is_ignore,
1248                deployability_index=deployability_index,
1249            )
1250        finally:
1251            if snapshot.is_materialized:
1252                adapter.drop_table(tmp_table_name)
1253
1254    def _promote_snapshot(
1255        self,
1256        snapshot: Snapshot,
1257        environment_naming_info: EnvironmentNamingInfo,
1258        deployability_index: DeployabilityIndex,
1259        on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]],
1260        start: t.Optional[TimeLike] = None,
1261        end: t.Optional[TimeLike] = None,
1262        execution_time: t.Optional[TimeLike] = None,
1263        snapshots: t.Optional[t.Dict[SnapshotId, Snapshot]] = None,
1264        table_mapping: t.Optional[t.Dict[str, str]] = None,
1265    ) -> None:
1266        if not snapshot.is_model:
1267            return
1268
1269        adapter = (
1270            self.get_adapter(snapshot.model_gateway)
1271            if environment_naming_info.gateway_managed
1272            else self.adapter
1273        )
1274        table_name = snapshot.table_name(deployability_index.is_representative(snapshot))
1275        view_name = snapshot.qualified_view_name.for_environment(
1276            environment_naming_info, dialect=adapter.dialect
1277        )
1278        render_kwargs: t.Dict[str, t.Any] = dict(
1279            start=start,
1280            end=end,
1281            execution_time=execution_time,
1282            engine_adapter=adapter,
1283            deployability_index=deployability_index,
1284            table_mapping=table_mapping,
1285            runtime_stage=RuntimeStage.PROMOTING,
1286        )
1287
1288        with (
1289            adapter.transaction(),
1290            adapter.session(snapshot.model.render_session_properties(**render_kwargs)),
1291        ):
1292            _evaluation_strategy(snapshot, adapter).promote(
1293                table_name=table_name,
1294                view_name=view_name,
1295                model=snapshot.model,
1296                environment=environment_naming_info.name,
1297                snapshots=snapshots,
1298                snapshot=snapshot,
1299                **render_kwargs,
1300            )
1301
1302            snapshot_by_name = {s.name: s for s in (snapshots or {}).values()}
1303            render_kwargs["snapshots"] = snapshot_by_name
1304            adapter.execute(snapshot.model.render_on_virtual_update(**render_kwargs))
1305
1306        if on_complete is not None:
1307            on_complete(snapshot)
1308
1309    def _demote_snapshot(
1310        self,
1311        snapshot: Snapshot,
1312        environment_naming_info: EnvironmentNamingInfo,
1313        deployability_index: t.Optional[DeployabilityIndex],
1314        on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]],
1315        table_mapping: t.Optional[t.Dict[str, str]] = None,
1316    ) -> None:
1317        if not snapshot.is_model:
1318            return
1319
1320        adapter = (
1321            self.get_adapter(snapshot.model_gateway)
1322            if environment_naming_info.gateway_managed
1323            else self.adapter
1324        )
1325        view_name = snapshot.qualified_view_name.for_environment(
1326            environment_naming_info, dialect=adapter.dialect
1327        )
1328        with (
1329            adapter.transaction(),
1330            adapter.session(
1331                snapshot.model.render_session_properties(
1332                    engine_adapter=adapter,
1333                    deployability_index=deployability_index,
1334                    table_mapping=table_mapping,
1335                    runtime_stage=RuntimeStage.DEMOTING,
1336                )
1337            ),
1338        ):
1339            _evaluation_strategy(snapshot, adapter).demote(view_name)
1340
1341        if on_complete is not None:
1342            on_complete(snapshot)
1343
1344    def _cleanup_snapshot(
1345        self,
1346        snapshot: SnapshotInfoLike,
1347        dev_table_only: bool,
1348        adapter: EngineAdapter,
1349        on_complete: t.Optional[t.Callable[[str], None]],
1350    ) -> None:
1351        snapshot = snapshot.table_info
1352
1353        table_names = [(False, snapshot.table_name(is_deployable=False))]
1354        if not dev_table_only:
1355            table_names.append((True, snapshot.table_name(is_deployable=True)))
1356
1357        evaluation_strategy = _evaluation_strategy(snapshot, adapter)
1358        for is_table_deployable, table_name in table_names:
1359            try:
1360                evaluation_strategy.delete(
1361                    table_name,
1362                    is_table_deployable=is_table_deployable,
1363                    physical_schema=snapshot.physical_schema,
1364                    # we need to set cascade=true or we will get a 'cant drop because other objects depend on it'-style
1365                    # error on engines that enforce referential integrity, such as Postgres
1366                    # this situation can happen when a snapshot expires but downstream view snapshots that reference it have not yet expired
1367                    cascade=True,
1368                )
1369            except Exception:
1370                # Use `get_data_object` to check if the table exists instead of `table_exists` since the former
1371                # is based on `INFORMATION_SCHEMA` and avoids touching the table directly.
1372                # This is important when the table name is malformed for some reason and running any statement
1373                # that touches the table would result in an error.
1374                if adapter.get_data_object(table_name) is not None:
1375                    raise
1376                logger.warning(
1377                    "Skipping cleanup of table '%s' because it does not exist", table_name
1378                )
1379
1380            if on_complete is not None:
1381                on_complete(table_name)
1382
1383    def _audit(
1384        self,
1385        audit: Audit,
1386        audit_args: t.Dict[t.Any, t.Any],
1387        snapshot: Snapshot,
1388        snapshots: t.Dict[str, Snapshot],
1389        start: t.Optional[TimeLike],
1390        end: t.Optional[TimeLike],
1391        execution_time: t.Optional[TimeLike],
1392        deployability_index: t.Optional[DeployabilityIndex],
1393        **kwargs: t.Any,
1394    ) -> AuditResult:
1395        if audit.skip:
1396            return AuditResult(
1397                audit=audit,
1398                audit_args=audit_args,
1399                model=snapshot.model_or_none,
1400                skipped=True,
1401            )
1402
1403        # Model's "blocking" argument takes precedence over the audit's default setting
1404        blocking = audit_args.pop("blocking", None)
1405        blocking = blocking == exp.true() if blocking else audit.blocking
1406
1407        adapter = self.get_adapter(snapshot.model_gateway)
1408
1409        kwargs = {
1410            "start": start,
1411            "end": end,
1412            "execution_time": execution_time,
1413            "snapshots": snapshots,
1414            "deployability_index": deployability_index,
1415            "engine_adapter": adapter,
1416            "runtime_stage": RuntimeStage.AUDITING,
1417            **audit_args,
1418            **kwargs,
1419        }
1420
1421        if snapshot.is_model:
1422            query = snapshot.model.render_audit_query(audit, **kwargs)
1423        elif isinstance(audit, StandaloneAudit):
1424            query = audit.render_audit_query(**kwargs)
1425        else:
1426            raise SQLMeshError("Expected model or standalone audit. {snapshot}: {audit}")
1427
1428        count, *_ = adapter.fetchone(
1429            select("COUNT(*)").from_(query.subquery("audit")),
1430            quote_identifiers=True,
1431        )  # type: ignore
1432
1433        return AuditResult(
1434            audit=audit,
1435            audit_args=audit_args,
1436            model=snapshot.model_or_none,
1437            count=count,
1438            query=query,
1439            blocking=blocking,
1440        )
1441
1442    def _create_catalogs(
1443        self,
1444        tables: t.Iterable[t.Union[exp.Table, str]],
1445        gateway: t.Optional[str] = None,
1446    ) -> None:
1447        # attempt to create catalogs for the virtual layer if possible
1448        adapter = self.get_adapter(gateway)
1449        if adapter.SUPPORTS_CREATE_DROP_CATALOG:
1450            unique_catalogs = {t.catalog for t in [exp.to_table(maybe_t) for maybe_t in tables]}
1451            for catalog_name in unique_catalogs:
1452                adapter.create_catalog(catalog_name)
1453
1454    def _create_schemas(
1455        self,
1456        gateway_table_pairs: t.Iterable[t.Tuple[t.Optional[str], t.Union[exp.Table, str]]],
1457    ) -> None:
1458        table_exprs = [(gateway, exp.to_table(t)) for gateway, t in gateway_table_pairs]
1459        unique_schemas = {
1460            (gateway, t.args["db"], t.args.get("catalog"))
1461            for gateway, t in table_exprs
1462            if t and t.db
1463        }
1464
1465        def _create_schema(
1466            gateway: t.Optional[str], schema_name: str, catalog: t.Optional[str]
1467        ) -> None:
1468            schema = schema_(schema_name, catalog)
1469            logger.info("Creating schema '%s'", schema)
1470            adapter = self.get_adapter(gateway)
1471            adapter.create_schema(schema)
1472
1473        with self.concurrent_context():
1474            concurrent_apply_to_values(
1475                list(unique_schemas),
1476                lambda item: _create_schema(item[0], item[1], item[2]),
1477                self.ddl_concurrent_tasks,
1478            )
1479
1480    def get_adapter(self, gateway: t.Optional[str] = None) -> EngineAdapter:
1481        """Returns the adapter for the specified gateway or the default adapter if none is provided."""
1482        if gateway:
1483            if adapter := self.adapters.get(gateway):
1484                return adapter
1485            raise SQLMeshError(f"Gateway '{gateway}' not found in the available engine adapters.")
1486        return self.adapter
1487
1488    def _execute_create(
1489        self,
1490        snapshot: Snapshot,
1491        table_name: str,
1492        is_table_deployable: bool,
1493        deployability_index: DeployabilityIndex,
1494        create_render_kwargs: t.Dict[str, t.Any],
1495        rendered_physical_properties: t.Dict[str, exp.Expr],
1496        dry_run: bool,
1497        run_pre_post_statements: bool = True,
1498        skip_grants: bool = False,
1499    ) -> None:
1500        adapter = self.get_adapter(snapshot.model.gateway)
1501        evaluation_strategy = _evaluation_strategy(snapshot, adapter)
1502
1503        # It can still be useful for some strategies to know if the snapshot was actually deployable
1504        is_snapshot_deployable = deployability_index.is_deployable(snapshot)
1505        is_snapshot_representative = deployability_index.is_representative(snapshot)
1506
1507        create_render_kwargs = {
1508            **create_render_kwargs,
1509            "table_mapping": {snapshot.name: table_name},
1510        }
1511        if run_pre_post_statements:
1512            evaluation_strategy.run_pre_statements(
1513                snapshot=snapshot,
1514                render_kwargs={**create_render_kwargs, "inside_transaction": True},
1515            )
1516        evaluation_strategy.create(
1517            table_name=table_name,
1518            model=snapshot.model,
1519            is_table_deployable=is_table_deployable,
1520            skip_grants=skip_grants,
1521            render_kwargs=create_render_kwargs,
1522            is_snapshot_deployable=is_snapshot_deployable,
1523            is_snapshot_representative=is_snapshot_representative,
1524            dry_run=dry_run,
1525            physical_properties=rendered_physical_properties,
1526            snapshot=snapshot,
1527            deployability_index=deployability_index,
1528        )
1529        if run_pre_post_statements:
1530            evaluation_strategy.run_post_statements(
1531                snapshot=snapshot,
1532                render_kwargs={**create_render_kwargs, "inside_transaction": True},
1533            )
1534
1535    def _can_clone(self, snapshot: Snapshot, deployability_index: DeployabilityIndex) -> bool:
1536        adapter = self.get_adapter(snapshot.model.gateway)
1537        return (
1538            snapshot.is_forward_only
1539            and snapshot.is_materialized
1540            and bool(snapshot.previous_versions)
1541            and adapter.SUPPORTS_CLONING
1542            # managed models cannot have their schema mutated because they're based on queries, so clone + alter won't work
1543            and not snapshot.is_managed
1544            and not snapshot.is_dbt_custom
1545            and not deployability_index.is_deployable(snapshot)
1546            # If the deployable table is missing we can't clone it
1547            and adapter.table_exists(snapshot.table_name())
1548        )
1549
1550    def _get_physical_data_objects(
1551        self,
1552        target_snapshots: t.Iterable[Snapshot],
1553        deployability_index: DeployabilityIndex,
1554    ) -> t.Dict[SnapshotId, DataObject]:
1555        """Returns a dictionary of snapshot IDs to existing data objects of their physical tables.
1556
1557        Args:
1558            target_snapshots: Target snapshots.
1559            deployability_index: The deployability index to determine whether to look for a deployable or
1560                a non-deployable physical table.
1561
1562        Returns:
1563            A dictionary of snapshot IDs to existing data objects of their physical tables. If the data object
1564            for a snapshot is not found, it will not be included in the dictionary.
1565        """
1566        return self._get_data_objects(
1567            target_snapshots,
1568            lambda s: exp.to_table(
1569                s.table_name(deployability_index.is_deployable(s)), dialect=s.model.dialect
1570            ),
1571        )
1572
1573    def _get_virtual_data_objects(
1574        self,
1575        target_snapshots: t.Iterable[Snapshot],
1576        environment_naming_info: EnvironmentNamingInfo,
1577    ) -> t.Dict[SnapshotId, DataObject]:
1578        """Returns a dictionary of snapshot IDs to existing data objects of their virtual views.
1579
1580        Args:
1581            target_snapshots: Target snapshots.
1582             environment_naming_info: The environment naming info of the target virtual environment.
1583
1584        Returns:
1585            A dictionary of snapshot IDs to existing data objects of their virtual views. If the data object
1586            for a snapshot is not found, it will not be included in the dictionary.
1587        """
1588
1589        def _get_view_name(s: Snapshot) -> exp.Table:
1590            adapter = (
1591                self.get_adapter(s.model_gateway)
1592                if environment_naming_info.gateway_managed
1593                else self.adapter
1594            )
1595            return exp.to_table(
1596                s.qualified_view_name.for_environment(
1597                    environment_naming_info, dialect=adapter.dialect
1598                ),
1599                dialect=adapter.dialect,
1600            )
1601
1602        return self._get_data_objects(target_snapshots, _get_view_name)
1603
1604    def _get_data_objects(
1605        self,
1606        target_snapshots: t.Iterable[Snapshot],
1607        table_name_callable: t.Callable[[Snapshot], exp.Table],
1608    ) -> t.Dict[SnapshotId, DataObject]:
1609        """Returns a dictionary of snapshot IDs to existing data objects.
1610
1611        Args:
1612            target_snapshots: Target snapshots.
1613            table_name_callable: A function that takes a snapshot and returns the table to look for.
1614
1615        Returns:
1616            A dictionary of snapshot IDs to existing data objects. If the data object for a snapshot is not found,
1617            it will not be included in the dictionary.
1618        """
1619        tables_by_gateway_and_schema: t.Dict[t.Union[str, None], t.Dict[exp.Table, set[str]]] = (
1620            defaultdict(lambda: defaultdict(set))
1621        )
1622        snapshots_by_table_name: t.Dict[exp.Table, t.Dict[str, Snapshot]] = defaultdict(dict)
1623        for snapshot in target_snapshots:
1624            if not snapshot.is_model or snapshot.is_symbolic:
1625                continue
1626            table = table_name_callable(snapshot)
1627            table_schema = d.schema_(table.db, catalog=table.catalog)
1628            tables_by_gateway_and_schema[snapshot.model_gateway][table_schema].add(table.name)
1629            snapshots_by_table_name[table_schema][table.name] = snapshot
1630
1631        def _get_data_objects_in_schema(
1632            schema: exp.Table,
1633            object_names: t.Optional[t.Set[str]] = None,
1634            gateway: t.Optional[str] = None,
1635        ) -> t.List[DataObject]:
1636            logger.info("Listing data objects in schema %s", schema.sql())
1637            return self.get_adapter(gateway).get_data_objects(
1638                schema, object_names, safe_to_cache=True
1639            )
1640
1641        with self.concurrent_context():
1642            snapshot_id_to_obj: t.Dict[SnapshotId, DataObject] = {}
1643            # A schema can be shared across multiple engines, so we need to group tables by both gateway and schema
1644            for gateway, tables_by_schema in tables_by_gateway_and_schema.items():
1645                schema_list = list(tables_by_schema.keys())
1646                results = concurrent_apply_to_values(
1647                    schema_list,
1648                    lambda s: _get_data_objects_in_schema(
1649                        schema=s, object_names=tables_by_schema.get(s), gateway=gateway
1650                    ),
1651                    self.ddl_concurrent_tasks,
1652                )
1653
1654                for schema, objs in zip(schema_list, results):
1655                    snapshots_by_name = snapshots_by_table_name.get(schema, {})
1656                    for obj in objs:
1657                        if obj.name in snapshots_by_name:
1658                            snapshot_id_to_obj[snapshots_by_name[obj.name].snapshot_id] = obj
1659
1660        return snapshot_id_to_obj

Evaluates a snapshot given runtime arguments through an arbitrary EngineAdapter.

The SnapshotEvaluator contains the business logic to generically evaluate a snapshot. It is responsible for delegating queries to the EngineAdapter. The SnapshotEvaluator does not directly communicate with the underlying execution engine.

Arguments:
  • adapters: A single EngineAdapter or a dictionary of EngineAdapters where the key is the gateway name. When a dictionary is provided, and not an explicit default gateway its first item is treated as the default adapter and used for the virtual layer.
  • ddl_concurrent_tasks: The number of concurrent tasks used for DDL operations (table / view creation, deletion, etc). Default: 1.
SnapshotEvaluator( adapters: <MagicMock name='mock.__or__()' id='130969723464256'>, ddl_concurrent_tasks: int = 1, selected_gateway: Optional[str] = None)
129    def __init__(
130        self,
131        adapters: EngineAdapter | t.Dict[str, EngineAdapter],
132        ddl_concurrent_tasks: int = 1,
133        selected_gateway: t.Optional[str] = None,
134    ):
135        self.adapters = (
136            adapters if isinstance(adapters, t.Dict) else {selected_gateway or "": adapters}
137        )
138        self.execution_tracker = QueryExecutionTracker()
139        self.adapters = {
140            gateway: adapter.with_settings(query_execution_tracker=self.execution_tracker)
141            for gateway, adapter in self.adapters.items()
142        }
143        self.adapter = (
144            next(iter(self.adapters.values()))
145            if not selected_gateway
146            else self.adapters[selected_gateway]
147        )
148        self.selected_gateway = selected_gateway
149        self.ddl_concurrent_tasks = ddl_concurrent_tasks
adapters
execution_tracker
adapter
selected_gateway
ddl_concurrent_tasks
def evaluate( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, *, start: Union[datetime.date, datetime.datetime, str, int, float], end: Union[datetime.date, datetime.datetime, str, int, float], execution_time: Union[datetime.date, datetime.datetime, str, int, float], snapshots: Dict[str, sqlmesh.core.snapshot.definition.Snapshot], allow_destructive_snapshots: Optional[Set[str]] = None, allow_additive_snapshots: Optional[Set[str]] = None, deployability_index: Optional[sqlmesh.core.snapshot.definition.DeployabilityIndex] = None, batch_index: int = 0, target_table_exists: Optional[bool] = None, **kwargs: Any) -> Optional[str]:
151    def evaluate(
152        self,
153        snapshot: Snapshot,
154        *,
155        start: TimeLike,
156        end: TimeLike,
157        execution_time: TimeLike,
158        snapshots: t.Dict[str, Snapshot],
159        allow_destructive_snapshots: t.Optional[t.Set[str]] = None,
160        allow_additive_snapshots: t.Optional[t.Set[str]] = None,
161        deployability_index: t.Optional[DeployabilityIndex] = None,
162        batch_index: int = 0,
163        target_table_exists: t.Optional[bool] = None,
164        **kwargs: t.Any,
165    ) -> t.Optional[str]:
166        """Renders the snapshot's model, executes it and stores the result in the snapshot's physical table.
167
168        Args:
169            snapshot: Snapshot to evaluate.
170            start: The start datetime to render.
171            end: The end datetime to render.
172            execution_time: The date/time time reference to use for execution time.
173            snapshots: All upstream snapshots (by name) to use for expansion and mapping of physical locations.
174            allow_destructive_snapshots: Snapshots for which destructive schema changes are allowed.
175            allow_additive_snapshots: Snapshots for which additive schema changes are allowed.
176            deployability_index: Determines snapshots that are deployable in the context of this evaluation.
177            batch_index: If the snapshot is part of a batch of related snapshots; which index in the batch is it
178            target_table_exists: Whether the target table exists. If None, the table will be checked for existence.
179            kwargs: Additional kwargs to pass to the renderer.
180
181        Returns:
182            The WAP ID of this evaluation if supported, None otherwise.
183        """
184        with self.execution_tracker.track_execution(
185            SnapshotIdBatch(snapshot_id=snapshot.snapshot_id, batch_id=batch_index)
186        ):
187            result = self._evaluate_snapshot(
188                start=start,
189                end=end,
190                execution_time=execution_time,
191                snapshot=snapshot,
192                snapshots=snapshots,
193                allow_destructive_snapshots=allow_destructive_snapshots or set(),
194                allow_additive_snapshots=allow_additive_snapshots or set(),
195                deployability_index=deployability_index,
196                batch_index=batch_index,
197                target_table_exists=target_table_exists,
198                **kwargs,
199            )
200        if result is None or isinstance(result, str):
201            return result
202        raise SQLMeshError(
203            f"Unexpected result {result} when evaluating snapshot {snapshot.snapshot_id}."
204        )

Renders the snapshot's model, executes it and stores the result in the snapshot's physical table.

Arguments:
  • snapshot: Snapshot to evaluate.
  • start: The start datetime to render.
  • end: The end datetime to render.
  • execution_time: The date/time time reference to use for execution time.
  • snapshots: All upstream snapshots (by name) to use for expansion and mapping of physical locations.
  • allow_destructive_snapshots: Snapshots for which destructive schema changes are allowed.
  • allow_additive_snapshots: Snapshots for which additive schema changes are allowed.
  • deployability_index: Determines snapshots that are deployable in the context of this evaluation.
  • batch_index: If the snapshot is part of a batch of related snapshots; which index in the batch is it
  • target_table_exists: Whether the target table exists. If None, the table will be checked for existence.
  • kwargs: Additional kwargs to pass to the renderer.
Returns:

The WAP ID of this evaluation if supported, None otherwise.

def evaluate_and_fetch( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, *, start: Union[datetime.date, datetime.datetime, str, int, float], end: Union[datetime.date, datetime.datetime, str, int, float], execution_time: Union[datetime.date, datetime.datetime, str, int, float], snapshots: Dict[str, sqlmesh.core.snapshot.definition.Snapshot], limit: int, deployability_index: Optional[sqlmesh.core.snapshot.definition.DeployabilityIndex] = None, **kwargs: Any) -> <MagicMock id='130969721622432'>:
206    def evaluate_and_fetch(
207        self,
208        snapshot: Snapshot,
209        *,
210        start: TimeLike,
211        end: TimeLike,
212        execution_time: TimeLike,
213        snapshots: t.Dict[str, Snapshot],
214        limit: int,
215        deployability_index: t.Optional[DeployabilityIndex] = None,
216        **kwargs: t.Any,
217    ) -> DF:
218        """Renders the snapshot's model, executes it and returns a dataframe with the result.
219
220        Args:
221            snapshot: Snapshot to evaluate.
222            start: The start datetime to render.
223            end: The end datetime to render.
224            execution_time: The date/time time reference to use for execution time.
225            snapshots: All upstream snapshots (by name) to use for expansion and mapping of physical locations.
226            limit: The maximum number of rows to fetch.
227            deployability_index: Determines snapshots that are deployable in the context of this evaluation.
228            kwargs: Additional kwargs to pass to the renderer.
229
230        Returns:
231            The result of the evaluation as a dataframe.
232        """
233        import pandas as pd
234
235        adapter = self.get_adapter(snapshot.model.gateway)
236        render_kwargs = dict(
237            start=start,
238            end=end,
239            execution_time=execution_time,
240            snapshot=snapshot,
241            runtime_stage=RuntimeStage.EVALUATING,
242            **kwargs,
243        )
244        queries_or_dfs = self._render_snapshot_for_evaluation(
245            snapshot,
246            snapshots,
247            deployability_index or DeployabilityIndex.all_deployable(),
248            render_kwargs,
249        )
250        query_or_df = next(queries_or_dfs)
251        if isinstance(query_or_df, pd.DataFrame):
252            return query_or_df.head(limit)
253        if not isinstance(query_or_df, exp.Expr):
254            # We assume that if this branch is reached, `query_or_df` is a pyspark / snowpark / bigframe dataframe,
255            # so we use `limit` instead of `head` to get back a dataframe instead of List[Row]
256            # https://spark.apache.org/docs/3.1.1/api/python/reference/api/pyspark.sql.DataFrame.head.html#pyspark.sql.DataFrame.head
257            return query_or_df.limit(limit)
258
259        assert isinstance(query_or_df, exp.Query)
260
261        existing_limit = query_or_df.args.get("limit")
262        if existing_limit:
263            limit = min(limit, execute(exp.select(existing_limit.expression)).rows[0][0])
264            assert limit is not None
265
266        return adapter._fetch_native_df(query_or_df.limit(limit))

Renders the snapshot's model, executes it and returns a dataframe with the result.

Arguments:
  • snapshot: Snapshot to evaluate.
  • start: The start datetime to render.
  • end: The end datetime to render.
  • execution_time: The date/time time reference to use for execution time.
  • snapshots: All upstream snapshots (by name) to use for expansion and mapping of physical locations.
  • limit: The maximum number of rows to fetch.
  • deployability_index: Determines snapshots that are deployable in the context of this evaluation.
  • kwargs: Additional kwargs to pass to the renderer.
Returns:

The result of the evaluation as a dataframe.

def promote( self, target_snapshots: Iterable[sqlmesh.core.snapshot.definition.Snapshot], environment_naming_info: <MagicMock id='130969721882944'>, deployability_index: Optional[sqlmesh.core.snapshot.definition.DeployabilityIndex] = None, start: Union[datetime.date, datetime.datetime, str, int, float, NoneType] = None, end: Union[datetime.date, datetime.datetime, str, int, float, NoneType] = None, execution_time: Union[datetime.date, datetime.datetime, str, int, float, NoneType] = None, snapshots: Optional[Dict[sqlmesh.core.snapshot.definition.SnapshotId, sqlmesh.core.snapshot.definition.Snapshot]] = None, table_mapping: Optional[Dict[str, str]] = None, on_complete: Optional[Callable[[Union[sqlmesh.core.snapshot.definition.SnapshotTableInfo, sqlmesh.core.snapshot.definition.Snapshot]], NoneType]] = None) -> None:
268    def promote(
269        self,
270        target_snapshots: t.Iterable[Snapshot],
271        environment_naming_info: EnvironmentNamingInfo,
272        deployability_index: t.Optional[DeployabilityIndex] = None,
273        start: t.Optional[TimeLike] = None,
274        end: t.Optional[TimeLike] = None,
275        execution_time: t.Optional[TimeLike] = None,
276        snapshots: t.Optional[t.Dict[SnapshotId, Snapshot]] = None,
277        table_mapping: t.Optional[t.Dict[str, str]] = None,
278        on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]] = None,
279    ) -> None:
280        """Promotes the given collection of snapshots in the target environment by replacing a corresponding
281        view with a physical table associated with the given snapshot.
282
283        Args:
284            target_snapshots: Snapshots to promote.
285            environment_naming_info: Naming information for the target environment.
286            deployability_index: Determines snapshots that are deployable in the context of this promotion.
287            on_complete: A callback to call on each successfully promoted snapshot.
288        """
289
290        tables_by_gateway: t.Dict[t.Union[str, None], t.List[exp.Table]] = defaultdict(list)
291        for snapshot in target_snapshots:
292            if snapshot.is_model and not snapshot.is_symbolic:
293                gateway = (
294                    snapshot.model_gateway if environment_naming_info.gateway_managed else None
295                )
296                adapter = self.get_adapter(gateway)
297                table = snapshot.qualified_view_name.table_for_environment(
298                    environment_naming_info, dialect=adapter.dialect
299                )
300                tables_by_gateway[gateway].append(table)
301
302        # A schema can be shared across multiple engines, so we need to group by gateway
303        for gateway, tables in tables_by_gateway.items():
304            if environment_naming_info.suffix_target.is_catalog:
305                self._create_catalogs(tables=tables, gateway=gateway)
306
307        gateway_table_pairs = [
308            (gateway, table) for gateway, tables in tables_by_gateway.items() for table in tables
309        ]
310        self._create_schemas(gateway_table_pairs=gateway_table_pairs)
311
312        # Fetch the view data objects for the promoted snapshots to get them cached
313        self._get_virtual_data_objects(target_snapshots, environment_naming_info)
314
315        deployability_index = deployability_index or DeployabilityIndex.all_deployable()
316        with self.concurrent_context():
317            concurrent_apply_to_snapshots(
318                target_snapshots,
319                lambda s: self._promote_snapshot(
320                    s,
321                    start=start,
322                    end=end,
323                    execution_time=execution_time,
324                    snapshots=snapshots,
325                    table_mapping=table_mapping,
326                    environment_naming_info=environment_naming_info,
327                    deployability_index=deployability_index,  # type: ignore
328                    on_complete=on_complete,
329                ),
330                self.ddl_concurrent_tasks,
331            )

Promotes the given collection of snapshots in the target environment by replacing a corresponding view with a physical table associated with the given snapshot.

Arguments:
  • target_snapshots: Snapshots to promote.
  • environment_naming_info: Naming information for the target environment.
  • deployability_index: Determines snapshots that are deployable in the context of this promotion.
  • on_complete: A callback to call on each successfully promoted snapshot.
def demote( self, target_snapshots: Iterable[sqlmesh.core.snapshot.definition.Snapshot], environment_naming_info: <MagicMock id='130969721882944'>, table_mapping: Optional[Dict[str, str]] = None, deployability_index: Optional[sqlmesh.core.snapshot.definition.DeployabilityIndex] = None, on_complete: Optional[Callable[[Union[sqlmesh.core.snapshot.definition.SnapshotTableInfo, sqlmesh.core.snapshot.definition.Snapshot]], NoneType]] = None) -> None:
333    def demote(
334        self,
335        target_snapshots: t.Iterable[Snapshot],
336        environment_naming_info: EnvironmentNamingInfo,
337        table_mapping: t.Optional[t.Dict[str, str]] = None,
338        deployability_index: t.Optional[DeployabilityIndex] = None,
339        on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]] = None,
340    ) -> None:
341        """Demotes the given collection of snapshots in the target environment by removing its view.
342
343        Args:
344            target_snapshots: Snapshots to demote.
345            environment_naming_info: Naming info for the target environment.
346            on_complete: A callback to call on each successfully demoted snapshot.
347        """
348        with self.concurrent_context():
349            concurrent_apply_to_snapshots(
350                target_snapshots,
351                lambda s: self._demote_snapshot(
352                    s,
353                    environment_naming_info,
354                    deployability_index=deployability_index,
355                    on_complete=on_complete,
356                    table_mapping=table_mapping,
357                ),
358                self.ddl_concurrent_tasks,
359            )

Demotes the given collection of snapshots in the target environment by removing its view.

Arguments:
  • target_snapshots: Snapshots to demote.
  • environment_naming_info: Naming info for the target environment.
  • on_complete: A callback to call on each successfully demoted snapshot.
def create( self, target_snapshots: Iterable[sqlmesh.core.snapshot.definition.Snapshot], snapshots: Dict[sqlmesh.core.snapshot.definition.SnapshotId, sqlmesh.core.snapshot.definition.Snapshot], deployability_index: Optional[sqlmesh.core.snapshot.definition.DeployabilityIndex] = None, on_start: Optional[Callable] = None, on_complete: Optional[Callable[[Union[sqlmesh.core.snapshot.definition.SnapshotTableInfo, sqlmesh.core.snapshot.definition.Snapshot]], NoneType]] = None, allow_destructive_snapshots: Optional[Set[str]] = None, allow_additive_snapshots: Optional[Set[str]] = None) -> sqlmesh.utils.CompletionStatus:
361    def create(
362        self,
363        target_snapshots: t.Iterable[Snapshot],
364        snapshots: t.Dict[SnapshotId, Snapshot],
365        deployability_index: t.Optional[DeployabilityIndex] = None,
366        on_start: t.Optional[t.Callable] = None,
367        on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]] = None,
368        allow_destructive_snapshots: t.Optional[t.Set[str]] = None,
369        allow_additive_snapshots: t.Optional[t.Set[str]] = None,
370    ) -> CompletionStatus:
371        """Creates a physical snapshot schema and table for the given collection of snapshots.
372
373        Args:
374            target_snapshots: Target snapshots.
375            snapshots: Mapping of snapshot ID to snapshot.
376            deployability_index: Determines snapshots that are deployable in the context of this creation.
377            on_start: A callback to initialize the snapshot creation progress bar.
378            on_complete: A callback to call on each successfully created snapshot.
379            allow_destructive_snapshots: Set of snapshots that are allowed to have destructive schema changes.
380            allow_additive_snapshots: Set of snapshots that are allowed to have additive schema changes.
381
382        Returns:
383            CompletionStatus: The status of the creation operation (success, failure, nothing to do).
384        """
385        deployability_index = deployability_index or DeployabilityIndex.all_deployable()
386
387        snapshots_to_create = self.get_snapshots_to_create(target_snapshots, deployability_index)
388        if not snapshots_to_create:
389            return CompletionStatus.NOTHING_TO_DO
390        if on_start:
391            on_start(snapshots_to_create)
392
393        self._create_snapshots(
394            snapshots_to_create=snapshots_to_create,
395            snapshots={s.name: s for s in snapshots.values()},
396            deployability_index=deployability_index,
397            on_complete=on_complete,
398            allow_destructive_snapshots=allow_destructive_snapshots or set(),
399            allow_additive_snapshots=allow_additive_snapshots or set(),
400        )
401        return CompletionStatus.SUCCESS

Creates a physical snapshot schema and table for the given collection of snapshots.

Arguments:
  • target_snapshots: Target snapshots.
  • snapshots: Mapping of snapshot ID to snapshot.
  • deployability_index: Determines snapshots that are deployable in the context of this creation.
  • on_start: A callback to initialize the snapshot creation progress bar.
  • on_complete: A callback to call on each successfully created snapshot.
  • allow_destructive_snapshots: Set of snapshots that are allowed to have destructive schema changes.
  • allow_additive_snapshots: Set of snapshots that are allowed to have additive schema changes.
Returns:

CompletionStatus: The status of the creation operation (success, failure, nothing to do).

def create_physical_schemas( self, snapshots: Iterable[sqlmesh.core.snapshot.definition.Snapshot], deployability_index: sqlmesh.core.snapshot.definition.DeployabilityIndex) -> None:
403    def create_physical_schemas(
404        self, snapshots: t.Iterable[Snapshot], deployability_index: DeployabilityIndex
405    ) -> None:
406        """Creates the physical schemas for the given snapshots.
407
408        Args:
409            snapshots: Snapshots to create physical schemas for.
410            deployability_index: Determines snapshots that are deployable in the context of this creation.
411        """
412        tables_by_gateway: t.Dict[t.Optional[str], t.List[str]] = defaultdict(list)
413        for snapshot in snapshots:
414            if snapshot.is_model and not snapshot.is_symbolic:
415                tables_by_gateway[snapshot.model_gateway].append(
416                    snapshot.table_name(is_deployable=deployability_index.is_deployable(snapshot))
417                )
418
419        gateway_table_pairs = [
420            (gateway, table) for gateway, tables in tables_by_gateway.items() for table in tables
421        ]
422        self._create_schemas(gateway_table_pairs=gateway_table_pairs)

Creates the physical schemas for the given snapshots.

Arguments:
  • snapshots: Snapshots to create physical schemas for.
  • deployability_index: Determines snapshots that are deployable in the context of this creation.
def get_snapshots_to_create( self, target_snapshots: Iterable[sqlmesh.core.snapshot.definition.Snapshot], deployability_index: sqlmesh.core.snapshot.definition.DeployabilityIndex) -> List[sqlmesh.core.snapshot.definition.Snapshot]:
424    def get_snapshots_to_create(
425        self, target_snapshots: t.Iterable[Snapshot], deployability_index: DeployabilityIndex
426    ) -> t.List[Snapshot]:
427        """Returns a list of snapshots that need to have their physical tables created.
428
429        Args:
430            target_snapshots: Target snapshots.
431            deployability_index: Determines snapshots that are deployable / representative in the context of this creation.
432        """
433        existing_data_objects = self._get_physical_data_objects(
434            target_snapshots, deployability_index
435        )
436        snapshots_to_create = []
437        for snapshot in target_snapshots:
438            if not snapshot.is_model or snapshot.is_symbolic:
439                continue
440            if snapshot.snapshot_id not in existing_data_objects or (
441                snapshot.is_seed and not snapshot.intervals
442            ):
443                snapshots_to_create.append(snapshot)
444
445        return snapshots_to_create

Returns a list of snapshots that need to have their physical tables created.

Arguments:
  • target_snapshots: Target snapshots.
  • deployability_index: Determines snapshots that are deployable / representative in the context of this creation.
def migrate( self, target_snapshots: Iterable[sqlmesh.core.snapshot.definition.Snapshot], snapshots: Dict[sqlmesh.core.snapshot.definition.SnapshotId, sqlmesh.core.snapshot.definition.Snapshot], allow_destructive_snapshots: Optional[Set[str]] = None, allow_additive_snapshots: Optional[Set[str]] = None, deployability_index: Optional[sqlmesh.core.snapshot.definition.DeployabilityIndex] = None) -> None:
474    def migrate(
475        self,
476        target_snapshots: t.Iterable[Snapshot],
477        snapshots: t.Dict[SnapshotId, Snapshot],
478        allow_destructive_snapshots: t.Optional[t.Set[str]] = None,
479        allow_additive_snapshots: t.Optional[t.Set[str]] = None,
480        deployability_index: t.Optional[DeployabilityIndex] = None,
481    ) -> None:
482        """Alters a physical snapshot table to match its snapshot's schema for the given collection of snapshots.
483
484        Args:
485            target_snapshots: Target snapshots.
486            snapshots: Mapping of snapshot ID to snapshot.
487            allow_destructive_snapshots: Set of snapshots that are allowed to have destructive schema changes.
488            allow_additive_snapshots: Set of snapshots that are allowed to have additive schema changes.
489            deployability_index: Determines snapshots that are deployable in the context of this evaluation.
490        """
491        deployability_index = deployability_index or DeployabilityIndex.all_deployable()
492        target_data_objects = self._get_physical_data_objects(target_snapshots, deployability_index)
493        if not target_data_objects:
494            return
495
496        if not snapshots:
497            snapshots = {s.snapshot_id: s for s in target_snapshots}
498
499        allow_destructive_snapshots = allow_destructive_snapshots or set()
500        allow_additive_snapshots = allow_additive_snapshots or set()
501        snapshots_by_name = {s.name: s for s in snapshots.values()}
502        with self.concurrent_context():
503            # Only migrate snapshots for which there's an existing data object
504            concurrent_apply_to_snapshots(
505                target_snapshots,
506                lambda s: self._migrate_snapshot(
507                    s,
508                    snapshots_by_name,
509                    target_data_objects.get(s.snapshot_id),
510                    allow_destructive_snapshots,
511                    allow_additive_snapshots,
512                    self.get_adapter(s.model_gateway),
513                    deployability_index,
514                ),
515                self.ddl_concurrent_tasks,
516            )

Alters a physical snapshot table to match its snapshot's schema for the given collection of snapshots.

Arguments:
  • target_snapshots: Target snapshots.
  • snapshots: Mapping of snapshot ID to snapshot.
  • allow_destructive_snapshots: Set of snapshots that are allowed to have destructive schema changes.
  • allow_additive_snapshots: Set of snapshots that are allowed to have additive schema changes.
  • deployability_index: Determines snapshots that are deployable in the context of this evaluation.
def cleanup( self, target_snapshots: Iterable[sqlmesh.core.snapshot.definition.SnapshotTableCleanupTask], on_complete: Optional[Callable[[str], NoneType]] = None) -> None:
518    def cleanup(
519        self,
520        target_snapshots: t.Iterable[SnapshotTableCleanupTask],
521        on_complete: t.Optional[t.Callable[[str], None]] = None,
522    ) -> None:
523        """Cleans up the given snapshots by removing its table
524
525        Args:
526            target_snapshots: Snapshots to cleanup.
527            on_complete: A callback to call on each successfully deleted database object.
528        """
529        target_snapshots = [
530            t for t in target_snapshots if t.snapshot.is_model and not t.snapshot.is_symbolic
531        ]
532        available_gateways = set(self.adapters.keys())
533        skipped = []
534        filtered_targets = []
535        for t in target_snapshots:
536            gw = t.snapshot.model_gateway
537            if gw and gw not in available_gateways:
538                skipped.append((t.snapshot.snapshot_id, gw))
539            else:
540                filtered_targets.append(t)
541        if skipped:
542            logger.warning(
543                "Skipping cleanup of %d snapshot(s) with unavailable gateway(s): %s",
544                len(skipped),
545                ", ".join(f"{sid} (gateway={gw})" for sid, gw in skipped),
546            )
547        snapshots_to_dev_table_only = {
548            t.snapshot.snapshot_id: t.dev_table_only for t in filtered_targets
549        }
550        with self.concurrent_context():
551            errors, _ = concurrent_apply_to_snapshots(
552                [t.snapshot for t in filtered_targets],
553                lambda s: self._cleanup_snapshot(
554                    s,
555                    snapshots_to_dev_table_only[s.snapshot_id],
556                    self.get_adapter(s.model_gateway),
557                    on_complete,
558                ),
559                self.ddl_concurrent_tasks,
560                reverse_order=True,
561                raise_on_error=False,
562            )
563        if errors:
564            errored_snapshots = "\n".join(f"  {e.node.name}: {e.__cause__}" for e in errors)
565            raise SQLMeshError(f"\n{errored_snapshots}")

Cleans up the given snapshots by removing its table

Arguments:
  • target_snapshots: Snapshots to cleanup.
  • on_complete: A callback to call on each successfully deleted database object.
def audit( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, *, snapshots: Dict[str, sqlmesh.core.snapshot.definition.Snapshot], start: Union[datetime.date, datetime.datetime, str, int, float, NoneType] = None, end: Union[datetime.date, datetime.datetime, str, int, float, NoneType] = None, execution_time: Union[datetime.date, datetime.datetime, str, int, float, NoneType] = None, deployability_index: Optional[sqlmesh.core.snapshot.definition.DeployabilityIndex] = None, wap_id: Optional[str] = None, **kwargs: Any) -> List[sqlmesh.core.model.definition.AuditResult]:
567    def audit(
568        self,
569        snapshot: Snapshot,
570        *,
571        snapshots: t.Dict[str, Snapshot],
572        start: t.Optional[TimeLike] = None,
573        end: t.Optional[TimeLike] = None,
574        execution_time: t.Optional[TimeLike] = None,
575        deployability_index: t.Optional[DeployabilityIndex] = None,
576        wap_id: t.Optional[str] = None,
577        **kwargs: t.Any,
578    ) -> t.List[AuditResult]:
579        """Execute a snapshot's node's audit queries.
580
581        Args:
582            snapshot: Snapshot to evaluate.
583            snapshots: All upstream snapshots (by name) to use for expansion and mapping of physical locations.
584            start: The start datetime to audit. Defaults to epoch start.
585            end: The end datetime to audit. Defaults to epoch start.
586            execution_time: The date/time time reference to use for execution time.
587            deployability_index: Determines snapshots that are deployable in the context of this evaluation.
588            wap_id: The WAP ID if applicable, None otherwise.
589            kwargs: Additional kwargs to pass to the renderer.
590        """
591        deployability_index = deployability_index or DeployabilityIndex.all_deployable()
592        adapter = self.get_adapter(snapshot.model_gateway)
593
594        if not snapshot.version:
595            raise ConfigError(
596                f"Cannot audit '{snapshot.name}' because it has not been versioned yet. Apply a plan first."
597            )
598
599        if wap_id is not None:
600            deployability_index = deployability_index or DeployabilityIndex.all_deployable()
601            original_table_name = snapshot.table_name(
602                is_deployable=deployability_index.is_deployable(snapshot)
603            )
604            wap_table_name = adapter.wap_table_name(original_table_name, wap_id)
605            logger.info(
606                "Auditing WAP table '%s', snapshot %s",
607                wap_table_name,
608                snapshot.snapshot_id,
609            )
610
611            table_mapping = kwargs.get("table_mapping") or {}
612            table_mapping[snapshot.name] = wap_table_name
613            kwargs["table_mapping"] = table_mapping
614            kwargs["this_model"] = exp.to_table(wap_table_name, dialect=adapter.dialect)
615
616        results = []
617
618        audits_with_args = snapshot.node.audits_with_args
619
620        force_non_blocking = False
621
622        if audits_with_args:
623            logger.info("Auditing snapshot %s", snapshot.snapshot_id)
624
625            if not deployability_index.is_deployable(snapshot) and not adapter.SUPPORTS_CLONING:
626                # For dev preview tables that aren't based on clones of the production table, only a subset of the data is typically available
627                # However, users still expect audits to run anwyay. Some audits (such as row count) are practically guaranteed to fail
628                # when run on only a subset of data, so we switch all audits to non blocking and the user can decide if they still want to proceed
629                force_non_blocking = True
630
631        for audit, audit_args in audits_with_args:
632            if force_non_blocking:
633                # remove any blocking indicator on the model itself
634                audit_args.pop("blocking", None)
635                # so that we can fall back to the audit's setting, which we override to blocking: False
636                audit = audit.model_copy(update={"blocking": False})
637
638            results.append(
639                self._audit(
640                    audit=audit,
641                    audit_args=audit_args,
642                    snapshot=snapshot,
643                    snapshots=snapshots,
644                    start=start,
645                    end=end,
646                    execution_time=execution_time,
647                    deployability_index=deployability_index,
648                    **kwargs,
649                )
650            )
651
652        if wap_id is not None:
653            logger.info(
654                "Publishing evaluation results for snapshot %s, WAP ID '%s'",
655                snapshot.snapshot_id,
656                wap_id,
657            )
658            self.wap_publish_snapshot(snapshot, wap_id, deployability_index)
659
660        return results

Execute a snapshot's node's audit queries.

Arguments:
  • snapshot: Snapshot to evaluate.
  • snapshots: All upstream snapshots (by name) to use for expansion and mapping of physical locations.
  • start: The start datetime to audit. Defaults to epoch start.
  • end: The end datetime to audit. Defaults to epoch start.
  • execution_time: The date/time time reference to use for execution time.
  • deployability_index: Determines snapshots that are deployable in the context of this evaluation.
  • wap_id: The WAP ID if applicable, None otherwise.
  • kwargs: Additional kwargs to pass to the renderer.
@contextmanager
def concurrent_context(self) -> Iterator[NoneType]:
662    @contextmanager
663    def concurrent_context(self) -> t.Iterator[None]:
664        try:
665            yield
666        finally:
667            self.recycle()
def recycle(self) -> None:
669    def recycle(self) -> None:
670        """Closes all open connections and releases all allocated resources associated with any thread
671        except the calling one."""
672        try:
673            for adapter in self.adapters.values():
674                adapter.recycle()
675
676        except Exception:
677            logger.exception("Failed to recycle Snapshot Evaluator")

Closes all open connections and releases all allocated resources associated with any thread except the calling one.

def close(self) -> None:
679    def close(self) -> None:
680        """Closes all open connections and releases all allocated resources."""
681        try:
682            for adapter in self.adapters.values():
683                adapter.close()
684        except Exception:
685            logger.exception("Failed to close Snapshot Evaluator")

Closes all open connections and releases all allocated resources.

def set_correlation_id( self, correlation_id: sqlmesh.utils.CorrelationId) -> SnapshotEvaluator:
687    def set_correlation_id(self, correlation_id: CorrelationId) -> SnapshotEvaluator:
688        return SnapshotEvaluator(
689            {
690                gateway: adapter.with_settings(correlation_id=correlation_id)
691                for gateway, adapter in self.adapters.items()
692            },
693            self.ddl_concurrent_tasks,
694            self.selected_gateway,
695        )
def create_snapshot( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, snapshots: Dict[str, sqlmesh.core.snapshot.definition.Snapshot], deployability_index: sqlmesh.core.snapshot.definition.DeployabilityIndex, allow_destructive_snapshots: Set[str], allow_additive_snapshots: Set[str], on_complete: Optional[Callable[[Union[sqlmesh.core.snapshot.definition.SnapshotTableInfo, sqlmesh.core.snapshot.definition.Snapshot]], NoneType]] = None) -> None:
868    def create_snapshot(
869        self,
870        snapshot: Snapshot,
871        snapshots: t.Dict[str, Snapshot],
872        deployability_index: DeployabilityIndex,
873        allow_destructive_snapshots: t.Set[str],
874        allow_additive_snapshots: t.Set[str],
875        on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]] = None,
876    ) -> None:
877        """Creates a physical table for the given snapshot.
878
879        Args:
880            snapshot: Snapshot to create.
881            snapshots: All upstream snapshots to use for expansion and mapping of physical locations.
882            deployability_index: Determines snapshots that are deployable in the context of this creation.
883            on_complete: A callback to call on each successfully created database object.
884            allow_destructive_snapshots: Snapshots for which destructive schema changes are allowed.
885            allow_additive_snapshots: Snapshots for which additive schema changes are allowed.
886        """
887        if not snapshot.is_model:
888            return
889
890        logger.info("Creating a physical table for snapshot %s", snapshot.snapshot_id)
891
892        adapter = self.get_adapter(snapshot.model.gateway)
893        create_render_kwargs: t.Dict[str, t.Any] = dict(
894            engine_adapter=adapter,
895            snapshots=snapshots,
896            runtime_stage=RuntimeStage.CREATING,
897            deployability_index=deployability_index,
898        )
899
900        evaluation_strategy = _evaluation_strategy(snapshot, adapter)
901        evaluation_strategy.run_pre_statements(
902            snapshot=snapshot, render_kwargs={**create_render_kwargs, "inside_transaction": False}
903        )
904
905        with (
906            adapter.transaction(),
907            adapter.session(snapshot.model.render_session_properties(**create_render_kwargs)),
908        ):
909            rendered_physical_properties = snapshot.model.render_physical_properties(
910                **create_render_kwargs
911            )
912
913            if self._can_clone(snapshot, deployability_index):
914                self._clone_snapshot_in_dev(
915                    snapshot=snapshot,
916                    snapshots=snapshots,
917                    deployability_index=deployability_index,
918                    render_kwargs=create_render_kwargs,
919                    rendered_physical_properties=rendered_physical_properties,
920                    allow_destructive_snapshots=allow_destructive_snapshots,
921                    allow_additive_snapshots=allow_additive_snapshots,
922                    run_pre_post_statements=True,
923                )
924            else:
925                is_table_deployable = deployability_index.is_deployable(snapshot)
926                self._execute_create(
927                    snapshot=snapshot,
928                    table_name=snapshot.table_name(is_deployable=is_table_deployable),
929                    is_table_deployable=is_table_deployable,
930                    deployability_index=deployability_index,
931                    create_render_kwargs=create_render_kwargs,
932                    rendered_physical_properties=rendered_physical_properties,
933                    dry_run=True,
934                )
935
936        evaluation_strategy.run_post_statements(
937            snapshot=snapshot, render_kwargs={**create_render_kwargs, "inside_transaction": False}
938        )
939
940        if on_complete is not None:
941            on_complete(snapshot)

Creates a physical table for the given snapshot.

Arguments:
  • snapshot: Snapshot to create.
  • snapshots: All upstream snapshots to use for expansion and mapping of physical locations.
  • deployability_index: Determines snapshots that are deployable in the context of this creation.
  • on_complete: A callback to call on each successfully created database object.
  • allow_destructive_snapshots: Snapshots for which destructive schema changes are allowed.
  • allow_additive_snapshots: Snapshots for which additive schema changes are allowed.
def wap_publish_snapshot( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, wap_id: str, deployability_index: Optional[sqlmesh.core.snapshot.definition.DeployabilityIndex]) -> None:
943    def wap_publish_snapshot(
944        self,
945        snapshot: Snapshot,
946        wap_id: str,
947        deployability_index: t.Optional[DeployabilityIndex],
948    ) -> None:
949        deployability_index = deployability_index or DeployabilityIndex.all_deployable()
950        table_name = snapshot.table_name(is_deployable=deployability_index.is_deployable(snapshot))
951        adapter = self.get_adapter(snapshot.model_gateway)
952        adapter.wap_publish(table_name, wap_id)
def get_adapter(self, gateway: Optional[str] = None) -> <MagicMock id='130969723718864'>:
1480    def get_adapter(self, gateway: t.Optional[str] = None) -> EngineAdapter:
1481        """Returns the adapter for the specified gateway or the default adapter if none is provided."""
1482        if gateway:
1483            if adapter := self.adapters.get(gateway):
1484                return adapter
1485            raise SQLMeshError(f"Gateway '{gateway}' not found in the available engine adapters.")
1486        return self.adapter

Returns the adapter for the specified gateway or the default adapter if none is provided.

class EvaluationStrategy(abc.ABC):
1713class EvaluationStrategy(abc.ABC):
1714    def __init__(self, adapter: EngineAdapter):
1715        self.adapter = adapter
1716
1717    @abc.abstractmethod
1718    def insert(
1719        self,
1720        table_name: str,
1721        query_or_df: QueryOrDF,
1722        model: Model,
1723        is_first_insert: bool,
1724        render_kwargs: t.Dict[str, t.Any],
1725        **kwargs: t.Any,
1726    ) -> None:
1727        """Inserts the given query or a DataFrame into the target table or a view.
1728
1729        Args:
1730            table_name: The name of the target table or view.
1731            query_or_df: A query or a DataFrame to insert.
1732            model: The target model.
1733            is_first_insert: Whether this is the first insert for this version of a model. This value is set to True
1734                if no data has been previously inserted into the target table, or when the entire history of the target model has
1735                been restated. Note that in the latter case, the table might contain data from previous executions, and it is the
1736                responsibility of a specific evaluation strategy to handle the truncation of the table if necessary.
1737            render_kwargs: Additional key-value arguments to pass when rendering the model's query.
1738        """
1739
1740    @abc.abstractmethod
1741    def append(
1742        self,
1743        table_name: str,
1744        query_or_df: QueryOrDF,
1745        model: Model,
1746        render_kwargs: t.Dict[str, t.Any],
1747        **kwargs: t.Any,
1748    ) -> None:
1749        """Appends the given query or a DataFrame to the existing table.
1750
1751        Args:
1752            table_name: The target table name.
1753            query_or_df: A query or a DataFrame to insert.
1754            model: The target model.
1755            render_kwargs: Additional key-value arguments to pass when rendering the model's query.
1756        """
1757
1758    @abc.abstractmethod
1759    def create(
1760        self,
1761        table_name: str,
1762        model: Model,
1763        is_table_deployable: bool,
1764        render_kwargs: t.Dict[str, t.Any],
1765        skip_grants: bool,
1766        **kwargs: t.Any,
1767    ) -> None:
1768        """Creates the target table or view.
1769
1770        Note that the intention here is to just create the table structure, data is loaded in insert() and append()
1771
1772        Args:
1773            table_name: The name of a table or a view.
1774            model: The target model.
1775            is_table_deployable: True if this creation request is for the "main" table that *might* be deployed to a production environment.
1776                False if this creation request is for the "dev preview" table. Note that this flag is not related to the DeployabilityIndex
1777                which determines if the snapshot is deployable to production or not
1778            render_kwargs: Additional key-value arguments to pass when rendering the model's query.
1779        """
1780
1781    @abc.abstractmethod
1782    def migrate(
1783        self,
1784        target_table_name: str,
1785        source_table_name: str,
1786        snapshot: Snapshot,
1787        *,
1788        ignore_destructive: bool,
1789        ignore_additive: bool,
1790        **kwargs: t.Any,
1791    ) -> None:
1792        """Migrates the target table schema so that it corresponds to the source table schema.
1793
1794        Args:
1795            target_table_name: The target table name.
1796            source_table_name: The source table name.
1797            snapshot: The target snapshot.
1798            ignore_destructive: If True, destructive changes are not created when migrating.
1799                This is used for forward-only models that are being migrated to a new version.
1800            ignore_additive: If True, additive changes are not created when migrating.
1801                This is used for forward-only models that are being migrated to a new version.
1802        """
1803
1804    @abc.abstractmethod
1805    def delete(self, name: str, **kwargs: t.Any) -> None:
1806        """Deletes a target table or a view.
1807
1808        Args:
1809            name: The name of a table or a view.
1810        """
1811
1812    @abc.abstractmethod
1813    def promote(
1814        self,
1815        table_name: str,
1816        view_name: str,
1817        model: Model,
1818        environment: str,
1819        **kwargs: t.Any,
1820    ) -> None:
1821        """Updates the target view to point to the target table.
1822
1823        Args:
1824            table_name: The name of a table in the physical layer that is being promoted.
1825            view_name: The name of the target view in the virtual layer.
1826            model: The model that is being promoted.
1827            environment: The name of the target environment.
1828        """
1829
1830    @abc.abstractmethod
1831    def demote(self, view_name: str, **kwargs: t.Any) -> None:
1832        """Deletes the target view in the virtual layer.
1833
1834        Args:
1835            view_name: The name of the target view in the virtual layer.
1836        """
1837
1838    @abc.abstractmethod
1839    def run_pre_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
1840        """Executes the snapshot's pre statements.
1841
1842        Args:
1843            snapshot: The target snapshot.
1844            render_kwargs: Additional key-value arguments to pass when rendering the statements.
1845        """
1846
1847    @abc.abstractmethod
1848    def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
1849        """Executes the snapshot's post statements.
1850
1851        Args:
1852            snapshot: The target snapshot.
1853            render_kwargs: Additional key-value arguments to pass when rendering the statements.
1854        """
1855
1856    def _apply_grants(
1857        self,
1858        model: Model,
1859        table_name: str,
1860        target_layer: GrantsTargetLayer,
1861        is_snapshot_deployable: bool = False,
1862    ) -> None:
1863        """Apply grants for a model if grants are configured.
1864
1865        This method provides consistent grants application across all evaluation strategies.
1866        It ensures that whenever a physical database object (table, view, materialized view)
1867        is created or modified, the appropriate grants are applied.
1868
1869        Args:
1870            model: The SQLMesh model containing grants configuration
1871            table_name: The target table/view name to apply grants to
1872            target_layer: The grants application layer (physical or virtual)
1873            is_snapshot_deployable: Whether the snapshot is deployable (targeting production)
1874        """
1875        grants_config = model.grants
1876        if grants_config is None:
1877            return
1878
1879        if not self.adapter.SUPPORTS_GRANTS:
1880            logger.warning(
1881                f"Engine {self.adapter.__class__.__name__} does not support grants. "
1882                f"Skipping grants application for model {model.name}"
1883            )
1884            return
1885
1886        model_grants_target_layer = model.grants_target_layer
1887        deployable_vde_dev_only = (
1888            is_snapshot_deployable and model.virtual_environment_mode.is_dev_only
1889        )
1890
1891        # table_type is always a VIEW in the virtual layer unless model is deployable and VDE is dev_only
1892        # in which case we fall back to the model's model_grants_table_type
1893        if target_layer == GrantsTargetLayer.VIRTUAL and not deployable_vde_dev_only:
1894            model_grants_table_type = DataObjectType.VIEW
1895        else:
1896            model_grants_table_type = model.grants_table_type
1897
1898        if (
1899            model_grants_target_layer.is_all
1900            or model_grants_target_layer == target_layer
1901            # Always apply grants in production when VDE is dev_only regardless of target_layer
1902            # since only physical tables are created in production
1903            or deployable_vde_dev_only
1904        ):
1905            logger.info(f"Applying grants for model {model.name} to table {table_name}")
1906            self.adapter.sync_grants_config(
1907                exp.to_table(table_name, dialect=self.adapter.dialect),
1908                grants_config,
1909                model_grants_table_type,
1910            )
1911        else:
1912            logger.debug(
1913                f"Skipping grants application for model {model.name} in {target_layer} layer"
1914            )

Helper class that provides a standard way to create an ABC using inheritance.

adapter
@abc.abstractmethod
def insert( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_first_insert: bool, render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
1717    @abc.abstractmethod
1718    def insert(
1719        self,
1720        table_name: str,
1721        query_or_df: QueryOrDF,
1722        model: Model,
1723        is_first_insert: bool,
1724        render_kwargs: t.Dict[str, t.Any],
1725        **kwargs: t.Any,
1726    ) -> None:
1727        """Inserts the given query or a DataFrame into the target table or a view.
1728
1729        Args:
1730            table_name: The name of the target table or view.
1731            query_or_df: A query or a DataFrame to insert.
1732            model: The target model.
1733            is_first_insert: Whether this is the first insert for this version of a model. This value is set to True
1734                if no data has been previously inserted into the target table, or when the entire history of the target model has
1735                been restated. Note that in the latter case, the table might contain data from previous executions, and it is the
1736                responsibility of a specific evaluation strategy to handle the truncation of the table if necessary.
1737            render_kwargs: Additional key-value arguments to pass when rendering the model's query.
1738        """

Inserts the given query or a DataFrame into the target table or a view.

Arguments:
  • table_name: The name of the target table or view.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • is_first_insert: Whether this is the first insert for this version of a model. This value is set to True if no data has been previously inserted into the target table, or when the entire history of the target model has been restated. Note that in the latter case, the table might contain data from previous executions, and it is the responsibility of a specific evaluation strategy to handle the truncation of the table if necessary.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
@abc.abstractmethod
def append( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
1740    @abc.abstractmethod
1741    def append(
1742        self,
1743        table_name: str,
1744        query_or_df: QueryOrDF,
1745        model: Model,
1746        render_kwargs: t.Dict[str, t.Any],
1747        **kwargs: t.Any,
1748    ) -> None:
1749        """Appends the given query or a DataFrame to the existing table.
1750
1751        Args:
1752            table_name: The target table name.
1753            query_or_df: A query or a DataFrame to insert.
1754            model: The target model.
1755            render_kwargs: Additional key-value arguments to pass when rendering the model's query.
1756        """

Appends the given query or a DataFrame to the existing table.

Arguments:
  • table_name: The target table name.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
@abc.abstractmethod
def create( self, table_name: str, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_table_deployable: bool, render_kwargs: Dict[str, Any], skip_grants: bool, **kwargs: Any) -> None:
1758    @abc.abstractmethod
1759    def create(
1760        self,
1761        table_name: str,
1762        model: Model,
1763        is_table_deployable: bool,
1764        render_kwargs: t.Dict[str, t.Any],
1765        skip_grants: bool,
1766        **kwargs: t.Any,
1767    ) -> None:
1768        """Creates the target table or view.
1769
1770        Note that the intention here is to just create the table structure, data is loaded in insert() and append()
1771
1772        Args:
1773            table_name: The name of a table or a view.
1774            model: The target model.
1775            is_table_deployable: True if this creation request is for the "main" table that *might* be deployed to a production environment.
1776                False if this creation request is for the "dev preview" table. Note that this flag is not related to the DeployabilityIndex
1777                which determines if the snapshot is deployable to production or not
1778            render_kwargs: Additional key-value arguments to pass when rendering the model's query.
1779        """

Creates the target table or view.

Note that the intention here is to just create the table structure, data is loaded in insert() and append()

Arguments:
  • table_name: The name of a table or a view.
  • model: The target model.
  • is_table_deployable: True if this creation request is for the "main" table that might be deployed to a production environment. False if this creation request is for the "dev preview" table. Note that this flag is not related to the DeployabilityIndex which determines if the snapshot is deployable to production or not
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
@abc.abstractmethod
def migrate( self, target_table_name: str, source_table_name: str, snapshot: sqlmesh.core.snapshot.definition.Snapshot, *, ignore_destructive: bool, ignore_additive: bool, **kwargs: Any) -> None:
1781    @abc.abstractmethod
1782    def migrate(
1783        self,
1784        target_table_name: str,
1785        source_table_name: str,
1786        snapshot: Snapshot,
1787        *,
1788        ignore_destructive: bool,
1789        ignore_additive: bool,
1790        **kwargs: t.Any,
1791    ) -> None:
1792        """Migrates the target table schema so that it corresponds to the source table schema.
1793
1794        Args:
1795            target_table_name: The target table name.
1796            source_table_name: The source table name.
1797            snapshot: The target snapshot.
1798            ignore_destructive: If True, destructive changes are not created when migrating.
1799                This is used for forward-only models that are being migrated to a new version.
1800            ignore_additive: If True, additive changes are not created when migrating.
1801                This is used for forward-only models that are being migrated to a new version.
1802        """

Migrates the target table schema so that it corresponds to the source table schema.

Arguments:
  • target_table_name: The target table name.
  • source_table_name: The source table name.
  • snapshot: The target snapshot.
  • ignore_destructive: If True, destructive changes are not created when migrating. This is used for forward-only models that are being migrated to a new version.
  • ignore_additive: If True, additive changes are not created when migrating. This is used for forward-only models that are being migrated to a new version.
@abc.abstractmethod
def delete(self, name: str, **kwargs: Any) -> None:
1804    @abc.abstractmethod
1805    def delete(self, name: str, **kwargs: t.Any) -> None:
1806        """Deletes a target table or a view.
1807
1808        Args:
1809            name: The name of a table or a view.
1810        """

Deletes a target table or a view.

Arguments:
  • name: The name of a table or a view.
@abc.abstractmethod
def promote( self, table_name: str, view_name: str, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], environment: str, **kwargs: Any) -> None:
1812    @abc.abstractmethod
1813    def promote(
1814        self,
1815        table_name: str,
1816        view_name: str,
1817        model: Model,
1818        environment: str,
1819        **kwargs: t.Any,
1820    ) -> None:
1821        """Updates the target view to point to the target table.
1822
1823        Args:
1824            table_name: The name of a table in the physical layer that is being promoted.
1825            view_name: The name of the target view in the virtual layer.
1826            model: The model that is being promoted.
1827            environment: The name of the target environment.
1828        """

Updates the target view to point to the target table.

Arguments:
  • table_name: The name of a table in the physical layer that is being promoted.
  • view_name: The name of the target view in the virtual layer.
  • model: The model that is being promoted.
  • environment: The name of the target environment.
@abc.abstractmethod
def demote(self, view_name: str, **kwargs: Any) -> None:
1830    @abc.abstractmethod
1831    def demote(self, view_name: str, **kwargs: t.Any) -> None:
1832        """Deletes the target view in the virtual layer.
1833
1834        Args:
1835            view_name: The name of the target view in the virtual layer.
1836        """

Deletes the target view in the virtual layer.

Arguments:
  • view_name: The name of the target view in the virtual layer.
@abc.abstractmethod
def run_pre_statements( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, render_kwargs: Any) -> None:
1838    @abc.abstractmethod
1839    def run_pre_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
1840        """Executes the snapshot's pre statements.
1841
1842        Args:
1843            snapshot: The target snapshot.
1844            render_kwargs: Additional key-value arguments to pass when rendering the statements.
1845        """

Executes the snapshot's pre statements.

Arguments:
  • snapshot: The target snapshot.
  • render_kwargs: Additional key-value arguments to pass when rendering the statements.
@abc.abstractmethod
def run_post_statements( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, render_kwargs: Any) -> None:
1847    @abc.abstractmethod
1848    def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
1849        """Executes the snapshot's post statements.
1850
1851        Args:
1852            snapshot: The target snapshot.
1853            render_kwargs: Additional key-value arguments to pass when rendering the statements.
1854        """

Executes the snapshot's post statements.

Arguments:
  • snapshot: The target snapshot.
  • render_kwargs: Additional key-value arguments to pass when rendering the statements.
class SymbolicStrategy(EvaluationStrategy):
1917class SymbolicStrategy(EvaluationStrategy):
1918    def insert(
1919        self,
1920        table_name: str,
1921        query_or_df: QueryOrDF,
1922        model: Model,
1923        is_first_insert: bool,
1924        render_kwargs: t.Dict[str, t.Any],
1925        **kwargs: t.Any,
1926    ) -> None:
1927        pass
1928
1929    def append(
1930        self,
1931        table_name: str,
1932        query_or_df: QueryOrDF,
1933        model: Model,
1934        render_kwargs: t.Dict[str, t.Any],
1935        **kwargs: t.Any,
1936    ) -> None:
1937        pass
1938
1939    def create(
1940        self,
1941        table_name: str,
1942        model: Model,
1943        is_table_deployable: bool,
1944        render_kwargs: t.Dict[str, t.Any],
1945        skip_grants: bool,
1946        **kwargs: t.Any,
1947    ) -> None:
1948        pass
1949
1950    def migrate(
1951        self,
1952        target_table_name: str,
1953        source_table_name: str,
1954        snapshot: Snapshot,
1955        *,
1956        ignore_destructive: bool,
1957        ignore_additive: bool,
1958        **kwarg: t.Any,
1959    ) -> None:
1960        pass
1961
1962    def delete(self, name: str, **kwargs: t.Any) -> None:
1963        pass
1964
1965    def promote(
1966        self,
1967        table_name: str,
1968        view_name: str,
1969        model: Model,
1970        environment: str,
1971        **kwargs: t.Any,
1972    ) -> None:
1973        pass
1974
1975    def demote(self, view_name: str, **kwargs: t.Any) -> None:
1976        pass
1977
1978    def run_pre_statements(self, snapshot: Snapshot, render_kwargs: t.Dict[str, t.Any]) -> None:
1979        pass
1980
1981    def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Dict[str, t.Any]) -> None:
1982        pass

Helper class that provides a standard way to create an ABC using inheritance.

def insert( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_first_insert: bool, render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
1918    def insert(
1919        self,
1920        table_name: str,
1921        query_or_df: QueryOrDF,
1922        model: Model,
1923        is_first_insert: bool,
1924        render_kwargs: t.Dict[str, t.Any],
1925        **kwargs: t.Any,
1926    ) -> None:
1927        pass

Inserts the given query or a DataFrame into the target table or a view.

Arguments:
  • table_name: The name of the target table or view.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • is_first_insert: Whether this is the first insert for this version of a model. This value is set to True if no data has been previously inserted into the target table, or when the entire history of the target model has been restated. Note that in the latter case, the table might contain data from previous executions, and it is the responsibility of a specific evaluation strategy to handle the truncation of the table if necessary.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
def append( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
1929    def append(
1930        self,
1931        table_name: str,
1932        query_or_df: QueryOrDF,
1933        model: Model,
1934        render_kwargs: t.Dict[str, t.Any],
1935        **kwargs: t.Any,
1936    ) -> None:
1937        pass

Appends the given query or a DataFrame to the existing table.

Arguments:
  • table_name: The target table name.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
def create( self, table_name: str, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_table_deployable: bool, render_kwargs: Dict[str, Any], skip_grants: bool, **kwargs: Any) -> None:
1939    def create(
1940        self,
1941        table_name: str,
1942        model: Model,
1943        is_table_deployable: bool,
1944        render_kwargs: t.Dict[str, t.Any],
1945        skip_grants: bool,
1946        **kwargs: t.Any,
1947    ) -> None:
1948        pass

Creates the target table or view.

Note that the intention here is to just create the table structure, data is loaded in insert() and append()

Arguments:
  • table_name: The name of a table or a view.
  • model: The target model.
  • is_table_deployable: True if this creation request is for the "main" table that might be deployed to a production environment. False if this creation request is for the "dev preview" table. Note that this flag is not related to the DeployabilityIndex which determines if the snapshot is deployable to production or not
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
def migrate( self, target_table_name: str, source_table_name: str, snapshot: sqlmesh.core.snapshot.definition.Snapshot, *, ignore_destructive: bool, ignore_additive: bool, **kwarg: Any) -> None:
1950    def migrate(
1951        self,
1952        target_table_name: str,
1953        source_table_name: str,
1954        snapshot: Snapshot,
1955        *,
1956        ignore_destructive: bool,
1957        ignore_additive: bool,
1958        **kwarg: t.Any,
1959    ) -> None:
1960        pass

Migrates the target table schema so that it corresponds to the source table schema.

Arguments:
  • target_table_name: The target table name.
  • source_table_name: The source table name.
  • snapshot: The target snapshot.
  • ignore_destructive: If True, destructive changes are not created when migrating. This is used for forward-only models that are being migrated to a new version.
  • ignore_additive: If True, additive changes are not created when migrating. This is used for forward-only models that are being migrated to a new version.
def delete(self, name: str, **kwargs: Any) -> None:
1962    def delete(self, name: str, **kwargs: t.Any) -> None:
1963        pass

Deletes a target table or a view.

Arguments:
  • name: The name of a table or a view.
def promote( self, table_name: str, view_name: str, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], environment: str, **kwargs: Any) -> None:
1965    def promote(
1966        self,
1967        table_name: str,
1968        view_name: str,
1969        model: Model,
1970        environment: str,
1971        **kwargs: t.Any,
1972    ) -> None:
1973        pass

Updates the target view to point to the target table.

Arguments:
  • table_name: The name of a table in the physical layer that is being promoted.
  • view_name: The name of the target view in the virtual layer.
  • model: The model that is being promoted.
  • environment: The name of the target environment.
def demote(self, view_name: str, **kwargs: Any) -> None:
1975    def demote(self, view_name: str, **kwargs: t.Any) -> None:
1976        pass

Deletes the target view in the virtual layer.

Arguments:
  • view_name: The name of the target view in the virtual layer.
def run_pre_statements( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, render_kwargs: Dict[str, Any]) -> None:
1978    def run_pre_statements(self, snapshot: Snapshot, render_kwargs: t.Dict[str, t.Any]) -> None:
1979        pass

Executes the snapshot's pre statements.

Arguments:
  • snapshot: The target snapshot.
  • render_kwargs: Additional key-value arguments to pass when rendering the statements.
def run_post_statements( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, render_kwargs: Dict[str, Any]) -> None:
1981    def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Dict[str, t.Any]) -> None:
1982        pass

Executes the snapshot's post statements.

Arguments:
  • snapshot: The target snapshot.
  • render_kwargs: Additional key-value arguments to pass when rendering the statements.
class EmbeddedStrategy(SymbolicStrategy):
1985class EmbeddedStrategy(SymbolicStrategy):
1986    def promote(
1987        self,
1988        table_name: str,
1989        view_name: str,
1990        model: Model,
1991        environment: str,
1992        **kwargs: t.Any,
1993    ) -> None:
1994        logger.info("Dropping view '%s' for non-materialized table", view_name)
1995        self.adapter.drop_view(view_name, cascade=False)

Helper class that provides a standard way to create an ABC using inheritance.

def promote( self, table_name: str, view_name: str, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], environment: str, **kwargs: Any) -> None:
1986    def promote(
1987        self,
1988        table_name: str,
1989        view_name: str,
1990        model: Model,
1991        environment: str,
1992        **kwargs: t.Any,
1993    ) -> None:
1994        logger.info("Dropping view '%s' for non-materialized table", view_name)
1995        self.adapter.drop_view(view_name, cascade=False)

Updates the target view to point to the target table.

Arguments:
  • table_name: The name of a table in the physical layer that is being promoted.
  • view_name: The name of the target view in the virtual layer.
  • model: The model that is being promoted.
  • environment: The name of the target environment.
class PromotableStrategy(EvaluationStrategy, abc.ABC):
1998class PromotableStrategy(EvaluationStrategy, abc.ABC):
1999    def promote(
2000        self,
2001        table_name: str,
2002        view_name: str,
2003        model: Model,
2004        environment: str,
2005        **kwargs: t.Any,
2006    ) -> None:
2007        is_prod = environment == c.PROD
2008        logger.info("Updating view '%s' to point at table '%s'", view_name, table_name)
2009        render_kwargs: t.Dict[str, t.Any] = dict(
2010            start=kwargs.get("start"),
2011            end=kwargs.get("end"),
2012            execution_time=kwargs.get("execution_time"),
2013            engine_adapter=kwargs.get("engine_adapter"),
2014            snapshots=kwargs.get("snapshots"),
2015            deployability_index=kwargs.get("deployability_index"),
2016            table_mapping=kwargs.get("table_mapping"),
2017            runtime_stage=kwargs.get("runtime_stage"),
2018        )
2019        self.adapter.create_view(
2020            view_name,
2021            exp.select("*").from_(table_name, dialect=self.adapter.dialect),
2022            table_description=model.description if is_prod else None,
2023            column_descriptions=model.column_descriptions if is_prod else None,
2024            view_properties=model.render_virtual_properties(**render_kwargs),
2025        )
2026
2027        snapshot = kwargs.get("snapshot")
2028        deployability_index = kwargs.get("deployability_index")
2029        is_snapshot_deployable = (
2030            deployability_index.is_deployable(snapshot)
2031            if snapshot and deployability_index
2032            else False
2033        )
2034
2035        # Apply grants to the virtual layer (view) after promotion
2036        self._apply_grants(model, view_name, GrantsTargetLayer.VIRTUAL, is_snapshot_deployable)
2037
2038    def demote(self, view_name: str, **kwargs: t.Any) -> None:
2039        logger.info("Dropping view '%s'", view_name)
2040        self.adapter.drop_view(view_name, cascade=False)
2041
2042    def run_pre_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
2043        self.adapter.execute(snapshot.model.render_pre_statements(**render_kwargs))
2044
2045    def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
2046        self.adapter.execute(snapshot.model.render_post_statements(**render_kwargs))

Helper class that provides a standard way to create an ABC using inheritance.

def promote( self, table_name: str, view_name: str, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], environment: str, **kwargs: Any) -> None:
1999    def promote(
2000        self,
2001        table_name: str,
2002        view_name: str,
2003        model: Model,
2004        environment: str,
2005        **kwargs: t.Any,
2006    ) -> None:
2007        is_prod = environment == c.PROD
2008        logger.info("Updating view '%s' to point at table '%s'", view_name, table_name)
2009        render_kwargs: t.Dict[str, t.Any] = dict(
2010            start=kwargs.get("start"),
2011            end=kwargs.get("end"),
2012            execution_time=kwargs.get("execution_time"),
2013            engine_adapter=kwargs.get("engine_adapter"),
2014            snapshots=kwargs.get("snapshots"),
2015            deployability_index=kwargs.get("deployability_index"),
2016            table_mapping=kwargs.get("table_mapping"),
2017            runtime_stage=kwargs.get("runtime_stage"),
2018        )
2019        self.adapter.create_view(
2020            view_name,
2021            exp.select("*").from_(table_name, dialect=self.adapter.dialect),
2022            table_description=model.description if is_prod else None,
2023            column_descriptions=model.column_descriptions if is_prod else None,
2024            view_properties=model.render_virtual_properties(**render_kwargs),
2025        )
2026
2027        snapshot = kwargs.get("snapshot")
2028        deployability_index = kwargs.get("deployability_index")
2029        is_snapshot_deployable = (
2030            deployability_index.is_deployable(snapshot)
2031            if snapshot and deployability_index
2032            else False
2033        )
2034
2035        # Apply grants to the virtual layer (view) after promotion
2036        self._apply_grants(model, view_name, GrantsTargetLayer.VIRTUAL, is_snapshot_deployable)

Updates the target view to point to the target table.

Arguments:
  • table_name: The name of a table in the physical layer that is being promoted.
  • view_name: The name of the target view in the virtual layer.
  • model: The model that is being promoted.
  • environment: The name of the target environment.
def demote(self, view_name: str, **kwargs: Any) -> None:
2038    def demote(self, view_name: str, **kwargs: t.Any) -> None:
2039        logger.info("Dropping view '%s'", view_name)
2040        self.adapter.drop_view(view_name, cascade=False)

Deletes the target view in the virtual layer.

Arguments:
  • view_name: The name of the target view in the virtual layer.
def run_pre_statements( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, render_kwargs: Any) -> None:
2042    def run_pre_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
2043        self.adapter.execute(snapshot.model.render_pre_statements(**render_kwargs))

Executes the snapshot's pre statements.

Arguments:
  • snapshot: The target snapshot.
  • render_kwargs: Additional key-value arguments to pass when rendering the statements.
def run_post_statements( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, render_kwargs: Any) -> None:
2045    def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
2046        self.adapter.execute(snapshot.model.render_post_statements(**render_kwargs))

Executes the snapshot's post statements.

Arguments:
  • snapshot: The target snapshot.
  • render_kwargs: Additional key-value arguments to pass when rendering the statements.
class MaterializableStrategy(PromotableStrategy, abc.ABC):
2081class MaterializableStrategy(PromotableStrategy, abc.ABC):
2082    def create(
2083        self,
2084        table_name: str,
2085        model: Model,
2086        is_table_deployable: bool,
2087        render_kwargs: t.Dict[str, t.Any],
2088        skip_grants: bool,
2089        **kwargs: t.Any,
2090    ) -> None:
2091        ctas_query = model.ctas_query(**render_kwargs)
2092        physical_properties = _adjust_physical_properties_for_engine(
2093            self.adapter, model, kwargs.get("physical_properties", model.physical_properties)
2094        )
2095
2096        logger.info("Creating table '%s'", table_name)
2097        if model.annotated:
2098            self.adapter.create_table(
2099                table_name,
2100                target_columns_to_types=model.columns_to_types_or_raise,
2101                table_format=model.table_format,
2102                storage_format=model.storage_format,
2103                partitioned_by=model.partitioned_by,
2104                partition_interval_unit=model.partition_interval_unit,
2105                clustered_by=model.clustered_by,
2106                table_properties=physical_properties,
2107                table_description=model.description if is_table_deployable else None,
2108                column_descriptions=model.column_descriptions if is_table_deployable else None,
2109            )
2110
2111            # If we create both temp and prod tables, we need to make sure that we dry run once.
2112            dry_run = kwargs.get("dry_run", True) or not is_table_deployable
2113
2114            # Only sql models have queries that can be tested.
2115            # We also need to make sure that we don't dry run on Redshift because its planner / optimizer sometimes
2116            # breaks on our CTAS queries due to us relying on the WHERE FALSE LIMIT 0 combo.
2117            if model.is_sql and dry_run and self.adapter.dialect != "redshift":
2118                logger.info("Dry running model '%s'", model.name)
2119                self.adapter.fetchall(ctas_query)
2120        else:
2121            self.adapter.ctas(
2122                table_name,
2123                ctas_query,
2124                model.columns_to_types,
2125                table_format=model.table_format,
2126                storage_format=model.storage_format,
2127                partitioned_by=model.partitioned_by,
2128                partition_interval_unit=model.partition_interval_unit,
2129                clustered_by=model.clustered_by,
2130                table_properties=physical_properties,
2131                table_description=model.description if is_table_deployable else None,
2132                column_descriptions=model.column_descriptions if is_table_deployable else None,
2133            )
2134
2135        # Apply grants after table creation (unless explicitly skipped by caller)
2136        if not skip_grants:
2137            is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2138            self._apply_grants(
2139                model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2140            )
2141
2142    def migrate(
2143        self,
2144        target_table_name: str,
2145        source_table_name: str,
2146        snapshot: Snapshot,
2147        *,
2148        ignore_destructive: bool,
2149        ignore_additive: bool,
2150        **kwargs: t.Any,
2151    ) -> None:
2152        logger.info(f"Altering table '{target_table_name}'")
2153        alter_operations = self.adapter.get_alter_operations(
2154            target_table_name,
2155            source_table_name,
2156            ignore_destructive=ignore_destructive,
2157            ignore_additive=ignore_additive,
2158        )
2159        _check_destructive_schema_change(
2160            snapshot, alter_operations, kwargs["allow_destructive_snapshots"]
2161        )
2162        _check_additive_schema_change(
2163            snapshot, alter_operations, kwargs["allow_additive_snapshots"]
2164        )
2165        self.adapter.alter_table(alter_operations)
2166
2167        # Apply grants after schema migration
2168        deployability_index = kwargs.get("deployability_index")
2169        is_snapshot_deployable = (
2170            deployability_index.is_deployable(snapshot) if deployability_index else False
2171        )
2172        self._apply_grants(
2173            snapshot.model, target_table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2174        )
2175
2176    def delete(self, name: str, **kwargs: t.Any) -> None:
2177        _check_table_db_is_physical_schema(name, kwargs["physical_schema"])
2178        self.adapter.drop_table(name, cascade=kwargs.pop("cascade", False))
2179        logger.info("Dropped table '%s'", name)
2180
2181    def _replace_query_for_model(
2182        self,
2183        model: Model,
2184        name: str,
2185        query_or_df: QueryOrDF,
2186        render_kwargs: t.Dict[str, t.Any],
2187        skip_grants: bool = False,
2188        **kwargs: t.Any,
2189    ) -> None:
2190        """Replaces the table for the given model.
2191
2192        Args:
2193            model: The target model.
2194            name: The name of the target table.
2195            query_or_df: The query or DataFrame to replace the target table with.
2196        """
2197        if (model.is_seed or model.kind.is_full) and model.annotated:
2198            columns_to_types = model.columns_to_types_or_raise
2199            source_columns: t.Optional[t.List[str]] = list(columns_to_types)
2200        else:
2201            try:
2202                # Source columns from the underlying table to prevent unintentional table schema changes during restatement of incremental models.
2203                columns_to_types, source_columns = self._get_target_and_source_columns(
2204                    model, name, render_kwargs, force_get_columns_from_target=True
2205                )
2206            except Exception:
2207                columns_to_types, source_columns = None, None
2208
2209        physical_properties = _adjust_physical_properties_for_engine(
2210            self.adapter, model, kwargs.get("physical_properties", model.physical_properties)
2211        )
2212        self.adapter.replace_query(
2213            name,
2214            query_or_df,
2215            table_format=model.table_format,
2216            storage_format=model.storage_format,
2217            partitioned_by=model.partitioned_by,
2218            partition_interval_unit=model.partition_interval_unit,
2219            clustered_by=model.clustered_by,
2220            table_properties=physical_properties,
2221            table_description=model.description,
2222            column_descriptions=model.column_descriptions,
2223            target_columns_to_types=columns_to_types,
2224            source_columns=source_columns,
2225        )
2226
2227        # Apply grants after table replacement (unless explicitly skipped by caller)
2228        if not skip_grants:
2229            is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2230            self._apply_grants(model, name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable)
2231
2232    def _get_target_and_source_columns(
2233        self,
2234        model: Model,
2235        table_name: str,
2236        render_kwargs: t.Dict[str, t.Any],
2237        force_get_columns_from_target: bool = False,
2238    ) -> t.Tuple[t.Dict[str, exp.DataType], t.Optional[t.List[str]]]:
2239        if force_get_columns_from_target:
2240            target_column_to_types = self.adapter.columns(table_name)
2241        else:
2242            target_column_to_types = (
2243                model.columns_to_types  # type: ignore
2244                if model.annotated
2245                and not model.on_destructive_change.is_ignore
2246                and not model.on_additive_change.is_ignore
2247                else self.adapter.columns(table_name)
2248            )
2249        assert target_column_to_types is not None
2250        if model.on_destructive_change.is_ignore or model.on_additive_change.is_ignore:
2251            # We need to identify the columns that are only in the source so we create an empty table with
2252            # the user query to determine that
2253            temp_table_name = exp.table_(
2254                "diff",
2255                db=model.physical_schema,
2256            )
2257            with self.adapter.temp_table(
2258                model.ctas_query(**render_kwargs), name=temp_table_name
2259            ) as temp_table:
2260                source_columns = list(self.adapter.columns(temp_table))
2261        else:
2262            source_columns = None
2263        return target_column_to_types, source_columns

Helper class that provides a standard way to create an ABC using inheritance.

def create( self, table_name: str, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_table_deployable: bool, render_kwargs: Dict[str, Any], skip_grants: bool, **kwargs: Any) -> None:
2082    def create(
2083        self,
2084        table_name: str,
2085        model: Model,
2086        is_table_deployable: bool,
2087        render_kwargs: t.Dict[str, t.Any],
2088        skip_grants: bool,
2089        **kwargs: t.Any,
2090    ) -> None:
2091        ctas_query = model.ctas_query(**render_kwargs)
2092        physical_properties = _adjust_physical_properties_for_engine(
2093            self.adapter, model, kwargs.get("physical_properties", model.physical_properties)
2094        )
2095
2096        logger.info("Creating table '%s'", table_name)
2097        if model.annotated:
2098            self.adapter.create_table(
2099                table_name,
2100                target_columns_to_types=model.columns_to_types_or_raise,
2101                table_format=model.table_format,
2102                storage_format=model.storage_format,
2103                partitioned_by=model.partitioned_by,
2104                partition_interval_unit=model.partition_interval_unit,
2105                clustered_by=model.clustered_by,
2106                table_properties=physical_properties,
2107                table_description=model.description if is_table_deployable else None,
2108                column_descriptions=model.column_descriptions if is_table_deployable else None,
2109            )
2110
2111            # If we create both temp and prod tables, we need to make sure that we dry run once.
2112            dry_run = kwargs.get("dry_run", True) or not is_table_deployable
2113
2114            # Only sql models have queries that can be tested.
2115            # We also need to make sure that we don't dry run on Redshift because its planner / optimizer sometimes
2116            # breaks on our CTAS queries due to us relying on the WHERE FALSE LIMIT 0 combo.
2117            if model.is_sql and dry_run and self.adapter.dialect != "redshift":
2118                logger.info("Dry running model '%s'", model.name)
2119                self.adapter.fetchall(ctas_query)
2120        else:
2121            self.adapter.ctas(
2122                table_name,
2123                ctas_query,
2124                model.columns_to_types,
2125                table_format=model.table_format,
2126                storage_format=model.storage_format,
2127                partitioned_by=model.partitioned_by,
2128                partition_interval_unit=model.partition_interval_unit,
2129                clustered_by=model.clustered_by,
2130                table_properties=physical_properties,
2131                table_description=model.description if is_table_deployable else None,
2132                column_descriptions=model.column_descriptions if is_table_deployable else None,
2133            )
2134
2135        # Apply grants after table creation (unless explicitly skipped by caller)
2136        if not skip_grants:
2137            is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2138            self._apply_grants(
2139                model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2140            )

Creates the target table or view.

Note that the intention here is to just create the table structure, data is loaded in insert() and append()

Arguments:
  • table_name: The name of a table or a view.
  • model: The target model.
  • is_table_deployable: True if this creation request is for the "main" table that might be deployed to a production environment. False if this creation request is for the "dev preview" table. Note that this flag is not related to the DeployabilityIndex which determines if the snapshot is deployable to production or not
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
def migrate( self, target_table_name: str, source_table_name: str, snapshot: sqlmesh.core.snapshot.definition.Snapshot, *, ignore_destructive: bool, ignore_additive: bool, **kwargs: Any) -> None:
2142    def migrate(
2143        self,
2144        target_table_name: str,
2145        source_table_name: str,
2146        snapshot: Snapshot,
2147        *,
2148        ignore_destructive: bool,
2149        ignore_additive: bool,
2150        **kwargs: t.Any,
2151    ) -> None:
2152        logger.info(f"Altering table '{target_table_name}'")
2153        alter_operations = self.adapter.get_alter_operations(
2154            target_table_name,
2155            source_table_name,
2156            ignore_destructive=ignore_destructive,
2157            ignore_additive=ignore_additive,
2158        )
2159        _check_destructive_schema_change(
2160            snapshot, alter_operations, kwargs["allow_destructive_snapshots"]
2161        )
2162        _check_additive_schema_change(
2163            snapshot, alter_operations, kwargs["allow_additive_snapshots"]
2164        )
2165        self.adapter.alter_table(alter_operations)
2166
2167        # Apply grants after schema migration
2168        deployability_index = kwargs.get("deployability_index")
2169        is_snapshot_deployable = (
2170            deployability_index.is_deployable(snapshot) if deployability_index else False
2171        )
2172        self._apply_grants(
2173            snapshot.model, target_table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2174        )

Migrates the target table schema so that it corresponds to the source table schema.

Arguments:
  • target_table_name: The target table name.
  • source_table_name: The source table name.
  • snapshot: The target snapshot.
  • ignore_destructive: If True, destructive changes are not created when migrating. This is used for forward-only models that are being migrated to a new version.
  • ignore_additive: If True, additive changes are not created when migrating. This is used for forward-only models that are being migrated to a new version.
def delete(self, name: str, **kwargs: Any) -> None:
2176    def delete(self, name: str, **kwargs: t.Any) -> None:
2177        _check_table_db_is_physical_schema(name, kwargs["physical_schema"])
2178        self.adapter.drop_table(name, cascade=kwargs.pop("cascade", False))
2179        logger.info("Dropped table '%s'", name)

Deletes a target table or a view.

Arguments:
  • name: The name of a table or a view.
class IncrementalStrategy(MaterializableStrategy, abc.ABC):
2266class IncrementalStrategy(MaterializableStrategy, abc.ABC):
2267    def append(
2268        self,
2269        table_name: str,
2270        query_or_df: QueryOrDF,
2271        model: Model,
2272        render_kwargs: t.Dict[str, t.Any],
2273        **kwargs: t.Any,
2274    ) -> None:
2275        columns_to_types, source_columns = self._get_target_and_source_columns(
2276            model, table_name, render_kwargs=render_kwargs
2277        )
2278        self.adapter.insert_append(
2279            table_name,
2280            query_or_df,
2281            target_columns_to_types=columns_to_types,
2282            source_columns=source_columns,
2283        )

Helper class that provides a standard way to create an ABC using inheritance.

def append( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
2267    def append(
2268        self,
2269        table_name: str,
2270        query_or_df: QueryOrDF,
2271        model: Model,
2272        render_kwargs: t.Dict[str, t.Any],
2273        **kwargs: t.Any,
2274    ) -> None:
2275        columns_to_types, source_columns = self._get_target_and_source_columns(
2276            model, table_name, render_kwargs=render_kwargs
2277        )
2278        self.adapter.insert_append(
2279            table_name,
2280            query_or_df,
2281            target_columns_to_types=columns_to_types,
2282            source_columns=source_columns,
2283        )

Appends the given query or a DataFrame to the existing table.

Arguments:
  • table_name: The target table name.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
class IncrementalByPartitionStrategy(IncrementalStrategy):
2286class IncrementalByPartitionStrategy(IncrementalStrategy):
2287    def insert(
2288        self,
2289        table_name: str,
2290        query_or_df: QueryOrDF,
2291        model: Model,
2292        is_first_insert: bool,
2293        render_kwargs: t.Dict[str, t.Any],
2294        **kwargs: t.Any,
2295    ) -> None:
2296        if is_first_insert:
2297            self._replace_query_for_model(model, table_name, query_or_df, render_kwargs, **kwargs)
2298        else:
2299            columns_to_types, source_columns = self._get_target_and_source_columns(
2300                model, table_name, render_kwargs=render_kwargs
2301            )
2302            self.adapter.insert_overwrite_by_partition(
2303                table_name,
2304                query_or_df,
2305                partitioned_by=model.partitioned_by,
2306                target_columns_to_types=columns_to_types,
2307                source_columns=source_columns,
2308            )

Helper class that provides a standard way to create an ABC using inheritance.

def insert( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_first_insert: bool, render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
2287    def insert(
2288        self,
2289        table_name: str,
2290        query_or_df: QueryOrDF,
2291        model: Model,
2292        is_first_insert: bool,
2293        render_kwargs: t.Dict[str, t.Any],
2294        **kwargs: t.Any,
2295    ) -> None:
2296        if is_first_insert:
2297            self._replace_query_for_model(model, table_name, query_or_df, render_kwargs, **kwargs)
2298        else:
2299            columns_to_types, source_columns = self._get_target_and_source_columns(
2300                model, table_name, render_kwargs=render_kwargs
2301            )
2302            self.adapter.insert_overwrite_by_partition(
2303                table_name,
2304                query_or_df,
2305                partitioned_by=model.partitioned_by,
2306                target_columns_to_types=columns_to_types,
2307                source_columns=source_columns,
2308            )

Inserts the given query or a DataFrame into the target table or a view.

Arguments:
  • table_name: The name of the target table or view.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • is_first_insert: Whether this is the first insert for this version of a model. This value is set to True if no data has been previously inserted into the target table, or when the entire history of the target model has been restated. Note that in the latter case, the table might contain data from previous executions, and it is the responsibility of a specific evaluation strategy to handle the truncation of the table if necessary.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
class IncrementalByTimeRangeStrategy(IncrementalStrategy):
2311class IncrementalByTimeRangeStrategy(IncrementalStrategy):
2312    def insert(
2313        self,
2314        table_name: str,
2315        query_or_df: QueryOrDF,
2316        model: Model,
2317        is_first_insert: bool,
2318        render_kwargs: t.Dict[str, t.Any],
2319        **kwargs: t.Any,
2320    ) -> None:
2321        assert model.time_column
2322        columns_to_types, source_columns = self._get_target_and_source_columns(
2323            model, table_name, render_kwargs=render_kwargs
2324        )
2325        self.adapter.insert_overwrite_by_time_partition(
2326            table_name,
2327            query_or_df,
2328            time_formatter=model.convert_to_time_column,
2329            time_column=model.time_column,
2330            target_columns_to_types=columns_to_types,
2331            source_columns=source_columns,
2332            **kwargs,
2333        )

Helper class that provides a standard way to create an ABC using inheritance.

def insert( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_first_insert: bool, render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
2312    def insert(
2313        self,
2314        table_name: str,
2315        query_or_df: QueryOrDF,
2316        model: Model,
2317        is_first_insert: bool,
2318        render_kwargs: t.Dict[str, t.Any],
2319        **kwargs: t.Any,
2320    ) -> None:
2321        assert model.time_column
2322        columns_to_types, source_columns = self._get_target_and_source_columns(
2323            model, table_name, render_kwargs=render_kwargs
2324        )
2325        self.adapter.insert_overwrite_by_time_partition(
2326            table_name,
2327            query_or_df,
2328            time_formatter=model.convert_to_time_column,
2329            time_column=model.time_column,
2330            target_columns_to_types=columns_to_types,
2331            source_columns=source_columns,
2332            **kwargs,
2333        )

Inserts the given query or a DataFrame into the target table or a view.

Arguments:
  • table_name: The name of the target table or view.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • is_first_insert: Whether this is the first insert for this version of a model. This value is set to True if no data has been previously inserted into the target table, or when the entire history of the target model has been restated. Note that in the latter case, the table might contain data from previous executions, and it is the responsibility of a specific evaluation strategy to handle the truncation of the table if necessary.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
class IncrementalByUniqueKeyStrategy(IncrementalStrategy):
2336class IncrementalByUniqueKeyStrategy(IncrementalStrategy):
2337    def insert(
2338        self,
2339        table_name: str,
2340        query_or_df: QueryOrDF,
2341        model: Model,
2342        is_first_insert: bool,
2343        render_kwargs: t.Dict[str, t.Any],
2344        **kwargs: t.Any,
2345    ) -> None:
2346        if is_first_insert:
2347            self._replace_query_for_model(model, table_name, query_or_df, render_kwargs, **kwargs)
2348        else:
2349            columns_to_types, source_columns = self._get_target_and_source_columns(
2350                model,
2351                table_name,
2352                render_kwargs=render_kwargs,
2353            )
2354            physical_properties = _adjust_physical_properties_for_engine(
2355                self.adapter, model, kwargs.get("physical_properties", model.physical_properties)
2356            )
2357            self.adapter.merge(
2358                table_name,
2359                query_or_df,
2360                target_columns_to_types=columns_to_types,
2361                unique_key=model.unique_key,
2362                when_matched=model.when_matched,
2363                merge_filter=model.render_merge_filter(
2364                    start=kwargs.get("start"),
2365                    end=kwargs.get("end"),
2366                    execution_time=kwargs.get("execution_time"),
2367                ),
2368                physical_properties=physical_properties,
2369                source_columns=source_columns,
2370            )
2371
2372    def append(
2373        self,
2374        table_name: str,
2375        query_or_df: QueryOrDF,
2376        model: Model,
2377        render_kwargs: t.Dict[str, t.Any],
2378        **kwargs: t.Any,
2379    ) -> None:
2380        columns_to_types, source_columns = self._get_target_and_source_columns(
2381            model, table_name, render_kwargs=render_kwargs
2382        )
2383        physical_properties = _adjust_physical_properties_for_engine(
2384            self.adapter, model, kwargs.get("physical_properties", model.physical_properties)
2385        )
2386        self.adapter.merge(
2387            table_name,
2388            query_or_df,
2389            target_columns_to_types=columns_to_types,
2390            unique_key=model.unique_key,
2391            when_matched=model.when_matched,
2392            merge_filter=model.render_merge_filter(
2393                start=kwargs.get("start"),
2394                end=kwargs.get("end"),
2395                execution_time=kwargs.get("execution_time"),
2396            ),
2397            physical_properties=physical_properties,
2398            source_columns=source_columns,
2399        )

Helper class that provides a standard way to create an ABC using inheritance.

def insert( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_first_insert: bool, render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
2337    def insert(
2338        self,
2339        table_name: str,
2340        query_or_df: QueryOrDF,
2341        model: Model,
2342        is_first_insert: bool,
2343        render_kwargs: t.Dict[str, t.Any],
2344        **kwargs: t.Any,
2345    ) -> None:
2346        if is_first_insert:
2347            self._replace_query_for_model(model, table_name, query_or_df, render_kwargs, **kwargs)
2348        else:
2349            columns_to_types, source_columns = self._get_target_and_source_columns(
2350                model,
2351                table_name,
2352                render_kwargs=render_kwargs,
2353            )
2354            physical_properties = _adjust_physical_properties_for_engine(
2355                self.adapter, model, kwargs.get("physical_properties", model.physical_properties)
2356            )
2357            self.adapter.merge(
2358                table_name,
2359                query_or_df,
2360                target_columns_to_types=columns_to_types,
2361                unique_key=model.unique_key,
2362                when_matched=model.when_matched,
2363                merge_filter=model.render_merge_filter(
2364                    start=kwargs.get("start"),
2365                    end=kwargs.get("end"),
2366                    execution_time=kwargs.get("execution_time"),
2367                ),
2368                physical_properties=physical_properties,
2369                source_columns=source_columns,
2370            )

Inserts the given query or a DataFrame into the target table or a view.

Arguments:
  • table_name: The name of the target table or view.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • is_first_insert: Whether this is the first insert for this version of a model. This value is set to True if no data has been previously inserted into the target table, or when the entire history of the target model has been restated. Note that in the latter case, the table might contain data from previous executions, and it is the responsibility of a specific evaluation strategy to handle the truncation of the table if necessary.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
def append( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
2372    def append(
2373        self,
2374        table_name: str,
2375        query_or_df: QueryOrDF,
2376        model: Model,
2377        render_kwargs: t.Dict[str, t.Any],
2378        **kwargs: t.Any,
2379    ) -> None:
2380        columns_to_types, source_columns = self._get_target_and_source_columns(
2381            model, table_name, render_kwargs=render_kwargs
2382        )
2383        physical_properties = _adjust_physical_properties_for_engine(
2384            self.adapter, model, kwargs.get("physical_properties", model.physical_properties)
2385        )
2386        self.adapter.merge(
2387            table_name,
2388            query_or_df,
2389            target_columns_to_types=columns_to_types,
2390            unique_key=model.unique_key,
2391            when_matched=model.when_matched,
2392            merge_filter=model.render_merge_filter(
2393                start=kwargs.get("start"),
2394                end=kwargs.get("end"),
2395                execution_time=kwargs.get("execution_time"),
2396            ),
2397            physical_properties=physical_properties,
2398            source_columns=source_columns,
2399        )

Appends the given query or a DataFrame to the existing table.

Arguments:
  • table_name: The target table name.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
class IncrementalUnmanagedStrategy(IncrementalStrategy):
2402class IncrementalUnmanagedStrategy(IncrementalStrategy):
2403    def append(
2404        self,
2405        table_name: str,
2406        query_or_df: QueryOrDF,
2407        model: Model,
2408        render_kwargs: t.Dict[str, t.Any],
2409        **kwargs: t.Any,
2410    ) -> None:
2411        columns_to_types, source_columns = self._get_target_and_source_columns(
2412            model, table_name, render_kwargs=render_kwargs
2413        )
2414        self.adapter.insert_append(
2415            table_name,
2416            query_or_df,
2417            target_columns_to_types=columns_to_types,
2418            source_columns=source_columns,
2419        )
2420
2421    def insert(
2422        self,
2423        table_name: str,
2424        query_or_df: QueryOrDF,
2425        model: Model,
2426        is_first_insert: bool,
2427        render_kwargs: t.Dict[str, t.Any],
2428        **kwargs: t.Any,
2429    ) -> None:
2430        if is_first_insert:
2431            return self._replace_query_for_model(
2432                model, table_name, query_or_df, render_kwargs, **kwargs
2433            )
2434        if isinstance(model.kind, IncrementalUnmanagedKind) and model.kind.insert_overwrite:
2435            columns_to_types, source_columns = self._get_target_and_source_columns(
2436                model,
2437                table_name,
2438                render_kwargs=render_kwargs,
2439            )
2440
2441            return self.adapter.insert_overwrite_by_partition(
2442                table_name,
2443                query_or_df,
2444                model.partitioned_by,
2445                target_columns_to_types=columns_to_types,
2446                source_columns=source_columns,
2447            )
2448        return self.append(
2449            table_name,
2450            query_or_df,
2451            model,
2452            render_kwargs=render_kwargs,
2453            **kwargs,
2454        )

Helper class that provides a standard way to create an ABC using inheritance.

def append( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
2403    def append(
2404        self,
2405        table_name: str,
2406        query_or_df: QueryOrDF,
2407        model: Model,
2408        render_kwargs: t.Dict[str, t.Any],
2409        **kwargs: t.Any,
2410    ) -> None:
2411        columns_to_types, source_columns = self._get_target_and_source_columns(
2412            model, table_name, render_kwargs=render_kwargs
2413        )
2414        self.adapter.insert_append(
2415            table_name,
2416            query_or_df,
2417            target_columns_to_types=columns_to_types,
2418            source_columns=source_columns,
2419        )

Appends the given query or a DataFrame to the existing table.

Arguments:
  • table_name: The target table name.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
def insert( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_first_insert: bool, render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
2421    def insert(
2422        self,
2423        table_name: str,
2424        query_or_df: QueryOrDF,
2425        model: Model,
2426        is_first_insert: bool,
2427        render_kwargs: t.Dict[str, t.Any],
2428        **kwargs: t.Any,
2429    ) -> None:
2430        if is_first_insert:
2431            return self._replace_query_for_model(
2432                model, table_name, query_or_df, render_kwargs, **kwargs
2433            )
2434        if isinstance(model.kind, IncrementalUnmanagedKind) and model.kind.insert_overwrite:
2435            columns_to_types, source_columns = self._get_target_and_source_columns(
2436                model,
2437                table_name,
2438                render_kwargs=render_kwargs,
2439            )
2440
2441            return self.adapter.insert_overwrite_by_partition(
2442                table_name,
2443                query_or_df,
2444                model.partitioned_by,
2445                target_columns_to_types=columns_to_types,
2446                source_columns=source_columns,
2447            )
2448        return self.append(
2449            table_name,
2450            query_or_df,
2451            model,
2452            render_kwargs=render_kwargs,
2453            **kwargs,
2454        )

Inserts the given query or a DataFrame into the target table or a view.

Arguments:
  • table_name: The name of the target table or view.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • is_first_insert: Whether this is the first insert for this version of a model. This value is set to True if no data has been previously inserted into the target table, or when the entire history of the target model has been restated. Note that in the latter case, the table might contain data from previous executions, and it is the responsibility of a specific evaluation strategy to handle the truncation of the table if necessary.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
class FullRefreshStrategy(MaterializableStrategy):
2457class FullRefreshStrategy(MaterializableStrategy):
2458    def append(
2459        self,
2460        table_name: str,
2461        query_or_df: QueryOrDF,
2462        model: Model,
2463        render_kwargs: t.Dict[str, t.Any],
2464        **kwargs: t.Any,
2465    ) -> None:
2466        self.adapter.insert_append(
2467            table_name,
2468            query_or_df,
2469            target_columns_to_types=model.columns_to_types,
2470        )
2471
2472    def insert(
2473        self,
2474        table_name: str,
2475        query_or_df: QueryOrDF,
2476        model: Model,
2477        is_first_insert: bool,
2478        render_kwargs: t.Dict[str, t.Any],
2479        **kwargs: t.Any,
2480    ) -> None:
2481        self._replace_query_for_model(model, table_name, query_or_df, render_kwargs, **kwargs)

Helper class that provides a standard way to create an ABC using inheritance.

def append( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
2458    def append(
2459        self,
2460        table_name: str,
2461        query_or_df: QueryOrDF,
2462        model: Model,
2463        render_kwargs: t.Dict[str, t.Any],
2464        **kwargs: t.Any,
2465    ) -> None:
2466        self.adapter.insert_append(
2467            table_name,
2468            query_or_df,
2469            target_columns_to_types=model.columns_to_types,
2470        )

Appends the given query or a DataFrame to the existing table.

Arguments:
  • table_name: The target table name.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
def insert( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_first_insert: bool, render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
2472    def insert(
2473        self,
2474        table_name: str,
2475        query_or_df: QueryOrDF,
2476        model: Model,
2477        is_first_insert: bool,
2478        render_kwargs: t.Dict[str, t.Any],
2479        **kwargs: t.Any,
2480    ) -> None:
2481        self._replace_query_for_model(model, table_name, query_or_df, render_kwargs, **kwargs)

Inserts the given query or a DataFrame into the target table or a view.

Arguments:
  • table_name: The name of the target table or view.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • is_first_insert: Whether this is the first insert for this version of a model. This value is set to True if no data has been previously inserted into the target table, or when the entire history of the target model has been restated. Note that in the latter case, the table might contain data from previous executions, and it is the responsibility of a specific evaluation strategy to handle the truncation of the table if necessary.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
class SeedStrategy(MaterializableStrategy):
2484class SeedStrategy(MaterializableStrategy):
2485    def create(
2486        self,
2487        table_name: str,
2488        model: Model,
2489        is_table_deployable: bool,
2490        render_kwargs: t.Dict[str, t.Any],
2491        skip_grants: bool,
2492        **kwargs: t.Any,
2493    ) -> None:
2494        model = t.cast(SeedModel, model)
2495        if not model.is_hydrated and self.adapter.table_exists(table_name):
2496            # This likely means that the table was created and populated previously, but the evaluation stage
2497            # failed before the interval could be added for this model.
2498            logger.warning(
2499                "Seed model '%s' is not hydrated, but the table '%s' exists. Skipping creation",
2500                model.name,
2501                table_name,
2502            )
2503            return
2504
2505        super().create(
2506            table_name,
2507            model,
2508            is_table_deployable,
2509            render_kwargs,
2510            skip_grants=True,  # Skip grants; they're applied after data insertion
2511            **kwargs,
2512        )
2513        # For seeds we insert data at the time of table creation.
2514        try:
2515            for index, df in enumerate(model.render_seed()):
2516                if index == 0:
2517                    self._replace_query_for_model(
2518                        model,
2519                        table_name,
2520                        df,
2521                        render_kwargs,
2522                        skip_grants=True,  # Skip grants; they're applied after data insertion
2523                        **kwargs,
2524                    )
2525                else:
2526                    self.adapter.insert_append(
2527                        table_name, df, target_columns_to_types=model.columns_to_types
2528                    )
2529
2530            if not skip_grants:
2531                # Apply grants after seed table creation and data insertion
2532                is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2533                self._apply_grants(
2534                    model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2535                )
2536        except Exception:
2537            self.adapter.drop_table(table_name)
2538            raise
2539
2540    def migrate(
2541        self,
2542        target_table_name: str,
2543        source_table_name: str,
2544        snapshot: Snapshot,
2545        *,
2546        ignore_destructive: bool,
2547        ignore_additive: bool,
2548        **kwargs: t.Any,
2549    ) -> None:
2550        raise NotImplementedError("Seeds do not support migrations.")
2551
2552    def insert(
2553        self,
2554        table_name: str,
2555        query_or_df: QueryOrDF,
2556        model: Model,
2557        is_first_insert: bool,
2558        render_kwargs: t.Dict[str, t.Any],
2559        **kwargs: t.Any,
2560    ) -> None:
2561        # Data has already been inserted at the time of table creation.
2562        pass
2563
2564    def append(
2565        self,
2566        table_name: str,
2567        query_or_df: QueryOrDF,
2568        model: Model,
2569        render_kwargs: t.Dict[str, t.Any],
2570        **kwargs: t.Any,
2571    ) -> None:
2572        # Data has already been inserted at the time of table creation.
2573        pass

Helper class that provides a standard way to create an ABC using inheritance.

def create( self, table_name: str, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_table_deployable: bool, render_kwargs: Dict[str, Any], skip_grants: bool, **kwargs: Any) -> None:
2485    def create(
2486        self,
2487        table_name: str,
2488        model: Model,
2489        is_table_deployable: bool,
2490        render_kwargs: t.Dict[str, t.Any],
2491        skip_grants: bool,
2492        **kwargs: t.Any,
2493    ) -> None:
2494        model = t.cast(SeedModel, model)
2495        if not model.is_hydrated and self.adapter.table_exists(table_name):
2496            # This likely means that the table was created and populated previously, but the evaluation stage
2497            # failed before the interval could be added for this model.
2498            logger.warning(
2499                "Seed model '%s' is not hydrated, but the table '%s' exists. Skipping creation",
2500                model.name,
2501                table_name,
2502            )
2503            return
2504
2505        super().create(
2506            table_name,
2507            model,
2508            is_table_deployable,
2509            render_kwargs,
2510            skip_grants=True,  # Skip grants; they're applied after data insertion
2511            **kwargs,
2512        )
2513        # For seeds we insert data at the time of table creation.
2514        try:
2515            for index, df in enumerate(model.render_seed()):
2516                if index == 0:
2517                    self._replace_query_for_model(
2518                        model,
2519                        table_name,
2520                        df,
2521                        render_kwargs,
2522                        skip_grants=True,  # Skip grants; they're applied after data insertion
2523                        **kwargs,
2524                    )
2525                else:
2526                    self.adapter.insert_append(
2527                        table_name, df, target_columns_to_types=model.columns_to_types
2528                    )
2529
2530            if not skip_grants:
2531                # Apply grants after seed table creation and data insertion
2532                is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2533                self._apply_grants(
2534                    model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2535                )
2536        except Exception:
2537            self.adapter.drop_table(table_name)
2538            raise

Creates the target table or view.

Note that the intention here is to just create the table structure, data is loaded in insert() and append()

Arguments:
  • table_name: The name of a table or a view.
  • model: The target model.
  • is_table_deployable: True if this creation request is for the "main" table that might be deployed to a production environment. False if this creation request is for the "dev preview" table. Note that this flag is not related to the DeployabilityIndex which determines if the snapshot is deployable to production or not
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
def migrate( self, target_table_name: str, source_table_name: str, snapshot: sqlmesh.core.snapshot.definition.Snapshot, *, ignore_destructive: bool, ignore_additive: bool, **kwargs: Any) -> None:
2540    def migrate(
2541        self,
2542        target_table_name: str,
2543        source_table_name: str,
2544        snapshot: Snapshot,
2545        *,
2546        ignore_destructive: bool,
2547        ignore_additive: bool,
2548        **kwargs: t.Any,
2549    ) -> None:
2550        raise NotImplementedError("Seeds do not support migrations.")

Migrates the target table schema so that it corresponds to the source table schema.

Arguments:
  • target_table_name: The target table name.
  • source_table_name: The source table name.
  • snapshot: The target snapshot.
  • ignore_destructive: If True, destructive changes are not created when migrating. This is used for forward-only models that are being migrated to a new version.
  • ignore_additive: If True, additive changes are not created when migrating. This is used for forward-only models that are being migrated to a new version.
def insert( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_first_insert: bool, render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
2552    def insert(
2553        self,
2554        table_name: str,
2555        query_or_df: QueryOrDF,
2556        model: Model,
2557        is_first_insert: bool,
2558        render_kwargs: t.Dict[str, t.Any],
2559        **kwargs: t.Any,
2560    ) -> None:
2561        # Data has already been inserted at the time of table creation.
2562        pass

Inserts the given query or a DataFrame into the target table or a view.

Arguments:
  • table_name: The name of the target table or view.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • is_first_insert: Whether this is the first insert for this version of a model. This value is set to True if no data has been previously inserted into the target table, or when the entire history of the target model has been restated. Note that in the latter case, the table might contain data from previous executions, and it is the responsibility of a specific evaluation strategy to handle the truncation of the table if necessary.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
def append( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
2564    def append(
2565        self,
2566        table_name: str,
2567        query_or_df: QueryOrDF,
2568        model: Model,
2569        render_kwargs: t.Dict[str, t.Any],
2570        **kwargs: t.Any,
2571    ) -> None:
2572        # Data has already been inserted at the time of table creation.
2573        pass

Appends the given query or a DataFrame to the existing table.

Arguments:
  • table_name: The target table name.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
class SCDType2Strategy(IncrementalStrategy):
2576class SCDType2Strategy(IncrementalStrategy):
2577    def create(
2578        self,
2579        table_name: str,
2580        model: Model,
2581        is_table_deployable: bool,
2582        render_kwargs: t.Dict[str, t.Any],
2583        skip_grants: bool,
2584        **kwargs: t.Any,
2585    ) -> None:
2586        assert isinstance(model.kind, (SCDType2ByTimeKind, SCDType2ByColumnKind))
2587        if model.annotated:
2588            logger.info("Creating table '%s'", table_name)
2589            columns_to_types = model.columns_to_types_or_raise
2590            if isinstance(model.kind, SCDType2ByTimeKind):
2591                columns_to_types[model.kind.updated_at_name.name] = model.kind.time_data_type
2592            physical_properties = _adjust_physical_properties_for_engine(
2593                self.adapter, model, kwargs.get("physical_properties", model.physical_properties)
2594            )
2595            self.adapter.create_table(
2596                table_name,
2597                target_columns_to_types=columns_to_types,
2598                table_format=model.table_format,
2599                storage_format=model.storage_format,
2600                partitioned_by=model.partitioned_by,
2601                partition_interval_unit=model.partition_interval_unit,
2602                clustered_by=model.clustered_by,
2603                table_properties=physical_properties,
2604                table_description=model.description if is_table_deployable else None,
2605                column_descriptions=model.column_descriptions if is_table_deployable else None,
2606            )
2607        else:
2608            # We assume that the data type for `updated_at_name` matches the data type that is defined for
2609            # `time_data_type`. If that isn't the case, then the user might get an error about not being able
2610            # to do comparisons across different data types
2611            super().create(
2612                table_name,
2613                model,
2614                is_table_deployable,
2615                render_kwargs,
2616                skip_grants,
2617                **kwargs,
2618            )
2619
2620        if not skip_grants:
2621            # Apply grants after SCD Type 2 table creation
2622            is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2623            self._apply_grants(
2624                model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2625            )
2626
2627    def insert(
2628        self,
2629        table_name: str,
2630        query_or_df: QueryOrDF,
2631        model: Model,
2632        is_first_insert: bool,
2633        render_kwargs: t.Dict[str, t.Any],
2634        **kwargs: t.Any,
2635    ) -> None:
2636        # Source columns from the underlying table to prevent unintentional table schema changes during restatement of incremental models.
2637        columns_to_types, source_columns = self._get_target_and_source_columns(
2638            model,
2639            table_name,
2640            render_kwargs=render_kwargs,
2641            force_get_columns_from_target=True,
2642        )
2643        if isinstance(model.kind, SCDType2ByTimeKind):
2644            self.adapter.scd_type_2_by_time(
2645                target_table=table_name,
2646                source_table=query_or_df,
2647                unique_key=model.unique_key,
2648                valid_from_col=model.kind.valid_from_name,
2649                valid_to_col=model.kind.valid_to_name,
2650                execution_time=kwargs["execution_time"],
2651                updated_at_col=model.kind.updated_at_name,
2652                invalidate_hard_deletes=model.kind.invalidate_hard_deletes,
2653                updated_at_as_valid_from=model.kind.updated_at_as_valid_from,
2654                target_columns_to_types=columns_to_types,
2655                table_format=model.table_format,
2656                table_description=model.description,
2657                column_descriptions=model.column_descriptions,
2658                truncate=is_first_insert,
2659                source_columns=source_columns,
2660                storage_format=model.storage_format,
2661                partitioned_by=model.partitioned_by,
2662                partition_interval_unit=model.partition_interval_unit,
2663                clustered_by=model.clustered_by,
2664                table_properties=kwargs.get("physical_properties", model.physical_properties),
2665            )
2666        elif isinstance(model.kind, SCDType2ByColumnKind):
2667            self.adapter.scd_type_2_by_column(
2668                target_table=table_name,
2669                source_table=query_or_df,
2670                unique_key=model.unique_key,
2671                valid_from_col=model.kind.valid_from_name,
2672                valid_to_col=model.kind.valid_to_name,
2673                execution_time=model.kind.updated_at_name or kwargs["execution_time"],
2674                check_columns=model.kind.columns,
2675                invalidate_hard_deletes=model.kind.invalidate_hard_deletes,
2676                execution_time_as_valid_from=model.kind.execution_time_as_valid_from,
2677                target_columns_to_types=columns_to_types,
2678                table_format=model.table_format,
2679                table_description=model.description,
2680                column_descriptions=model.column_descriptions,
2681                truncate=is_first_insert,
2682                source_columns=source_columns,
2683                storage_format=model.storage_format,
2684                partitioned_by=model.partitioned_by,
2685                partition_interval_unit=model.partition_interval_unit,
2686                clustered_by=model.clustered_by,
2687                table_properties=kwargs.get("physical_properties", model.physical_properties),
2688            )
2689        else:
2690            raise SQLMeshError(
2691                f"Unexpected SCD Type 2 kind: {model.kind}. This is not expected and please report this as a bug."
2692            )
2693
2694        # Apply grants after SCD Type 2 table recreation
2695        is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2696        self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable)
2697
2698    def append(
2699        self,
2700        table_name: str,
2701        query_or_df: QueryOrDF,
2702        model: Model,
2703        render_kwargs: t.Dict[str, t.Any],
2704        **kwargs: t.Any,
2705    ) -> None:
2706        return self.insert(
2707            table_name,
2708            query_or_df,
2709            model,
2710            is_first_insert=False,
2711            render_kwargs=render_kwargs,
2712            **kwargs,
2713        )

Helper class that provides a standard way to create an ABC using inheritance.

def create( self, table_name: str, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_table_deployable: bool, render_kwargs: Dict[str, Any], skip_grants: bool, **kwargs: Any) -> None:
2577    def create(
2578        self,
2579        table_name: str,
2580        model: Model,
2581        is_table_deployable: bool,
2582        render_kwargs: t.Dict[str, t.Any],
2583        skip_grants: bool,
2584        **kwargs: t.Any,
2585    ) -> None:
2586        assert isinstance(model.kind, (SCDType2ByTimeKind, SCDType2ByColumnKind))
2587        if model.annotated:
2588            logger.info("Creating table '%s'", table_name)
2589            columns_to_types = model.columns_to_types_or_raise
2590            if isinstance(model.kind, SCDType2ByTimeKind):
2591                columns_to_types[model.kind.updated_at_name.name] = model.kind.time_data_type
2592            physical_properties = _adjust_physical_properties_for_engine(
2593                self.adapter, model, kwargs.get("physical_properties", model.physical_properties)
2594            )
2595            self.adapter.create_table(
2596                table_name,
2597                target_columns_to_types=columns_to_types,
2598                table_format=model.table_format,
2599                storage_format=model.storage_format,
2600                partitioned_by=model.partitioned_by,
2601                partition_interval_unit=model.partition_interval_unit,
2602                clustered_by=model.clustered_by,
2603                table_properties=physical_properties,
2604                table_description=model.description if is_table_deployable else None,
2605                column_descriptions=model.column_descriptions if is_table_deployable else None,
2606            )
2607        else:
2608            # We assume that the data type for `updated_at_name` matches the data type that is defined for
2609            # `time_data_type`. If that isn't the case, then the user might get an error about not being able
2610            # to do comparisons across different data types
2611            super().create(
2612                table_name,
2613                model,
2614                is_table_deployable,
2615                render_kwargs,
2616                skip_grants,
2617                **kwargs,
2618            )
2619
2620        if not skip_grants:
2621            # Apply grants after SCD Type 2 table creation
2622            is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2623            self._apply_grants(
2624                model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2625            )

Creates the target table or view.

Note that the intention here is to just create the table structure, data is loaded in insert() and append()

Arguments:
  • table_name: The name of a table or a view.
  • model: The target model.
  • is_table_deployable: True if this creation request is for the "main" table that might be deployed to a production environment. False if this creation request is for the "dev preview" table. Note that this flag is not related to the DeployabilityIndex which determines if the snapshot is deployable to production or not
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
def insert( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_first_insert: bool, render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
2627    def insert(
2628        self,
2629        table_name: str,
2630        query_or_df: QueryOrDF,
2631        model: Model,
2632        is_first_insert: bool,
2633        render_kwargs: t.Dict[str, t.Any],
2634        **kwargs: t.Any,
2635    ) -> None:
2636        # Source columns from the underlying table to prevent unintentional table schema changes during restatement of incremental models.
2637        columns_to_types, source_columns = self._get_target_and_source_columns(
2638            model,
2639            table_name,
2640            render_kwargs=render_kwargs,
2641            force_get_columns_from_target=True,
2642        )
2643        if isinstance(model.kind, SCDType2ByTimeKind):
2644            self.adapter.scd_type_2_by_time(
2645                target_table=table_name,
2646                source_table=query_or_df,
2647                unique_key=model.unique_key,
2648                valid_from_col=model.kind.valid_from_name,
2649                valid_to_col=model.kind.valid_to_name,
2650                execution_time=kwargs["execution_time"],
2651                updated_at_col=model.kind.updated_at_name,
2652                invalidate_hard_deletes=model.kind.invalidate_hard_deletes,
2653                updated_at_as_valid_from=model.kind.updated_at_as_valid_from,
2654                target_columns_to_types=columns_to_types,
2655                table_format=model.table_format,
2656                table_description=model.description,
2657                column_descriptions=model.column_descriptions,
2658                truncate=is_first_insert,
2659                source_columns=source_columns,
2660                storage_format=model.storage_format,
2661                partitioned_by=model.partitioned_by,
2662                partition_interval_unit=model.partition_interval_unit,
2663                clustered_by=model.clustered_by,
2664                table_properties=kwargs.get("physical_properties", model.physical_properties),
2665            )
2666        elif isinstance(model.kind, SCDType2ByColumnKind):
2667            self.adapter.scd_type_2_by_column(
2668                target_table=table_name,
2669                source_table=query_or_df,
2670                unique_key=model.unique_key,
2671                valid_from_col=model.kind.valid_from_name,
2672                valid_to_col=model.kind.valid_to_name,
2673                execution_time=model.kind.updated_at_name or kwargs["execution_time"],
2674                check_columns=model.kind.columns,
2675                invalidate_hard_deletes=model.kind.invalidate_hard_deletes,
2676                execution_time_as_valid_from=model.kind.execution_time_as_valid_from,
2677                target_columns_to_types=columns_to_types,
2678                table_format=model.table_format,
2679                table_description=model.description,
2680                column_descriptions=model.column_descriptions,
2681                truncate=is_first_insert,
2682                source_columns=source_columns,
2683                storage_format=model.storage_format,
2684                partitioned_by=model.partitioned_by,
2685                partition_interval_unit=model.partition_interval_unit,
2686                clustered_by=model.clustered_by,
2687                table_properties=kwargs.get("physical_properties", model.physical_properties),
2688            )
2689        else:
2690            raise SQLMeshError(
2691                f"Unexpected SCD Type 2 kind: {model.kind}. This is not expected and please report this as a bug."
2692            )
2693
2694        # Apply grants after SCD Type 2 table recreation
2695        is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2696        self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable)

Inserts the given query or a DataFrame into the target table or a view.

Arguments:
  • table_name: The name of the target table or view.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • is_first_insert: Whether this is the first insert for this version of a model. This value is set to True if no data has been previously inserted into the target table, or when the entire history of the target model has been restated. Note that in the latter case, the table might contain data from previous executions, and it is the responsibility of a specific evaluation strategy to handle the truncation of the table if necessary.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
def append( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
2698    def append(
2699        self,
2700        table_name: str,
2701        query_or_df: QueryOrDF,
2702        model: Model,
2703        render_kwargs: t.Dict[str, t.Any],
2704        **kwargs: t.Any,
2705    ) -> None:
2706        return self.insert(
2707            table_name,
2708            query_or_df,
2709            model,
2710            is_first_insert=False,
2711            render_kwargs=render_kwargs,
2712            **kwargs,
2713        )

Appends the given query or a DataFrame to the existing table.

Arguments:
  • table_name: The target table name.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
class ViewStrategy(PromotableStrategy):
2716class ViewStrategy(PromotableStrategy):
2717    def insert(
2718        self,
2719        table_name: str,
2720        query_or_df: QueryOrDF,
2721        model: Model,
2722        is_first_insert: bool,
2723        render_kwargs: t.Dict[str, t.Any],
2724        **kwargs: t.Any,
2725    ) -> None:
2726        # We should recreate MVs across supported engines (Snowflake, BigQuery etc) because
2727        # if upstream tables were recreated (e.g FULL models), the MVs would be silently invalidated.
2728        # The only exception to that rule is RisingWave which doesn't support CREATE OR REPLACE, so upstream
2729        # models don't recreate their physical tables for the MVs to be invalidated.
2730        # However, even for RW we still want to recreate MVs to avoid stale references, as is the case with normal views.
2731        # The flag is_first_insert is used for that matter as a signal to recreate the MV if the snapshot's intervals
2732        # have been cleared by `should_force_rebuild`
2733        is_materialized_view = self._is_materialized_view(model)
2734        must_recreate_view = not self.adapter.HAS_VIEW_BINDING or (
2735            is_materialized_view and is_first_insert
2736        )
2737
2738        # Some engines (e.g. StarRocks) maintain materialized views automatically (via auto/scheduled
2739        # REFRESH) and can only recreate them via a destructive DROP + CREATE, which deletes the
2740        # materialized data and forces a full rebuild. For those, an existing MV must not be recreated
2741        # on routine evaluation (e.g. every `sqlmesh run`); only build it on the first insert (a new
2742        # version) or when a rebuild is explicitly forced (intervals cleared by `should_force_rebuild`,
2743        # which sets `is_first_insert`).
2744        if (
2745            is_materialized_view
2746            and not is_first_insert
2747            and not self.adapter.RECREATE_MATERIALIZED_VIEW_ON_EVALUATION
2748        ):
2749            must_recreate_view = False
2750
2751        if self.adapter.table_exists(table_name) and not must_recreate_view:
2752            logger.info("Skipping creation of the view '%s'", table_name)
2753            return
2754
2755        logger.info("Replacing view '%s'", table_name)
2756        materialized_properties = None
2757        if is_materialized_view:
2758            materialized_properties = {
2759                "partitioned_by": model.partitioned_by,
2760                "partition_interval_unit": model.partition_interval_unit,
2761                "clustered_by": model.clustered_by,
2762                "has_audits": bool(model.audits_with_args),
2763            }
2764        self.adapter.create_view(
2765            table_name,
2766            query_or_df,
2767            model.columns_to_types,
2768            replace=must_recreate_view,
2769            materialized=is_materialized_view,
2770            materialized_properties=materialized_properties,
2771            view_properties=kwargs.get("physical_properties", model.physical_properties),
2772            table_description=model.description,
2773            column_descriptions=model.column_descriptions,
2774        )
2775
2776        # Apply grants after view creation / replacement
2777        is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2778        self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable)
2779
2780    def append(
2781        self,
2782        table_name: str,
2783        query_or_df: QueryOrDF,
2784        model: Model,
2785        render_kwargs: t.Dict[str, t.Any],
2786        **kwargs: t.Any,
2787    ) -> None:
2788        raise ConfigError(f"Cannot append to a view '{table_name}'.")
2789
2790    def create(
2791        self,
2792        table_name: str,
2793        model: Model,
2794        is_table_deployable: bool,
2795        render_kwargs: t.Dict[str, t.Any],
2796        skip_grants: bool,
2797        **kwargs: t.Any,
2798    ) -> None:
2799        is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2800
2801        if self.adapter.table_exists(table_name):
2802            # Make sure we don't recreate the view to prevent deletion of downstream views in engines with no late
2803            # binding support (because of DROP CASCADE).
2804            logger.info("View '%s' already exists", table_name)
2805
2806            if not skip_grants:
2807                # Always apply grants when present, even if view exists, to handle grants updates
2808                self._apply_grants(
2809                    model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2810                )
2811            return
2812
2813        logger.info("Creating view '%s'", table_name)
2814        materialized = self._is_materialized_view(model)
2815        materialized_properties = None
2816        if materialized:
2817            materialized_properties = {
2818                "partitioned_by": model.partitioned_by,
2819                "clustered_by": model.clustered_by,
2820                "partition_interval_unit": model.partition_interval_unit,
2821                "has_audits": bool(model.audits_with_args),
2822            }
2823        self.adapter.create_view(
2824            table_name,
2825            model.render_query_or_raise(**render_kwargs),
2826            # Make sure we never replace the view during creation to avoid race conditions in engines with no late binding support.
2827            replace=False,
2828            materialized=self._is_materialized_view(model),
2829            materialized_properties=materialized_properties,
2830            view_properties=kwargs.get("physical_properties", model.physical_properties),
2831            table_description=model.description if is_table_deployable else None,
2832            column_descriptions=model.column_descriptions if is_table_deployable else None,
2833        )
2834
2835        if not skip_grants:
2836            # Apply grants after view creation
2837            self._apply_grants(
2838                model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2839            )
2840
2841    def migrate(
2842        self,
2843        target_table_name: str,
2844        source_table_name: str,
2845        snapshot: Snapshot,
2846        *,
2847        ignore_destructive: bool,
2848        ignore_additive: bool,
2849        **kwargs: t.Any,
2850    ) -> None:
2851        logger.info("Migrating view '%s'", target_table_name)
2852        model = snapshot.model
2853        render_kwargs = dict(
2854            execution_time=now(), snapshots=kwargs["snapshots"], engine_adapter=self.adapter
2855        )
2856
2857        is_materialized_view = self._is_materialized_view(model)
2858        materialized_properties = None
2859        if is_materialized_view:
2860            materialized_properties = {
2861                "partitioned_by": model.partitioned_by,
2862                "clustered_by": model.clustered_by,
2863                "partition_interval_unit": model.partition_interval_unit,
2864                "has_audits": bool(model.audits_with_args),
2865            }
2866
2867        self.adapter.create_view(
2868            target_table_name,
2869            model.render_query_or_raise(**render_kwargs),
2870            model.columns_to_types,
2871            materialized=is_materialized_view,
2872            materialized_properties=materialized_properties,
2873            view_properties=model.render_physical_properties(**render_kwargs),
2874            table_description=model.description,
2875            column_descriptions=model.column_descriptions,
2876        )
2877
2878        # Apply grants after view migration
2879        deployability_index = kwargs.get("deployability_index")
2880        is_snapshot_deployable = (
2881            deployability_index.is_deployable(snapshot) if deployability_index else False
2882        )
2883        self._apply_grants(
2884            snapshot.model, target_table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2885        )
2886
2887    def delete(self, name: str, **kwargs: t.Any) -> None:
2888        cascade = kwargs.pop("cascade", False)
2889        try:
2890            # Some engines (e.g., RisingWave) don’t fail when dropping a materialized view with a DROP VIEW statement,
2891            # because views and materialized views don’t share the same namespace. Therefore, we should not ignore if the
2892            # view doesn't exist and let the exception handler attempt to drop the materialized view.
2893            self.adapter.drop_view(name, cascade=cascade, ignore_if_not_exists=False)
2894        except Exception:
2895            logger.debug(
2896                "Failed to drop view '%s'. Trying to drop the materialized view instead",
2897                name,
2898                exc_info=True,
2899            )
2900            self.adapter.drop_view(
2901                name, materialized=True, cascade=cascade, ignore_if_not_exists=True
2902            )
2903        logger.info("Dropped view '%s'", name)
2904
2905    def _is_materialized_view(self, model: Model) -> bool:
2906        return isinstance(model.kind, ViewKind) and model.kind.materialized

Helper class that provides a standard way to create an ABC using inheritance.

def insert( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_first_insert: bool, render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
2717    def insert(
2718        self,
2719        table_name: str,
2720        query_or_df: QueryOrDF,
2721        model: Model,
2722        is_first_insert: bool,
2723        render_kwargs: t.Dict[str, t.Any],
2724        **kwargs: t.Any,
2725    ) -> None:
2726        # We should recreate MVs across supported engines (Snowflake, BigQuery etc) because
2727        # if upstream tables were recreated (e.g FULL models), the MVs would be silently invalidated.
2728        # The only exception to that rule is RisingWave which doesn't support CREATE OR REPLACE, so upstream
2729        # models don't recreate their physical tables for the MVs to be invalidated.
2730        # However, even for RW we still want to recreate MVs to avoid stale references, as is the case with normal views.
2731        # The flag is_first_insert is used for that matter as a signal to recreate the MV if the snapshot's intervals
2732        # have been cleared by `should_force_rebuild`
2733        is_materialized_view = self._is_materialized_view(model)
2734        must_recreate_view = not self.adapter.HAS_VIEW_BINDING or (
2735            is_materialized_view and is_first_insert
2736        )
2737
2738        # Some engines (e.g. StarRocks) maintain materialized views automatically (via auto/scheduled
2739        # REFRESH) and can only recreate them via a destructive DROP + CREATE, which deletes the
2740        # materialized data and forces a full rebuild. For those, an existing MV must not be recreated
2741        # on routine evaluation (e.g. every `sqlmesh run`); only build it on the first insert (a new
2742        # version) or when a rebuild is explicitly forced (intervals cleared by `should_force_rebuild`,
2743        # which sets `is_first_insert`).
2744        if (
2745            is_materialized_view
2746            and not is_first_insert
2747            and not self.adapter.RECREATE_MATERIALIZED_VIEW_ON_EVALUATION
2748        ):
2749            must_recreate_view = False
2750
2751        if self.adapter.table_exists(table_name) and not must_recreate_view:
2752            logger.info("Skipping creation of the view '%s'", table_name)
2753            return
2754
2755        logger.info("Replacing view '%s'", table_name)
2756        materialized_properties = None
2757        if is_materialized_view:
2758            materialized_properties = {
2759                "partitioned_by": model.partitioned_by,
2760                "partition_interval_unit": model.partition_interval_unit,
2761                "clustered_by": model.clustered_by,
2762                "has_audits": bool(model.audits_with_args),
2763            }
2764        self.adapter.create_view(
2765            table_name,
2766            query_or_df,
2767            model.columns_to_types,
2768            replace=must_recreate_view,
2769            materialized=is_materialized_view,
2770            materialized_properties=materialized_properties,
2771            view_properties=kwargs.get("physical_properties", model.physical_properties),
2772            table_description=model.description,
2773            column_descriptions=model.column_descriptions,
2774        )
2775
2776        # Apply grants after view creation / replacement
2777        is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2778        self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable)

Inserts the given query or a DataFrame into the target table or a view.

Arguments:
  • table_name: The name of the target table or view.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • is_first_insert: Whether this is the first insert for this version of a model. This value is set to True if no data has been previously inserted into the target table, or when the entire history of the target model has been restated. Note that in the latter case, the table might contain data from previous executions, and it is the responsibility of a specific evaluation strategy to handle the truncation of the table if necessary.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
def append( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
2780    def append(
2781        self,
2782        table_name: str,
2783        query_or_df: QueryOrDF,
2784        model: Model,
2785        render_kwargs: t.Dict[str, t.Any],
2786        **kwargs: t.Any,
2787    ) -> None:
2788        raise ConfigError(f"Cannot append to a view '{table_name}'.")

Appends the given query or a DataFrame to the existing table.

Arguments:
  • table_name: The target table name.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
def create( self, table_name: str, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_table_deployable: bool, render_kwargs: Dict[str, Any], skip_grants: bool, **kwargs: Any) -> None:
2790    def create(
2791        self,
2792        table_name: str,
2793        model: Model,
2794        is_table_deployable: bool,
2795        render_kwargs: t.Dict[str, t.Any],
2796        skip_grants: bool,
2797        **kwargs: t.Any,
2798    ) -> None:
2799        is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2800
2801        if self.adapter.table_exists(table_name):
2802            # Make sure we don't recreate the view to prevent deletion of downstream views in engines with no late
2803            # binding support (because of DROP CASCADE).
2804            logger.info("View '%s' already exists", table_name)
2805
2806            if not skip_grants:
2807                # Always apply grants when present, even if view exists, to handle grants updates
2808                self._apply_grants(
2809                    model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2810                )
2811            return
2812
2813        logger.info("Creating view '%s'", table_name)
2814        materialized = self._is_materialized_view(model)
2815        materialized_properties = None
2816        if materialized:
2817            materialized_properties = {
2818                "partitioned_by": model.partitioned_by,
2819                "clustered_by": model.clustered_by,
2820                "partition_interval_unit": model.partition_interval_unit,
2821                "has_audits": bool(model.audits_with_args),
2822            }
2823        self.adapter.create_view(
2824            table_name,
2825            model.render_query_or_raise(**render_kwargs),
2826            # Make sure we never replace the view during creation to avoid race conditions in engines with no late binding support.
2827            replace=False,
2828            materialized=self._is_materialized_view(model),
2829            materialized_properties=materialized_properties,
2830            view_properties=kwargs.get("physical_properties", model.physical_properties),
2831            table_description=model.description if is_table_deployable else None,
2832            column_descriptions=model.column_descriptions if is_table_deployable else None,
2833        )
2834
2835        if not skip_grants:
2836            # Apply grants after view creation
2837            self._apply_grants(
2838                model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2839            )

Creates the target table or view.

Note that the intention here is to just create the table structure, data is loaded in insert() and append()

Arguments:
  • table_name: The name of a table or a view.
  • model: The target model.
  • is_table_deployable: True if this creation request is for the "main" table that might be deployed to a production environment. False if this creation request is for the "dev preview" table. Note that this flag is not related to the DeployabilityIndex which determines if the snapshot is deployable to production or not
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
def migrate( self, target_table_name: str, source_table_name: str, snapshot: sqlmesh.core.snapshot.definition.Snapshot, *, ignore_destructive: bool, ignore_additive: bool, **kwargs: Any) -> None:
2841    def migrate(
2842        self,
2843        target_table_name: str,
2844        source_table_name: str,
2845        snapshot: Snapshot,
2846        *,
2847        ignore_destructive: bool,
2848        ignore_additive: bool,
2849        **kwargs: t.Any,
2850    ) -> None:
2851        logger.info("Migrating view '%s'", target_table_name)
2852        model = snapshot.model
2853        render_kwargs = dict(
2854            execution_time=now(), snapshots=kwargs["snapshots"], engine_adapter=self.adapter
2855        )
2856
2857        is_materialized_view = self._is_materialized_view(model)
2858        materialized_properties = None
2859        if is_materialized_view:
2860            materialized_properties = {
2861                "partitioned_by": model.partitioned_by,
2862                "clustered_by": model.clustered_by,
2863                "partition_interval_unit": model.partition_interval_unit,
2864                "has_audits": bool(model.audits_with_args),
2865            }
2866
2867        self.adapter.create_view(
2868            target_table_name,
2869            model.render_query_or_raise(**render_kwargs),
2870            model.columns_to_types,
2871            materialized=is_materialized_view,
2872            materialized_properties=materialized_properties,
2873            view_properties=model.render_physical_properties(**render_kwargs),
2874            table_description=model.description,
2875            column_descriptions=model.column_descriptions,
2876        )
2877
2878        # Apply grants after view migration
2879        deployability_index = kwargs.get("deployability_index")
2880        is_snapshot_deployable = (
2881            deployability_index.is_deployable(snapshot) if deployability_index else False
2882        )
2883        self._apply_grants(
2884            snapshot.model, target_table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2885        )

Migrates the target table schema so that it corresponds to the source table schema.

Arguments:
  • target_table_name: The target table name.
  • source_table_name: The source table name.
  • snapshot: The target snapshot.
  • ignore_destructive: If True, destructive changes are not created when migrating. This is used for forward-only models that are being migrated to a new version.
  • ignore_additive: If True, additive changes are not created when migrating. This is used for forward-only models that are being migrated to a new version.
def delete(self, name: str, **kwargs: Any) -> None:
2887    def delete(self, name: str, **kwargs: t.Any) -> None:
2888        cascade = kwargs.pop("cascade", False)
2889        try:
2890            # Some engines (e.g., RisingWave) don’t fail when dropping a materialized view with a DROP VIEW statement,
2891            # because views and materialized views don’t share the same namespace. Therefore, we should not ignore if the
2892            # view doesn't exist and let the exception handler attempt to drop the materialized view.
2893            self.adapter.drop_view(name, cascade=cascade, ignore_if_not_exists=False)
2894        except Exception:
2895            logger.debug(
2896                "Failed to drop view '%s'. Trying to drop the materialized view instead",
2897                name,
2898                exc_info=True,
2899            )
2900            self.adapter.drop_view(
2901                name, materialized=True, cascade=cascade, ignore_if_not_exists=True
2902            )
2903        logger.info("Dropped view '%s'", name)

Deletes a target table or a view.

Arguments:
  • name: The name of a table or a view.
class CustomMaterialization(IncrementalStrategy, typing.Generic[~C]):
2912class CustomMaterialization(IncrementalStrategy, t.Generic[C]):
2913    """Base class for custom materializations."""
2914
2915    def insert(
2916        self,
2917        table_name: str,
2918        query_or_df: QueryOrDF,
2919        model: Model,
2920        is_first_insert: bool,
2921        render_kwargs: t.Dict[str, t.Any],
2922        **kwargs: t.Any,
2923    ) -> None:
2924        """Inserts the given query or a DataFrame into the target table or a view.
2925
2926        Args:
2927            table_name: The name of the target table or view.
2928            query_or_df: A query or a DataFrame to insert.
2929            model: The target model.
2930            is_first_insert: Whether this is the first insert for this version of a model. This value is set to True
2931                if no data has been previously inserted into the target table, or when the entire history of the target model has
2932                been restated. Note that in the latter case, the table might contain data from previous executions, and it is the
2933                responsibility of a specific evaluation strategy to handle the truncation of the table if necessary.
2934            render_kwargs: Additional key-value arguments to pass when rendering the model's query.
2935        """
2936        raise NotImplementedError(
2937            "Custom materialization strategies must implement the 'insert' method."
2938        )

Base class for custom materializations.

def insert( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_first_insert: bool, render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
2915    def insert(
2916        self,
2917        table_name: str,
2918        query_or_df: QueryOrDF,
2919        model: Model,
2920        is_first_insert: bool,
2921        render_kwargs: t.Dict[str, t.Any],
2922        **kwargs: t.Any,
2923    ) -> None:
2924        """Inserts the given query or a DataFrame into the target table or a view.
2925
2926        Args:
2927            table_name: The name of the target table or view.
2928            query_or_df: A query or a DataFrame to insert.
2929            model: The target model.
2930            is_first_insert: Whether this is the first insert for this version of a model. This value is set to True
2931                if no data has been previously inserted into the target table, or when the entire history of the target model has
2932                been restated. Note that in the latter case, the table might contain data from previous executions, and it is the
2933                responsibility of a specific evaluation strategy to handle the truncation of the table if necessary.
2934            render_kwargs: Additional key-value arguments to pass when rendering the model's query.
2935        """
2936        raise NotImplementedError(
2937            "Custom materialization strategies must implement the 'insert' method."
2938        )

Inserts the given query or a DataFrame into the target table or a view.

Arguments:
  • table_name: The name of the target table or view.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • is_first_insert: Whether this is the first insert for this version of a model. This value is set to True if no data has been previously inserted into the target table, or when the entire history of the target model has been restated. Note that in the latter case, the table might contain data from previous executions, and it is the responsibility of a specific evaluation strategy to handle the truncation of the table if necessary.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
def get_custom_materialization_kind_type( st: Type[CustomMaterialization]) -> Type[sqlmesh.core.model.kind.CustomKind]:
2946def get_custom_materialization_kind_type(st: t.Type[CustomMaterialization]) -> t.Type[CustomKind]:
2947    # try to read if there is a custom 'kind' type in use by inspecting the type signature
2948    # eg try to read 'MyCustomKind' from:
2949    # >>>> class MyCustomMaterialization(CustomMaterialization[MyCustomKind])
2950    # and fall back to base CustomKind if there is no generic type declared
2951    if hasattr(st, "__orig_bases__"):
2952        for base in st.__orig_bases__:
2953            if hasattr(base, "__origin__") and base.__origin__ == CustomMaterialization:
2954                for generic_arg in t.get_args(base):
2955                    if not issubclass(generic_arg, CustomKind):
2956                        raise SQLMeshError(
2957                            f"Custom materialization kind '{generic_arg.__name__}' must be a subclass of CustomKind"
2958                        )
2959
2960                    return generic_arg
2961
2962    return CustomKind
def get_custom_materialization_type( name: str, raise_errors: bool = True) -> Optional[Tuple[Type[sqlmesh.core.model.kind.CustomKind], Type[CustomMaterialization]]]:
2965def get_custom_materialization_type(
2966    name: str, raise_errors: bool = True
2967) -> t.Optional[t.Tuple[t.Type[CustomKind], t.Type[CustomMaterialization]]]:
2968    global _custom_materialization_type_cache
2969
2970    strategy_key = name.lower()
2971
2972    try:
2973        if (
2974            _custom_materialization_type_cache is None
2975            or strategy_key not in _custom_materialization_type_cache
2976        ):
2977            strategy_types = list(CustomMaterialization.__subclasses__())
2978
2979            entry_points = metadata.entry_points(group="sqlmesh.materializations")
2980            for entry_point in entry_points:
2981                strategy_type = entry_point.load()
2982                if not issubclass(strategy_type, CustomMaterialization):
2983                    raise SQLMeshError(
2984                        f"Custom materialization entry point '{entry_point.name}' must be a subclass of CustomMaterialization."
2985                    )
2986                strategy_types.append(strategy_type)
2987
2988            _custom_materialization_type_cache = {
2989                getattr(strategy_type, "NAME", strategy_type.__name__).lower(): (
2990                    get_custom_materialization_kind_type(strategy_type),
2991                    strategy_type,
2992                )
2993                for strategy_type in strategy_types
2994            }
2995
2996        if strategy_key not in _custom_materialization_type_cache:
2997            raise ConfigError(f"Materialization strategy with name '{name}' was not found.")
2998    except (SQLMeshError, ConfigError) as e:
2999        if raise_errors:
3000            raise e
3001
3002        from sqlmesh.core.console import get_console
3003
3004        get_console().log_warning(str(e))
3005        return None
3006
3007    strategy_kind_type, strategy_type = _custom_materialization_type_cache[strategy_key]
3008    logger.debug(
3009        "Resolved custom materialization '%s' to '%s' (%s)", name, strategy_type, strategy_kind_type
3010    )
3011
3012    return strategy_kind_type, strategy_type
def get_custom_materialization_type_or_raise( name: str) -> Tuple[Type[sqlmesh.core.model.kind.CustomKind], Type[CustomMaterialization]]:
3015def get_custom_materialization_type_or_raise(
3016    name: str,
3017) -> t.Tuple[t.Type[CustomKind], t.Type[CustomMaterialization]]:
3018    types = get_custom_materialization_type(name, raise_errors=True)
3019    if types is not None:
3020        return types[0], types[1]
3021
3022    # Shouldnt get here as get_custom_materialization_type() has raise_errors=True, but just in case...
3023    raise SQLMeshError(f"Custom materialization '{name}' not present in the Python environment")
class DbtCustomMaterializationStrategy(MaterializableStrategy):
3026class DbtCustomMaterializationStrategy(MaterializableStrategy):
3027    def __init__(
3028        self,
3029        adapter: EngineAdapter,
3030        materialization_name: str,
3031        materialization_template: str,
3032    ):
3033        super().__init__(adapter)
3034        self.materialization_name = materialization_name
3035        self.materialization_template = materialization_template
3036
3037    def create(
3038        self,
3039        table_name: str,
3040        model: Model,
3041        is_table_deployable: bool,
3042        render_kwargs: t.Dict[str, t.Any],
3043        skip_grants: bool,
3044        **kwargs: t.Any,
3045    ) -> None:
3046        original_query = model.render_query_or_raise(**render_kwargs)
3047        self._execute_materialization(
3048            table_name=table_name,
3049            query_or_df=original_query.limit(0),
3050            model=model,
3051            is_first_insert=True,
3052            render_kwargs=render_kwargs,
3053            create_only=True,
3054            **kwargs,
3055        )
3056
3057        # Apply grants after dbt custom materialization table creation
3058        if not skip_grants:
3059            is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
3060            self._apply_grants(
3061                model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
3062            )
3063
3064    def insert(
3065        self,
3066        table_name: str,
3067        query_or_df: QueryOrDF,
3068        model: Model,
3069        is_first_insert: bool,
3070        render_kwargs: t.Dict[str, t.Any],
3071        **kwargs: t.Any,
3072    ) -> None:
3073        self._execute_materialization(
3074            table_name=table_name,
3075            query_or_df=query_or_df,
3076            model=model,
3077            is_first_insert=is_first_insert,
3078            render_kwargs=render_kwargs,
3079            **kwargs,
3080        )
3081
3082        # Apply grants after custom materialization insert (only on first insert)
3083        if is_first_insert:
3084            is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
3085            self._apply_grants(
3086                model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
3087            )
3088
3089    def append(
3090        self,
3091        table_name: str,
3092        query_or_df: QueryOrDF,
3093        model: Model,
3094        render_kwargs: t.Dict[str, t.Any],
3095        **kwargs: t.Any,
3096    ) -> None:
3097        return self.insert(
3098            table_name,
3099            query_or_df,
3100            model,
3101            is_first_insert=False,
3102            render_kwargs=render_kwargs,
3103            **kwargs,
3104        )
3105
3106    def run_pre_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
3107        # in dbt custom materialisations it's up to the user to run the pre hooks inside the transaction
3108        if not render_kwargs.get("inside_transaction", True):
3109            super().run_pre_statements(
3110                snapshot=snapshot,
3111                render_kwargs=render_kwargs,
3112            )
3113
3114    def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
3115        # in dbt custom materialisations it's up to the user to run the post hooks inside the transaction
3116        if not render_kwargs.get("inside_transaction", True):
3117            super().run_post_statements(
3118                snapshot=snapshot,
3119                render_kwargs=render_kwargs,
3120            )
3121
3122    def _execute_materialization(
3123        self,
3124        table_name: str,
3125        query_or_df: QueryOrDF,
3126        model: Model,
3127        is_first_insert: bool,
3128        render_kwargs: t.Dict[str, t.Any],
3129        create_only: bool = False,
3130        **kwargs: t.Any,
3131    ) -> None:
3132        jinja_macros = model.jinja_macros
3133
3134        # For vdes we need to use the table, since we don't know the schema/table at parse time
3135        parts = exp.to_table(table_name, dialect=self.adapter.dialect)
3136
3137        existing_globals = jinja_macros.global_objs
3138        relation_info = existing_globals.get("this")
3139        if isinstance(relation_info, dict):
3140            relation_info["database"] = parts.catalog
3141            relation_info["identifier"] = parts.name
3142            relation_info["name"] = parts.name
3143
3144        jinja_globals = {
3145            **existing_globals,
3146            "this": relation_info,
3147            "database": parts.catalog,
3148            "schema": parts.db,
3149            "identifier": parts.name,
3150            "target": existing_globals.get("target", {"type": self.adapter.dialect}),
3151            "execution_dt": kwargs.get("execution_time"),
3152            "engine_adapter": self.adapter,
3153            "sql": str(query_or_df),
3154            "is_first_insert": is_first_insert,
3155            "create_only": create_only,
3156            "pre_hooks": [
3157                AttributeDict({"sql": s.this.this, "transaction": transaction})
3158                for s in model.pre_statements
3159                if (transaction := s.args.get("transaction", True))
3160            ],
3161            "post_hooks": [
3162                AttributeDict({"sql": s.this.this, "transaction": transaction})
3163                for s in model.post_statements
3164                if (transaction := s.args.get("transaction", True))
3165            ],
3166            "model_instance": model,
3167            **kwargs,
3168        }
3169
3170        try:
3171            jinja_env = jinja_macros.build_environment(**jinja_globals)
3172            template = jinja_env.from_string(self.materialization_template)
3173
3174            try:
3175                template.render()
3176            except MacroReturnVal as ret:
3177                # this is a successful return from a macro call (dbt uses this list of Relations to update their relation cache)
3178                returned_relations = ret.value.get("relations", [])
3179                logger.info(
3180                    f"Materialization {self.materialization_name} returned relations: {returned_relations}"
3181                )
3182
3183        except Exception as e:
3184            raise SQLMeshError(
3185                f"Failed to execute dbt materialization '{self.materialization_name}': {e}"
3186            ) from e

Helper class that provides a standard way to create an ABC using inheritance.

DbtCustomMaterializationStrategy( adapter: <MagicMock id='130969723718864'>, materialization_name: str, materialization_template: str)
3027    def __init__(
3028        self,
3029        adapter: EngineAdapter,
3030        materialization_name: str,
3031        materialization_template: str,
3032    ):
3033        super().__init__(adapter)
3034        self.materialization_name = materialization_name
3035        self.materialization_template = materialization_template
materialization_name
materialization_template
def create( self, table_name: str, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_table_deployable: bool, render_kwargs: Dict[str, Any], skip_grants: bool, **kwargs: Any) -> None:
3037    def create(
3038        self,
3039        table_name: str,
3040        model: Model,
3041        is_table_deployable: bool,
3042        render_kwargs: t.Dict[str, t.Any],
3043        skip_grants: bool,
3044        **kwargs: t.Any,
3045    ) -> None:
3046        original_query = model.render_query_or_raise(**render_kwargs)
3047        self._execute_materialization(
3048            table_name=table_name,
3049            query_or_df=original_query.limit(0),
3050            model=model,
3051            is_first_insert=True,
3052            render_kwargs=render_kwargs,
3053            create_only=True,
3054            **kwargs,
3055        )
3056
3057        # Apply grants after dbt custom materialization table creation
3058        if not skip_grants:
3059            is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
3060            self._apply_grants(
3061                model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
3062            )

Creates the target table or view.

Note that the intention here is to just create the table structure, data is loaded in insert() and append()

Arguments:
  • table_name: The name of a table or a view.
  • model: The target model.
  • is_table_deployable: True if this creation request is for the "main" table that might be deployed to a production environment. False if this creation request is for the "dev preview" table. Note that this flag is not related to the DeployabilityIndex which determines if the snapshot is deployable to production or not
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
def insert( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_first_insert: bool, render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
3064    def insert(
3065        self,
3066        table_name: str,
3067        query_or_df: QueryOrDF,
3068        model: Model,
3069        is_first_insert: bool,
3070        render_kwargs: t.Dict[str, t.Any],
3071        **kwargs: t.Any,
3072    ) -> None:
3073        self._execute_materialization(
3074            table_name=table_name,
3075            query_or_df=query_or_df,
3076            model=model,
3077            is_first_insert=is_first_insert,
3078            render_kwargs=render_kwargs,
3079            **kwargs,
3080        )
3081
3082        # Apply grants after custom materialization insert (only on first insert)
3083        if is_first_insert:
3084            is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
3085            self._apply_grants(
3086                model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
3087            )

Inserts the given query or a DataFrame into the target table or a view.

Arguments:
  • table_name: The name of the target table or view.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • is_first_insert: Whether this is the first insert for this version of a model. This value is set to True if no data has been previously inserted into the target table, or when the entire history of the target model has been restated. Note that in the latter case, the table might contain data from previous executions, and it is the responsibility of a specific evaluation strategy to handle the truncation of the table if necessary.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
def append( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
3089    def append(
3090        self,
3091        table_name: str,
3092        query_or_df: QueryOrDF,
3093        model: Model,
3094        render_kwargs: t.Dict[str, t.Any],
3095        **kwargs: t.Any,
3096    ) -> None:
3097        return self.insert(
3098            table_name,
3099            query_or_df,
3100            model,
3101            is_first_insert=False,
3102            render_kwargs=render_kwargs,
3103            **kwargs,
3104        )

Appends the given query or a DataFrame to the existing table.

Arguments:
  • table_name: The target table name.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
def run_pre_statements( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, render_kwargs: Any) -> None:
3106    def run_pre_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
3107        # in dbt custom materialisations it's up to the user to run the pre hooks inside the transaction
3108        if not render_kwargs.get("inside_transaction", True):
3109            super().run_pre_statements(
3110                snapshot=snapshot,
3111                render_kwargs=render_kwargs,
3112            )

Executes the snapshot's pre statements.

Arguments:
  • snapshot: The target snapshot.
  • render_kwargs: Additional key-value arguments to pass when rendering the statements.
def run_post_statements( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, render_kwargs: Any) -> None:
3114    def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
3115        # in dbt custom materialisations it's up to the user to run the post hooks inside the transaction
3116        if not render_kwargs.get("inside_transaction", True):
3117            super().run_post_statements(
3118                snapshot=snapshot,
3119                render_kwargs=render_kwargs,
3120            )

Executes the snapshot's post statements.

Arguments:
  • snapshot: The target snapshot.
  • render_kwargs: Additional key-value arguments to pass when rendering the statements.
class EngineManagedStrategy(MaterializableStrategy):
3189class EngineManagedStrategy(MaterializableStrategy):
3190    def create(
3191        self,
3192        table_name: str,
3193        model: Model,
3194        is_table_deployable: bool,
3195        render_kwargs: t.Dict[str, t.Any],
3196        skip_grants: bool,
3197        **kwargs: t.Any,
3198    ) -> None:
3199        is_snapshot_deployable: bool = kwargs["is_snapshot_deployable"]
3200
3201        if is_table_deployable and is_snapshot_deployable:
3202            # We could deploy this to prod; create a proper managed table
3203            logger.info("Creating managed table: %s", table_name)
3204            physical_properties = _adjust_physical_properties_for_engine(
3205                self.adapter, model, kwargs.get("physical_properties", model.physical_properties)
3206            )
3207            self.adapter.create_managed_table(
3208                table_name=table_name,
3209                query=model.render_query_or_raise(**render_kwargs),
3210                target_columns_to_types=model.columns_to_types,
3211                partitioned_by=model.partitioned_by,
3212                clustered_by=model.clustered_by,  # type: ignore[arg-type]
3213                table_properties=physical_properties,
3214                table_description=model.description,
3215                column_descriptions=model.column_descriptions,
3216                table_format=model.table_format,
3217            )
3218
3219            # Apply grants after managed table creation
3220            if not skip_grants:
3221                self._apply_grants(
3222                    model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
3223                )
3224
3225        elif not is_table_deployable:
3226            # Only create the dev preview table as a normal table.
3227            # For the main table, if the snapshot is cant be deployed to prod (eg upstream is forward-only) do nothing.
3228            # Any downstream models that reference it will be updated to point to the dev preview table.
3229            # If the user eventually tries to deploy it, the logic in insert() will see it doesnt exist and create it
3230            super().create(
3231                table_name=table_name,
3232                model=model,
3233                is_table_deployable=is_table_deployable,
3234                render_kwargs=render_kwargs,
3235                skip_grants=skip_grants,
3236                **kwargs,
3237            )
3238
3239    def insert(
3240        self,
3241        table_name: str,
3242        query_or_df: QueryOrDF,
3243        model: Model,
3244        is_first_insert: bool,
3245        render_kwargs: t.Dict[str, t.Any],
3246        **kwargs: t.Any,
3247    ) -> None:
3248        deployability_index: DeployabilityIndex = kwargs["deployability_index"]
3249        snapshot: Snapshot = kwargs["snapshot"]
3250        is_snapshot_deployable = deployability_index.is_deployable(snapshot)
3251        if is_first_insert and is_snapshot_deployable and not self.adapter.table_exists(table_name):
3252            self.adapter.create_managed_table(
3253                table_name=table_name,
3254                query=query_or_df,  # type: ignore
3255                target_columns_to_types=model.columns_to_types,
3256                partitioned_by=model.partitioned_by,
3257                clustered_by=model.clustered_by,  # type: ignore[arg-type]
3258                table_properties=kwargs.get("physical_properties", model.physical_properties),
3259                table_description=model.description,
3260                column_descriptions=model.column_descriptions,
3261                table_format=model.table_format,
3262            )
3263            self._apply_grants(
3264                model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
3265            )
3266        elif not is_snapshot_deployable:
3267            # Snapshot isnt deployable; update the preview table instead
3268            # If the snapshot was deployable, then data would have already been loaded in create() because a managed table would have been created
3269            logger.info(
3270                "Updating preview table: %s (for managed model: %s)",
3271                table_name,
3272                model.name,
3273            )
3274            self._replace_query_for_model(
3275                model=model,
3276                name=table_name,
3277                query_or_df=query_or_df,
3278                render_kwargs=render_kwargs,
3279                **kwargs,
3280            )
3281
3282    def append(
3283        self,
3284        table_name: str,
3285        query_or_df: QueryOrDF,
3286        model: Model,
3287        render_kwargs: t.Dict[str, t.Any],
3288        **kwargs: t.Any,
3289    ) -> None:
3290        raise ConfigError(f"Cannot append to a managed table '{table_name}'.")
3291
3292    def migrate(
3293        self,
3294        target_table_name: str,
3295        source_table_name: str,
3296        snapshot: Snapshot,
3297        *,
3298        ignore_destructive: bool,
3299        ignore_additive: bool,
3300        **kwargs: t.Any,
3301    ) -> None:
3302        potential_alter_operations = self.adapter.get_alter_operations(
3303            target_table_name,
3304            source_table_name,
3305            ignore_destructive=ignore_destructive,
3306            ignore_additive=ignore_additive,
3307        )
3308        if len(potential_alter_operations) > 0:
3309            # this can happen if a user changes a managed model and deliberately overrides a plan to be forward only, eg `sqlmesh plan --forward-only`
3310            raise MigrationNotSupportedError(
3311                f"The schema of the managed model '{target_table_name}' cannot be updated in a forward-only fashion."
3312            )
3313
3314        # Apply grants after verifying no schema changes
3315        deployability_index = kwargs.get("deployability_index")
3316        is_snapshot_deployable = (
3317            deployability_index.is_deployable(snapshot) if deployability_index else False
3318        )
3319        self._apply_grants(
3320            snapshot.model, target_table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
3321        )
3322
3323    def delete(self, name: str, **kwargs: t.Any) -> None:
3324        # a dev preview table is created as a normal table, so it needs to be dropped as a normal table
3325        _check_table_db_is_physical_schema(name, kwargs["physical_schema"])
3326        if kwargs["is_table_deployable"]:
3327            self.adapter.drop_managed_table(name)
3328            logger.info("Dropped managed table '%s'", name)
3329        else:
3330            self.adapter.drop_table(name)
3331            logger.info("Dropped dev preview for managed table '%s'", name)

Helper class that provides a standard way to create an ABC using inheritance.

def create( self, table_name: str, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_table_deployable: bool, render_kwargs: Dict[str, Any], skip_grants: bool, **kwargs: Any) -> None:
3190    def create(
3191        self,
3192        table_name: str,
3193        model: Model,
3194        is_table_deployable: bool,
3195        render_kwargs: t.Dict[str, t.Any],
3196        skip_grants: bool,
3197        **kwargs: t.Any,
3198    ) -> None:
3199        is_snapshot_deployable: bool = kwargs["is_snapshot_deployable"]
3200
3201        if is_table_deployable and is_snapshot_deployable:
3202            # We could deploy this to prod; create a proper managed table
3203            logger.info("Creating managed table: %s", table_name)
3204            physical_properties = _adjust_physical_properties_for_engine(
3205                self.adapter, model, kwargs.get("physical_properties", model.physical_properties)
3206            )
3207            self.adapter.create_managed_table(
3208                table_name=table_name,
3209                query=model.render_query_or_raise(**render_kwargs),
3210                target_columns_to_types=model.columns_to_types,
3211                partitioned_by=model.partitioned_by,
3212                clustered_by=model.clustered_by,  # type: ignore[arg-type]
3213                table_properties=physical_properties,
3214                table_description=model.description,
3215                column_descriptions=model.column_descriptions,
3216                table_format=model.table_format,
3217            )
3218
3219            # Apply grants after managed table creation
3220            if not skip_grants:
3221                self._apply_grants(
3222                    model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
3223                )
3224
3225        elif not is_table_deployable:
3226            # Only create the dev preview table as a normal table.
3227            # For the main table, if the snapshot is cant be deployed to prod (eg upstream is forward-only) do nothing.
3228            # Any downstream models that reference it will be updated to point to the dev preview table.
3229            # If the user eventually tries to deploy it, the logic in insert() will see it doesnt exist and create it
3230            super().create(
3231                table_name=table_name,
3232                model=model,
3233                is_table_deployable=is_table_deployable,
3234                render_kwargs=render_kwargs,
3235                skip_grants=skip_grants,
3236                **kwargs,
3237            )

Creates the target table or view.

Note that the intention here is to just create the table structure, data is loaded in insert() and append()

Arguments:
  • table_name: The name of a table or a view.
  • model: The target model.
  • is_table_deployable: True if this creation request is for the "main" table that might be deployed to a production environment. False if this creation request is for the "dev preview" table. Note that this flag is not related to the DeployabilityIndex which determines if the snapshot is deployable to production or not
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
def insert( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_first_insert: bool, render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
3239    def insert(
3240        self,
3241        table_name: str,
3242        query_or_df: QueryOrDF,
3243        model: Model,
3244        is_first_insert: bool,
3245        render_kwargs: t.Dict[str, t.Any],
3246        **kwargs: t.Any,
3247    ) -> None:
3248        deployability_index: DeployabilityIndex = kwargs["deployability_index"]
3249        snapshot: Snapshot = kwargs["snapshot"]
3250        is_snapshot_deployable = deployability_index.is_deployable(snapshot)
3251        if is_first_insert and is_snapshot_deployable and not self.adapter.table_exists(table_name):
3252            self.adapter.create_managed_table(
3253                table_name=table_name,
3254                query=query_or_df,  # type: ignore
3255                target_columns_to_types=model.columns_to_types,
3256                partitioned_by=model.partitioned_by,
3257                clustered_by=model.clustered_by,  # type: ignore[arg-type]
3258                table_properties=kwargs.get("physical_properties", model.physical_properties),
3259                table_description=model.description,
3260                column_descriptions=model.column_descriptions,
3261                table_format=model.table_format,
3262            )
3263            self._apply_grants(
3264                model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
3265            )
3266        elif not is_snapshot_deployable:
3267            # Snapshot isnt deployable; update the preview table instead
3268            # If the snapshot was deployable, then data would have already been loaded in create() because a managed table would have been created
3269            logger.info(
3270                "Updating preview table: %s (for managed model: %s)",
3271                table_name,
3272                model.name,
3273            )
3274            self._replace_query_for_model(
3275                model=model,
3276                name=table_name,
3277                query_or_df=query_or_df,
3278                render_kwargs=render_kwargs,
3279                **kwargs,
3280            )

Inserts the given query or a DataFrame into the target table or a view.

Arguments:
  • table_name: The name of the target table or view.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • is_first_insert: Whether this is the first insert for this version of a model. This value is set to True if no data has been previously inserted into the target table, or when the entire history of the target model has been restated. Note that in the latter case, the table might contain data from previous executions, and it is the responsibility of a specific evaluation strategy to handle the truncation of the table if necessary.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
def append( self, table_name: str, query_or_df: <MagicMock id='130969720704880'>, model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], render_kwargs: Dict[str, Any], **kwargs: Any) -> None:
3282    def append(
3283        self,
3284        table_name: str,
3285        query_or_df: QueryOrDF,
3286        model: Model,
3287        render_kwargs: t.Dict[str, t.Any],
3288        **kwargs: t.Any,
3289    ) -> None:
3290        raise ConfigError(f"Cannot append to a managed table '{table_name}'.")

Appends the given query or a DataFrame to the existing table.

Arguments:
  • table_name: The target table name.
  • query_or_df: A query or a DataFrame to insert.
  • model: The target model.
  • render_kwargs: Additional key-value arguments to pass when rendering the model's query.
def migrate( self, target_table_name: str, source_table_name: str, snapshot: sqlmesh.core.snapshot.definition.Snapshot, *, ignore_destructive: bool, ignore_additive: bool, **kwargs: Any) -> None:
3292    def migrate(
3293        self,
3294        target_table_name: str,
3295        source_table_name: str,
3296        snapshot: Snapshot,
3297        *,
3298        ignore_destructive: bool,
3299        ignore_additive: bool,
3300        **kwargs: t.Any,
3301    ) -> None:
3302        potential_alter_operations = self.adapter.get_alter_operations(
3303            target_table_name,
3304            source_table_name,
3305            ignore_destructive=ignore_destructive,
3306            ignore_additive=ignore_additive,
3307        )
3308        if len(potential_alter_operations) > 0:
3309            # this can happen if a user changes a managed model and deliberately overrides a plan to be forward only, eg `sqlmesh plan --forward-only`
3310            raise MigrationNotSupportedError(
3311                f"The schema of the managed model '{target_table_name}' cannot be updated in a forward-only fashion."
3312            )
3313
3314        # Apply grants after verifying no schema changes
3315        deployability_index = kwargs.get("deployability_index")
3316        is_snapshot_deployable = (
3317            deployability_index.is_deployable(snapshot) if deployability_index else False
3318        )
3319        self._apply_grants(
3320            snapshot.model, target_table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
3321        )

Migrates the target table schema so that it corresponds to the source table schema.

Arguments:
  • target_table_name: The target table name.
  • source_table_name: The source table name.
  • snapshot: The target snapshot.
  • ignore_destructive: If True, destructive changes are not created when migrating. This is used for forward-only models that are being migrated to a new version.
  • ignore_additive: If True, additive changes are not created when migrating. This is used for forward-only models that are being migrated to a new version.
def delete(self, name: str, **kwargs: Any) -> None:
3323    def delete(self, name: str, **kwargs: t.Any) -> None:
3324        # a dev preview table is created as a normal table, so it needs to be dropped as a normal table
3325        _check_table_db_is_physical_schema(name, kwargs["physical_schema"])
3326        if kwargs["is_table_deployable"]:
3327            self.adapter.drop_managed_table(name)
3328            logger.info("Dropped managed table '%s'", name)
3329        else:
3330            self.adapter.drop_table(name)
3331            logger.info("Dropped dev preview for managed table '%s'", name)

Deletes a target table or a view.

Arguments:
  • name: The name of a table or a view.