Edit on GitHub

sqlmesh.core.console

   1from __future__ import annotations
   2
   3import abc
   4import datetime
   5import typing as t
   6import unittest
   7import uuid
   8import logging
   9import textwrap
  10from humanize import metric, naturalsize
  11from itertools import zip_longest
  12from pathlib import Path
  13from hyperscript import h
  14from rich.console import Console as RichConsole
  15from rich.live import Live
  16from rich.progress import (
  17    BarColumn,
  18    Progress,
  19    SpinnerColumn,
  20    TaskID,
  21    TextColumn,
  22    TimeElapsedColumn,
  23)
  24from rich.prompt import Confirm, Prompt
  25from rich.status import Status
  26from rich.syntax import Syntax
  27from rich.table import Table
  28from rich.tree import Tree
  29from sqlglot import exp
  30
  31from sqlmesh.core.schema_diff import TableAlterOperation
  32from sqlmesh.core.test.result import ModelTextTestResult
  33from sqlmesh.core.environment import EnvironmentNamingInfo, EnvironmentSummary
  34from sqlmesh.core.linter.rule import RuleViolation
  35from sqlmesh.core.model import Model
  36from sqlmesh.core.snapshot import (
  37    Snapshot,
  38    SnapshotChangeCategory,
  39    SnapshotId,
  40    SnapshotInfoLike,
  41)
  42from sqlmesh.core.snapshot.definition import Interval, Intervals, SnapshotTableInfo
  43from sqlmesh.core.snapshot.execution_tracker import QueryExecutionStats
  44from sqlmesh.core.test import ModelTest
  45from sqlmesh.utils import rich as srich
  46from sqlmesh.utils import Verbosity
  47from sqlmesh.utils.concurrency import NodeExecutionFailedError
  48from sqlmesh.utils.date import time_like_to_str, to_date, yesterday_ds, to_ds, make_inclusive
  49from sqlmesh.utils.errors import (
  50    PythonModelEvalError,
  51    NodeAuditsErrors,
  52    format_destructive_change_msg,
  53    format_additive_change_msg,
  54)
  55from sqlmesh.utils.rich import strip_ansi_codes
  56
  57if t.TYPE_CHECKING:
  58    import ipywidgets as widgets
  59
  60    from sqlglot import exp
  61    from sqlglot.dialects.dialect import DialectType
  62    from sqlmesh.core.context_diff import ContextDiff
  63    from sqlmesh.core.plan import Plan, EvaluatablePlan, PlanBuilder, SnapshotIntervals
  64    from sqlmesh.core.table_diff import TableDiff, RowDiff, SchemaDiff
  65    from sqlmesh.core.config.connection import ConnectionConfig
  66    from sqlmesh.core.state_sync import Versions
  67
  68    LayoutWidget = t.TypeVar("LayoutWidget", bound=t.Union[widgets.VBox, widgets.HBox])
  69
  70
  71logger = logging.getLogger(__name__)
  72
  73
  74SNAPSHOT_CHANGE_CATEGORY_STR = {
  75    None: "Unknown",
  76    SnapshotChangeCategory.BREAKING: "Breaking",
  77    SnapshotChangeCategory.NON_BREAKING: "Non-breaking",
  78    SnapshotChangeCategory.FORWARD_ONLY: "Forward-only",
  79    SnapshotChangeCategory.INDIRECT_BREAKING: "Indirect Breaking",
  80    SnapshotChangeCategory.INDIRECT_NON_BREAKING: "Indirect Non-breaking",
  81    SnapshotChangeCategory.METADATA: "Metadata",
  82}
  83
  84PROGRESS_BAR_WIDTH = 40
  85LINE_WRAP_WIDTH = 100
  86
  87
  88class LinterConsole(abc.ABC):
  89    """Console for displaying linter violations"""
  90
  91    @abc.abstractmethod
  92    def show_linter_violations(
  93        self, violations: t.List[RuleViolation], model: Model, is_error: bool = False
  94    ) -> None:
  95        """Prints all linter violations depending on their severity"""
  96
  97
  98class StateExporterConsole(abc.ABC):
  99    """Console for describing a state export"""
 100
 101    @abc.abstractmethod
 102    def start_state_export(
 103        self,
 104        output_file: Path,
 105        gateway: t.Optional[str] = None,
 106        state_connection_config: t.Optional[ConnectionConfig] = None,
 107        environment_names: t.Optional[t.List[str]] = None,
 108        local_only: bool = False,
 109        confirm: bool = True,
 110    ) -> bool:
 111        """State a state export"""
 112
 113    @abc.abstractmethod
 114    def update_state_export_progress(
 115        self,
 116        version_count: t.Optional[int] = None,
 117        versions_complete: bool = False,
 118        snapshot_count: t.Optional[int] = None,
 119        snapshots_complete: bool = False,
 120        environment_count: t.Optional[int] = None,
 121        environments_complete: bool = False,
 122    ) -> None:
 123        """Update the state export progress"""
 124
 125    @abc.abstractmethod
 126    def stop_state_export(self, success: bool, output_file: Path) -> None:
 127        """Finish a state export"""
 128
 129
 130class StateImporterConsole(abc.ABC):
 131    """Console for describing a state import"""
 132
 133    @abc.abstractmethod
 134    def start_state_import(
 135        self,
 136        input_file: Path,
 137        gateway: str,
 138        state_connection_config: ConnectionConfig,
 139        clear: bool = False,
 140        confirm: bool = True,
 141    ) -> bool:
 142        """Start a state import"""
 143
 144    @abc.abstractmethod
 145    def update_state_import_progress(
 146        self,
 147        timestamp: t.Optional[str] = None,
 148        state_file_version: t.Optional[int] = None,
 149        versions: t.Optional[Versions] = None,
 150        snapshot_count: t.Optional[int] = None,
 151        snapshots_complete: bool = False,
 152        environment_count: t.Optional[int] = None,
 153        environments_complete: bool = False,
 154    ) -> None:
 155        """Update the state import process"""
 156
 157    @abc.abstractmethod
 158    def stop_state_import(self, success: bool, input_file: Path) -> None:
 159        """Finish a state import"""
 160
 161
 162class JanitorConsole(abc.ABC):
 163    """Console for describing a janitor / snapshot cleanup run"""
 164
 165    @abc.abstractmethod
 166    def start_cleanup(self, ignore_ttl: bool) -> bool:
 167        """Start a janitor / snapshot cleanup run.
 168
 169        Args:
 170            ignore_ttl: Indicates that the user wants to ignore the snapshot TTL and clean up everything not promoted to an environment
 171
 172        Returns:
 173            Whether or not the cleanup run should proceed
 174        """
 175
 176    @abc.abstractmethod
 177    def update_cleanup_progress(self, object_name: str) -> None:
 178        """Update the snapshot cleanup progress."""
 179
 180    @abc.abstractmethod
 181    def stop_cleanup(self, success: bool = True) -> None:
 182        """Indicates the janitor / snapshot cleanup run has ended
 183
 184        Args:
 185            success: Whether or not the cleanup completed successfully
 186        """
 187
 188
 189class DestroyConsole(abc.ABC):
 190    """Console for describing a destroy operation"""
 191
 192    @abc.abstractmethod
 193    def start_destroy(
 194        self,
 195        schemas_to_delete: t.Optional[t.Set[str]] = None,
 196        views_to_delete: t.Optional[t.Set[str]] = None,
 197        tables_to_delete: t.Optional[t.Set[str]] = None,
 198    ) -> bool:
 199        """Start a destroy operation.
 200
 201        Args:
 202            schemas_to_delete: Set of schemas that will be deleted
 203            views_to_delete: Set of views that will be deleted
 204            tables_to_delete: Set of tables that will be deleted
 205
 206        Returns:
 207            Whether or not the destroy operation should proceed
 208        """
 209
 210    @abc.abstractmethod
 211    def stop_destroy(self, success: bool = True) -> None:
 212        """Indicates the destroy operation has ended
 213
 214        Args:
 215            success: Whether or not the cleanup completed successfully
 216        """
 217
 218
 219class EnvironmentsConsole(abc.ABC):
 220    """Console for displaying environments"""
 221
 222    @abc.abstractmethod
 223    def print_environments(self, environments_summary: t.List[EnvironmentSummary]) -> None:
 224        """Prints all environment names along with expiry datetime."""
 225
 226    @abc.abstractmethod
 227    def show_intervals(self, snapshot_intervals: t.Dict[Snapshot, SnapshotIntervals]) -> None:
 228        """Show ready intervals"""
 229
 230
 231class DifferenceConsole(abc.ABC):
 232    """Console for displaying environment differences"""
 233
 234    @abc.abstractmethod
 235    def show_environment_difference_summary(
 236        self,
 237        context_diff: ContextDiff,
 238        no_diff: bool = True,
 239    ) -> None:
 240        """Displays a summary of differences for the environment."""
 241
 242    @abc.abstractmethod
 243    def show_model_difference_summary(
 244        self,
 245        context_diff: ContextDiff,
 246        environment_naming_info: EnvironmentNamingInfo,
 247        default_catalog: t.Optional[str],
 248        no_diff: bool = True,
 249    ) -> None:
 250        """Displays a summary of differences for the given models."""
 251
 252
 253class TableDiffConsole(abc.ABC):
 254    """Console for displaying table differences"""
 255
 256    @abc.abstractmethod
 257    def show_table_diff(
 258        self,
 259        table_diffs: t.List[TableDiff],
 260        show_sample: bool = True,
 261        skip_grain_check: bool = False,
 262        temp_schema: t.Optional[str] = None,
 263    ) -> None:
 264        """Display the table diff between two or multiple tables."""
 265
 266    @abc.abstractmethod
 267    def update_table_diff_progress(self, model: str) -> None:
 268        """Update table diff progress bar"""
 269
 270    @abc.abstractmethod
 271    def start_table_diff_progress(self, models_to_diff: int) -> None:
 272        """Start table diff progress bar"""
 273
 274    @abc.abstractmethod
 275    def start_table_diff_model_progress(self, model: str) -> None:
 276        """Start table diff model progress"""
 277
 278    @abc.abstractmethod
 279    def stop_table_diff_progress(self, success: bool) -> None:
 280        """Stop table diff progress bar"""
 281
 282    @abc.abstractmethod
 283    def show_table_diff_details(
 284        self,
 285        models_to_diff: t.List[str],
 286    ) -> None:
 287        """Display information about which tables are going to be diffed"""
 288
 289    @abc.abstractmethod
 290    def show_table_diff_summary(self, table_diff: TableDiff) -> None:
 291        """Display information about the tables being diffed and how they are being joined"""
 292
 293    @abc.abstractmethod
 294    def show_schema_diff(self, schema_diff: SchemaDiff) -> None:
 295        """Show table schema diff."""
 296
 297    @abc.abstractmethod
 298    def show_row_diff(
 299        self, row_diff: RowDiff, show_sample: bool = True, skip_grain_check: bool = False
 300    ) -> None:
 301        """Show table summary diff."""
 302
 303
 304class BaseConsole(abc.ABC):
 305    @abc.abstractmethod
 306    def log_error(self, message: str, *args: t.Any, **kwargs: t.Any) -> None:
 307        """Display error info to the user."""
 308
 309    @abc.abstractmethod
 310    def log_warning(
 311        self,
 312        short_message: str,
 313        long_message: t.Optional[str] = None,
 314        *args: t.Any,
 315        **kwargs: t.Any,
 316    ) -> None:
 317        """Display warning info to the user.
 318
 319        Args:
 320            short_message: The warning message to print to console.
 321            long_message: The warning message to log to file. If not provided, `short_message` is used.
 322        """
 323
 324    @abc.abstractmethod
 325    def log_success(self, message: str) -> None:
 326        """Display a general successful message to the user."""
 327
 328
 329class PlanBuilderConsole(BaseConsole, abc.ABC):
 330    @abc.abstractmethod
 331    def log_destructive_change(
 332        self,
 333        snapshot_name: str,
 334        alter_operations: t.List[TableAlterOperation],
 335        dialect: str,
 336        error: bool = True,
 337    ) -> None:
 338        """Display a destructive change error or warning to the user."""
 339
 340    @abc.abstractmethod
 341    def log_additive_change(
 342        self,
 343        snapshot_name: str,
 344        alter_operations: t.List[TableAlterOperation],
 345        dialect: str,
 346        error: bool = True,
 347    ) -> None:
 348        """Display an additive change error or warning to the user."""
 349
 350
 351class UnitTestConsole(abc.ABC):
 352    @abc.abstractmethod
 353    def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
 354        """Display the test result and output.
 355
 356        Args:
 357            result: The unittest test result that contains metrics like num success, fails, ect.
 358            target_dialect: The dialect that tests were run against. Assumes all tests run against the same dialect.
 359        """
 360
 361
 362class SignalConsole(abc.ABC):
 363    @abc.abstractmethod
 364    def start_signal_progress(
 365        self,
 366        snapshot: Snapshot,
 367        default_catalog: t.Optional[str],
 368        environment_naming_info: EnvironmentNamingInfo,
 369    ) -> None:
 370        """Indicates that signal checking has begun for a snapshot."""
 371
 372    @abc.abstractmethod
 373    def update_signal_progress(
 374        self,
 375        snapshot: Snapshot,
 376        signal_name: str,
 377        signal_idx: int,
 378        total_signals: int,
 379        ready_intervals: Intervals,
 380        check_intervals: Intervals,
 381        duration: float,
 382    ) -> None:
 383        """Updates the signal checking progress."""
 384
 385    @abc.abstractmethod
 386    def stop_signal_progress(self) -> None:
 387        """Indicates that signal checking has completed for a snapshot."""
 388
 389
 390class Console(
 391    SignalConsole,
 392    PlanBuilderConsole,
 393    LinterConsole,
 394    StateExporterConsole,
 395    StateImporterConsole,
 396    JanitorConsole,
 397    DestroyConsole,
 398    EnvironmentsConsole,
 399    DifferenceConsole,
 400    TableDiffConsole,
 401    BaseConsole,
 402    UnitTestConsole,
 403    abc.ABC,
 404):
 405    """Abstract base class for defining classes used for displaying information to the user and also interact
 406    with them when their input is needed."""
 407
 408    INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD = 10
 409
 410    @abc.abstractmethod
 411    def start_plan_evaluation(self, plan: EvaluatablePlan) -> None:
 412        """Indicates that a new evaluation has begun."""
 413
 414    @abc.abstractmethod
 415    def stop_plan_evaluation(self) -> None:
 416        """Indicates that the evaluation has ended."""
 417
 418    @abc.abstractmethod
 419    def start_evaluation_progress(
 420        self,
 421        batched_intervals: t.Dict[Snapshot, Intervals],
 422        environment_naming_info: EnvironmentNamingInfo,
 423        default_catalog: t.Optional[str],
 424        audit_only: bool = False,
 425    ) -> None:
 426        """Indicates that a new snapshot evaluation/auditing progress has begun."""
 427
 428    @abc.abstractmethod
 429    def start_snapshot_evaluation_progress(
 430        self, snapshot: Snapshot, audit_only: bool = False
 431    ) -> None:
 432        """Starts the snapshot evaluation progress."""
 433
 434    @abc.abstractmethod
 435    def update_snapshot_evaluation_progress(
 436        self,
 437        snapshot: Snapshot,
 438        interval: Interval,
 439        batch_idx: int,
 440        duration_ms: t.Optional[int],
 441        num_audits_passed: int,
 442        num_audits_failed: int,
 443        audit_only: bool = False,
 444        execution_stats: t.Optional[QueryExecutionStats] = None,
 445        auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None,
 446    ) -> None:
 447        """Updates the snapshot evaluation progress."""
 448
 449    @abc.abstractmethod
 450    def stop_evaluation_progress(self, success: bool = True) -> None:
 451        """Stops the snapshot evaluation progress."""
 452
 453    @abc.abstractmethod
 454    def start_creation_progress(
 455        self,
 456        snapshots: t.List[Snapshot],
 457        environment_naming_info: EnvironmentNamingInfo,
 458        default_catalog: t.Optional[str],
 459    ) -> None:
 460        """Indicates that a new snapshot creation progress has begun."""
 461
 462    @abc.abstractmethod
 463    def update_creation_progress(self, snapshot: SnapshotInfoLike) -> None:
 464        """Update the snapshot creation progress."""
 465
 466    @abc.abstractmethod
 467    def stop_creation_progress(self, success: bool = True) -> None:
 468        """Stop the snapshot creation progress."""
 469
 470    @abc.abstractmethod
 471    def start_promotion_progress(
 472        self,
 473        snapshots: t.List[SnapshotTableInfo],
 474        environment_naming_info: EnvironmentNamingInfo,
 475        default_catalog: t.Optional[str],
 476    ) -> None:
 477        """Indicates that a new snapshot promotion progress has begun."""
 478
 479    @abc.abstractmethod
 480    def update_promotion_progress(self, snapshot: SnapshotInfoLike, promoted: bool) -> None:
 481        """Update the snapshot promotion progress."""
 482
 483    @abc.abstractmethod
 484    def stop_promotion_progress(self, success: bool = True) -> None:
 485        """Stop the snapshot promotion progress."""
 486
 487    @abc.abstractmethod
 488    def start_snapshot_migration_progress(self, total_tasks: int) -> None:
 489        """Indicates that a new snapshot migration progress has begun."""
 490
 491    @abc.abstractmethod
 492    def update_snapshot_migration_progress(self, num_tasks: int) -> None:
 493        """Update the snapshot migration progress."""
 494
 495    @abc.abstractmethod
 496    def log_migration_status(self, success: bool = True) -> None:
 497        """Log the finished migration status."""
 498
 499    @abc.abstractmethod
 500    def stop_snapshot_migration_progress(self, success: bool = True) -> None:
 501        """Stop the snapshot migration progress."""
 502
 503    @abc.abstractmethod
 504    def start_env_migration_progress(self, total_tasks: int) -> None:
 505        """Indicates that a new environment migration progress has begun."""
 506
 507    @abc.abstractmethod
 508    def update_env_migration_progress(self, num_tasks: int) -> None:
 509        """Update the environment migration progress."""
 510
 511    @abc.abstractmethod
 512    def stop_env_migration_progress(self, success: bool = True) -> None:
 513        """Stop the environment migration progress."""
 514
 515    @abc.abstractmethod
 516    def plan(
 517        self,
 518        plan_builder: PlanBuilder,
 519        auto_apply: bool,
 520        default_catalog: t.Optional[str],
 521        no_diff: bool = False,
 522        no_prompts: bool = False,
 523    ) -> None:
 524        """The main plan flow.
 525
 526        The console should present the user with choices on how to backfill and version the snapshots
 527        of a plan.
 528
 529        Args:
 530            plan: The plan to make choices for.
 531            auto_apply: Whether to automatically apply the plan after all choices have been made.
 532            no_diff: Hide text differences for changed models.
 533            no_prompts: Whether to disable interactive prompts for the backfill time range. Please note that
 534                if this flag is set to true and there are uncategorized changes the plan creation will
 535                fail. Default: False
 536        """
 537
 538    @abc.abstractmethod
 539    def show_sql(self, sql: str) -> None:
 540        """Display to the user SQL."""
 541
 542    @abc.abstractmethod
 543    def log_status_update(self, message: str) -> None:
 544        """Display general status update to the user."""
 545
 546    @abc.abstractmethod
 547    def log_skipped_models(self, snapshot_names: t.Set[str]) -> None:
 548        """Display list of models skipped during evaluation to the user."""
 549
 550    @abc.abstractmethod
 551    def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None:
 552        """Display list of models that failed during evaluation to the user."""
 553
 554    @abc.abstractmethod
 555    def log_models_updated_during_restatement(
 556        self,
 557        snapshots: t.List[t.Tuple[SnapshotTableInfo, SnapshotTableInfo]],
 558        environment_naming_info: EnvironmentNamingInfo,
 559        default_catalog: t.Optional[str],
 560    ) -> None:
 561        """Display a list of models where new versions got deployed to the specified :environment while we were restating data the old versions
 562
 563        Args:
 564            snapshots: a list of (snapshot_we_restated, snapshot_it_got_replaced_with_during_restatement) tuples
 565            environment: which environment got updated while we were restating models
 566            environment_naming_info: how snapshots are named in that :environment (for display name purposes)
 567            default_catalog: the configured default catalog (for display name purposes)
 568        """
 569
 570    @abc.abstractmethod
 571    def loading_start(self, message: t.Optional[str] = None) -> uuid.UUID:
 572        """Starts loading and returns a unique ID that can be used to stop the loading. Optionally can display a message."""
 573
 574    @abc.abstractmethod
 575    def loading_stop(self, id: uuid.UUID) -> None:
 576        """Stop loading for the given id."""
 577
 578
 579class NoopConsole(Console):
 580    def start_plan_evaluation(self, plan: EvaluatablePlan) -> None:
 581        pass
 582
 583    def stop_plan_evaluation(self) -> None:
 584        pass
 585
 586    def start_evaluation_progress(
 587        self,
 588        batched_intervals: t.Dict[Snapshot, Intervals],
 589        environment_naming_info: EnvironmentNamingInfo,
 590        default_catalog: t.Optional[str],
 591        audit_only: bool = False,
 592    ) -> None:
 593        pass
 594
 595    def start_snapshot_evaluation_progress(
 596        self, snapshot: Snapshot, audit_only: bool = False
 597    ) -> None:
 598        pass
 599
 600    def update_snapshot_evaluation_progress(
 601        self,
 602        snapshot: Snapshot,
 603        interval: Interval,
 604        batch_idx: int,
 605        duration_ms: t.Optional[int],
 606        num_audits_passed: int,
 607        num_audits_failed: int,
 608        audit_only: bool = False,
 609        execution_stats: t.Optional[QueryExecutionStats] = None,
 610        auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None,
 611    ) -> None:
 612        pass
 613
 614    def stop_evaluation_progress(self, success: bool = True) -> None:
 615        pass
 616
 617    def start_signal_progress(
 618        self,
 619        snapshot: Snapshot,
 620        default_catalog: t.Optional[str],
 621        environment_naming_info: EnvironmentNamingInfo,
 622    ) -> None:
 623        pass
 624
 625    def update_signal_progress(
 626        self,
 627        snapshot: Snapshot,
 628        signal_name: str,
 629        signal_idx: int,
 630        total_signals: int,
 631        ready_intervals: Intervals,
 632        check_intervals: Intervals,
 633        duration: float,
 634    ) -> None:
 635        pass
 636
 637    def stop_signal_progress(self) -> None:
 638        pass
 639
 640    def start_creation_progress(
 641        self,
 642        snapshots: t.List[Snapshot],
 643        environment_naming_info: EnvironmentNamingInfo,
 644        default_catalog: t.Optional[str],
 645    ) -> None:
 646        pass
 647
 648    def update_creation_progress(self, snapshot: SnapshotInfoLike) -> None:
 649        pass
 650
 651    def stop_creation_progress(self, success: bool = True) -> None:
 652        pass
 653
 654    def start_cleanup(self, ignore_ttl: bool) -> bool:
 655        return True
 656
 657    def update_cleanup_progress(self, object_name: str) -> None:
 658        pass
 659
 660    def stop_cleanup(self, success: bool = True) -> None:
 661        pass
 662
 663    def start_promotion_progress(
 664        self,
 665        snapshots: t.List[SnapshotTableInfo],
 666        environment_naming_info: EnvironmentNamingInfo,
 667        default_catalog: t.Optional[str],
 668    ) -> None:
 669        pass
 670
 671    def update_promotion_progress(self, snapshot: SnapshotInfoLike, promoted: bool) -> None:
 672        pass
 673
 674    def stop_promotion_progress(self, success: bool = True) -> None:
 675        pass
 676
 677    def start_snapshot_migration_progress(self, total_tasks: int) -> None:
 678        pass
 679
 680    def update_snapshot_migration_progress(self, num_tasks: int) -> None:
 681        pass
 682
 683    def log_migration_status(self, success: bool = True) -> None:
 684        pass
 685
 686    def stop_snapshot_migration_progress(self, success: bool = True) -> None:
 687        pass
 688
 689    def start_env_migration_progress(self, total_tasks: int) -> None:
 690        pass
 691
 692    def update_env_migration_progress(self, num_tasks: int) -> None:
 693        pass
 694
 695    def stop_env_migration_progress(self, success: bool = True) -> None:
 696        pass
 697
 698    def start_state_export(
 699        self,
 700        output_file: Path,
 701        gateway: t.Optional[str] = None,
 702        state_connection_config: t.Optional[ConnectionConfig] = None,
 703        environment_names: t.Optional[t.List[str]] = None,
 704        local_only: bool = False,
 705        confirm: bool = True,
 706    ) -> bool:
 707        return confirm
 708
 709    def update_state_export_progress(
 710        self,
 711        version_count: t.Optional[int] = None,
 712        versions_complete: bool = False,
 713        snapshot_count: t.Optional[int] = None,
 714        snapshots_complete: bool = False,
 715        environment_count: t.Optional[int] = None,
 716        environments_complete: bool = False,
 717    ) -> None:
 718        pass
 719
 720    def stop_state_export(self, success: bool, output_file: Path) -> None:
 721        pass
 722
 723    def start_state_import(
 724        self,
 725        input_file: Path,
 726        gateway: str,
 727        state_connection_config: ConnectionConfig,
 728        clear: bool = False,
 729        confirm: bool = True,
 730    ) -> bool:
 731        return confirm
 732
 733    def update_state_import_progress(
 734        self,
 735        timestamp: t.Optional[str] = None,
 736        state_file_version: t.Optional[int] = None,
 737        versions: t.Optional[Versions] = None,
 738        snapshot_count: t.Optional[int] = None,
 739        snapshots_complete: bool = False,
 740        environment_count: t.Optional[int] = None,
 741        environments_complete: bool = False,
 742    ) -> None:
 743        pass
 744
 745    def stop_state_import(self, success: bool, input_file: Path) -> None:
 746        pass
 747
 748    def show_environment_difference_summary(
 749        self,
 750        context_diff: ContextDiff,
 751        no_diff: bool = True,
 752    ) -> None:
 753        pass
 754
 755    def show_model_difference_summary(
 756        self,
 757        context_diff: ContextDiff,
 758        environment_naming_info: EnvironmentNamingInfo,
 759        default_catalog: t.Optional[str],
 760        no_diff: bool = True,
 761    ) -> None:
 762        pass
 763
 764    def plan(
 765        self,
 766        plan_builder: PlanBuilder,
 767        auto_apply: bool,
 768        default_catalog: t.Optional[str],
 769        no_diff: bool = False,
 770        no_prompts: bool = False,
 771    ) -> None:
 772        if auto_apply:
 773            plan_builder.apply()
 774
 775    def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
 776        pass
 777
 778    def show_sql(self, sql: str) -> None:
 779        pass
 780
 781    def log_status_update(self, message: str) -> None:
 782        pass
 783
 784    def log_skipped_models(self, snapshot_names: t.Set[str]) -> None:
 785        pass
 786
 787    def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None:
 788        pass
 789
 790    def log_models_updated_during_restatement(
 791        self,
 792        snapshots: t.List[t.Tuple[SnapshotTableInfo, SnapshotTableInfo]],
 793        environment_naming_info: EnvironmentNamingInfo,
 794        default_catalog: t.Optional[str],
 795    ) -> None:
 796        pass
 797
 798    def log_destructive_change(
 799        self,
 800        snapshot_name: str,
 801        alter_operations: t.List[TableAlterOperation],
 802        dialect: str,
 803        error: bool = True,
 804    ) -> None:
 805        pass
 806
 807    def log_additive_change(
 808        self,
 809        snapshot_name: str,
 810        alter_operations: t.List[TableAlterOperation],
 811        dialect: str,
 812        error: bool = True,
 813    ) -> None:
 814        pass
 815
 816    def log_error(self, message: str) -> None:
 817        pass
 818
 819    def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None:
 820        logger.warning(long_message or short_message)
 821
 822    def log_success(self, message: str) -> None:
 823        pass
 824
 825    def loading_start(self, message: t.Optional[str] = None) -> uuid.UUID:
 826        return uuid.uuid4()
 827
 828    def loading_stop(self, id: uuid.UUID) -> None:
 829        pass
 830
 831    def show_table_diff(
 832        self,
 833        table_diffs: t.List[TableDiff],
 834        show_sample: bool = True,
 835        skip_grain_check: bool = False,
 836        temp_schema: t.Optional[str] = None,
 837    ) -> None:
 838        for table_diff in table_diffs:
 839            self.show_table_diff_summary(table_diff)
 840            self.show_schema_diff(table_diff.schema_diff())
 841            self.show_row_diff(
 842                table_diff.row_diff(temp_schema=temp_schema, skip_grain_check=skip_grain_check),
 843                show_sample=show_sample,
 844                skip_grain_check=skip_grain_check,
 845            )
 846
 847    def update_table_diff_progress(self, model: str) -> None:
 848        pass
 849
 850    def start_table_diff_progress(self, models_to_diff: int) -> None:
 851        pass
 852
 853    def start_table_diff_model_progress(self, model: str) -> None:
 854        pass
 855
 856    def stop_table_diff_progress(self, success: bool) -> None:
 857        pass
 858
 859    def show_table_diff_details(
 860        self,
 861        models_to_diff: t.List[str],
 862    ) -> None:
 863        pass
 864
 865    def show_table_diff_summary(self, table_diff: TableDiff) -> None:
 866        pass
 867
 868    def show_schema_diff(self, schema_diff: SchemaDiff) -> None:
 869        pass
 870
 871    def show_row_diff(
 872        self, row_diff: RowDiff, show_sample: bool = True, skip_grain_check: bool = False
 873    ) -> None:
 874        pass
 875
 876    def print_environments(self, environments_summary: t.List[EnvironmentSummary]) -> None:
 877        pass
 878
 879    def show_intervals(self, snapshot_intervals: t.Dict[Snapshot, SnapshotIntervals]) -> None:
 880        pass
 881
 882    def show_linter_violations(
 883        self, violations: t.List[RuleViolation], model: Model, is_error: bool = False
 884    ) -> None:
 885        pass
 886
 887    def print_connection_config(
 888        self, config: ConnectionConfig, title: t.Optional[str] = "Connection"
 889    ) -> None:
 890        pass
 891
 892    def start_destroy(
 893        self,
 894        schemas_to_delete: t.Optional[t.Set[str]] = None,
 895        views_to_delete: t.Optional[t.Set[str]] = None,
 896        tables_to_delete: t.Optional[t.Set[str]] = None,
 897    ) -> bool:
 898        return True
 899
 900    def stop_destroy(self, success: bool = True) -> None:
 901        pass
 902
 903
 904def make_progress_bar(
 905    message: str,
 906    console: t.Optional[RichConsole] = None,
 907    justify: t.Literal["default", "left", "center", "right", "full"] = "right",
 908) -> Progress:
 909    return Progress(
 910        TextColumn(f"[bold blue]{message}", justify=justify),
 911        BarColumn(bar_width=PROGRESS_BAR_WIDTH),
 912        "[progress.percentage]{task.percentage:>3.1f}%",
 913        "•",
 914        srich.BatchColumn(),
 915        "•",
 916        TimeElapsedColumn(),
 917        console=console,
 918    )
 919
 920
 921class TerminalConsole(Console):
 922    """A rich based implementation of the console."""
 923
 924    TABLE_DIFF_SOURCE_BLUE = "#0248ff"
 925    TABLE_DIFF_TARGET_GREEN = "green"
 926    AUDIT_PASS_MARK = "\u2714"
 927    GREEN_AUDIT_PASS_MARK = f"[green]{AUDIT_PASS_MARK}[/green]"
 928    AUDIT_FAIL_MARK = "\u274c"
 929    AUDIT_PADDING = 0
 930    CHECK_MARK = f"{AUDIT_PASS_MARK} "
 931
 932    def __init__(
 933        self,
 934        console: t.Optional[RichConsole] = None,
 935        verbosity: Verbosity = Verbosity.DEFAULT,
 936        dialect: DialectType = None,
 937        ignore_warnings: bool = False,
 938        **kwargs: t.Any,
 939    ) -> None:
 940        self.console: RichConsole = console or srich.console
 941
 942        self.evaluation_progress_live: t.Optional[Live] = None
 943        self.evaluation_total_progress: t.Optional[Progress] = None
 944        self.evaluation_total_task: t.Optional[TaskID] = None
 945        self.evaluation_model_progress: t.Optional[Progress] = None
 946        self.evaluation_model_tasks: t.Dict[str, TaskID] = {}
 947        self.evaluation_model_batch_sizes: t.Dict[Snapshot, int] = {}
 948        self.evaluation_column_widths: t.Dict[str, int] = {}
 949
 950        # Put in temporary values that are replaced when evaluating
 951        self.environment_naming_info = EnvironmentNamingInfo()
 952        self.default_catalog: t.Optional[str] = None
 953
 954        self.creation_progress: t.Optional[Progress] = None
 955        self.creation_column_widths: t.Dict[str, int] = {}
 956        self.creation_task: t.Optional[TaskID] = None
 957
 958        self.promotion_progress: t.Optional[Progress] = None
 959        self.promotion_column_widths: t.Dict[str, int] = {}
 960        self.promotion_task: t.Optional[TaskID] = None
 961
 962        self.migration_progress: t.Optional[Progress] = None
 963        self.migration_task: t.Optional[TaskID] = None
 964
 965        self.env_migration_progress: t.Optional[Progress] = None
 966        self.env_migration_task: t.Optional[TaskID] = None
 967
 968        self.loading_status: t.Dict[uuid.UUID, Status] = {}
 969
 970        self.state_export_progress: t.Optional[Progress] = None
 971        self.state_export_version_task: t.Optional[TaskID] = None
 972        self.state_export_snapshot_task: t.Optional[TaskID] = None
 973        self.state_export_environment_task: t.Optional[TaskID] = None
 974
 975        self.state_import_progress: t.Optional[Progress] = None
 976        self.state_import_version_task: t.Optional[TaskID] = None
 977        self.state_import_snapshot_task: t.Optional[TaskID] = None
 978        self.state_import_environment_task: t.Optional[TaskID] = None
 979
 980        self.table_diff_progress: t.Optional[Progress] = None
 981        self.table_diff_model_progress: t.Optional[Progress] = None
 982        self.table_diff_model_tasks: t.Dict[str, TaskID] = {}
 983        self.table_diff_progress_live: t.Optional[Live] = None
 984
 985        self.signal_progress_logged = False
 986        self.signal_status_tree: t.Optional[Tree] = None
 987
 988        self.verbosity = verbosity
 989        self.dialect = dialect
 990        self.ignore_warnings = ignore_warnings
 991
 992    def _limit_model_names(self, tree: Tree, verbosity: Verbosity = Verbosity.DEFAULT) -> Tree:
 993        """Trim long indirectly modified model lists below threshold."""
 994        modified_length = len(tree.children)
 995        if (
 996            verbosity < Verbosity.VERY_VERBOSE
 997            and modified_length > self.INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD
 998        ):
 999            tree.children = [
1000                tree.children[0],
1001                Tree(f".... {modified_length - 2} more ...."),
1002                tree.children[-1],
1003            ]
1004        return tree
1005
1006    def _print(self, value: t.Any, **kwargs: t.Any) -> None:
1007        self.console.print(value, **kwargs)
1008
1009    def _prompt(self, message: str, **kwargs: t.Any) -> t.Any:
1010        return Prompt.ask(message, console=self.console, **kwargs)
1011
1012    def _confirm(self, message: str, **kwargs: t.Any) -> bool:
1013        return Confirm.ask(message, console=self.console, **kwargs)
1014
1015    def start_plan_evaluation(self, plan: EvaluatablePlan) -> None:
1016        pass
1017
1018    def stop_plan_evaluation(self) -> None:
1019        pass
1020
1021    def start_evaluation_progress(
1022        self,
1023        batched_intervals: t.Dict[Snapshot, Intervals],
1024        environment_naming_info: EnvironmentNamingInfo,
1025        default_catalog: t.Optional[str],
1026        audit_only: bool = False,
1027    ) -> None:
1028        """Indicates that a new snapshot evaluation/auditing progress has begun."""
1029        # Add a newline to separate signal checking from evaluation
1030        if self.signal_progress_logged:
1031            self._print("")
1032
1033        if not self.evaluation_progress_live:
1034            self.evaluation_total_progress = make_progress_bar(
1035                "Executing model batches" if not audit_only else "Auditing models", self.console
1036            )
1037
1038            self.evaluation_model_progress = Progress(
1039                TextColumn("{task.fields[view_name]}", justify="right"),
1040                SpinnerColumn(spinner_name="simpleDots"),
1041                console=self.console,
1042            )
1043
1044            progress_table = Table.grid()
1045            progress_table.add_row(self.evaluation_total_progress)
1046            progress_table.add_row(self.evaluation_model_progress)
1047
1048            self.evaluation_progress_live = Live(
1049                progress_table, console=self.console, refresh_per_second=10
1050            )
1051            self.evaluation_progress_live.start()
1052
1053            batch_sizes = {
1054                snapshot: len(intervals) for snapshot, intervals in batched_intervals.items()
1055            }
1056            message = "Executing" if not audit_only else "Auditing"
1057            self.evaluation_total_task = self.evaluation_total_progress.add_task(
1058                f"{message} models...", total=sum(batch_sizes.values())
1059            )
1060
1061            # determine column widths
1062            self.evaluation_column_widths["annotation"] = (
1063                _calculate_annotation_str_len(
1064                    batched_intervals, self.AUDIT_PADDING, len(" (123.4m rows, 123.4 KiB)")
1065                )
1066                + 3  # brackets and opening escape backslash
1067            )
1068            self.evaluation_column_widths["name"] = max(
1069                len(
1070                    snapshot.display_name(
1071                        environment_naming_info,
1072                        default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1073                        dialect=self.dialect,
1074                    )
1075                )
1076                for snapshot in batched_intervals
1077            )
1078            largest_batch_size = max(batch_sizes.values())
1079            self.evaluation_column_widths["batch"] = len(str(largest_batch_size)) * 2 + 3  # [X/X]
1080            self.evaluation_column_widths["duration"] = 8
1081
1082            self.evaluation_model_batch_sizes = batch_sizes
1083            self.environment_naming_info = environment_naming_info
1084            self.default_catalog = default_catalog
1085
1086    def start_snapshot_evaluation_progress(
1087        self, snapshot: Snapshot, audit_only: bool = False
1088    ) -> None:
1089        if self.evaluation_model_progress and snapshot.name not in self.evaluation_model_tasks:
1090            display_name = snapshot.display_name(
1091                self.environment_naming_info,
1092                self.default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1093                dialect=self.dialect,
1094            )
1095            self.evaluation_model_tasks[snapshot.name] = self.evaluation_model_progress.add_task(
1096                f"{'Evaluating' if not audit_only else 'Auditing'} {display_name}...",
1097                view_name=display_name,
1098                total=self.evaluation_model_batch_sizes[snapshot],
1099            )
1100
1101    def update_snapshot_evaluation_progress(
1102        self,
1103        snapshot: Snapshot,
1104        interval: Interval,
1105        batch_idx: int,
1106        duration_ms: t.Optional[int],
1107        num_audits_passed: int,
1108        num_audits_failed: int,
1109        audit_only: bool = False,
1110        execution_stats: t.Optional[QueryExecutionStats] = None,
1111        auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None,
1112    ) -> None:
1113        """Update the snapshot evaluation progress."""
1114        if (
1115            self.evaluation_total_progress
1116            and self.evaluation_model_progress
1117            and self.evaluation_progress_live
1118        ):
1119            total_batches = self.evaluation_model_batch_sizes[snapshot]
1120            batch_num = str(batch_idx + 1).rjust(len(str(total_batches)))
1121            batch = f"[{batch_num}/{total_batches}]".ljust(self.evaluation_column_widths["batch"])
1122
1123            if duration_ms:
1124                display_name = snapshot.display_name(
1125                    self.environment_naming_info,
1126                    self.default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1127                    dialect=self.dialect,
1128                ).ljust(self.evaluation_column_widths["name"])
1129
1130                annotation = _create_evaluation_model_annotation(
1131                    snapshot, _format_evaluation_model_interval(snapshot, interval), execution_stats
1132                )
1133                audits_str = ""
1134                if num_audits_passed:
1135                    audits_str += f" {self.AUDIT_PASS_MARK}{num_audits_passed}"
1136                if num_audits_failed:
1137                    audits_str += f" {self.AUDIT_FAIL_MARK}{num_audits_failed}"
1138                audits_str = f", audits{audits_str}" if audits_str else ""
1139                annotation_len = self.evaluation_column_widths["annotation"]
1140                # don't adjust the annotation_len if we're using AUDIT_PADDING
1141                annotation = f"\\[{annotation + audits_str}]".ljust(
1142                    annotation_len - 1
1143                    if num_audits_failed and self.AUDIT_PADDING == 0
1144                    else annotation_len
1145                )
1146
1147                duration = f"{(duration_ms / 1000.0):.2f}s".ljust(
1148                    self.evaluation_column_widths["duration"]
1149                )
1150
1151                msg = f"{f'{batch} ' if not audit_only else ''}{display_name}   {annotation}   {duration}".replace(
1152                    self.AUDIT_PASS_MARK, self.GREEN_AUDIT_PASS_MARK
1153                )
1154
1155                self.evaluation_progress_live.console.print(msg)
1156
1157            self.evaluation_total_progress.update(
1158                self.evaluation_total_task or TaskID(0), refresh=True, advance=1
1159            )
1160
1161            model_task_id = self.evaluation_model_tasks[snapshot.name]
1162            self.evaluation_model_progress.update(model_task_id, refresh=True, advance=1)
1163            if (
1164                self.evaluation_model_progress._tasks[model_task_id].completed >= total_batches
1165                or audit_only
1166            ):
1167                self.evaluation_model_progress.remove_task(model_task_id)
1168
1169    def stop_evaluation_progress(self, success: bool = True) -> None:
1170        """Stop the snapshot evaluation progress."""
1171        if self.evaluation_progress_live:
1172            self.evaluation_progress_live.stop()
1173            if success:
1174                self.log_success(f"{self.CHECK_MARK}Model batches executed")
1175
1176        self.evaluation_progress_live = None
1177        self.evaluation_total_progress = None
1178        self.evaluation_total_task = None
1179        self.evaluation_model_progress = None
1180        self.evaluation_model_tasks = {}
1181        self.evaluation_model_batch_sizes = {}
1182        self.evaluation_column_widths = {}
1183        self.environment_naming_info = EnvironmentNamingInfo()
1184        self.default_catalog = None
1185
1186    def start_signal_progress(
1187        self,
1188        snapshot: Snapshot,
1189        default_catalog: t.Optional[str],
1190        environment_naming_info: EnvironmentNamingInfo,
1191    ) -> None:
1192        """Indicates that signal checking has begun for a snapshot."""
1193        display_name = snapshot.display_name(
1194            environment_naming_info,
1195            default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1196            dialect=self.dialect,
1197        )
1198        self.signal_status_tree = Tree(f"Checking signals for {display_name}")
1199
1200    def update_signal_progress(
1201        self,
1202        snapshot: Snapshot,
1203        signal_name: str,
1204        signal_idx: int,
1205        total_signals: int,
1206        ready_intervals: Intervals,
1207        check_intervals: Intervals,
1208        duration: float,
1209    ) -> None:
1210        """Updates the signal checking progress."""
1211        tree = Tree(f"[{signal_idx + 1}/{total_signals}] {signal_name} {duration:.2f}s")
1212
1213        formatted_check_intervals = [_format_signal_interval(snapshot, i) for i in check_intervals]
1214        formatted_ready_intervals = [_format_signal_interval(snapshot, i) for i in ready_intervals]
1215
1216        if not formatted_check_intervals:
1217            formatted_check_intervals = ["no intervals"]
1218        if not formatted_ready_intervals:
1219            formatted_ready_intervals = ["no intervals"]
1220
1221        # Color coding to help detect partial interval ranges quickly
1222        if ready_intervals == check_intervals:
1223            msg = "All ready"
1224            color = "green"
1225        elif ready_intervals:
1226            msg = "Some ready"
1227            color = "yellow"
1228        else:
1229            msg = "None ready"
1230            color = "red"
1231
1232        if self.verbosity < Verbosity.VERY_VERBOSE:
1233            num_check_intervals = len(formatted_check_intervals)
1234            if num_check_intervals > 3:
1235                formatted_check_intervals = formatted_check_intervals[:3]
1236                formatted_check_intervals.append(f"... and {num_check_intervals - 3} more")
1237
1238            num_ready_intervals = len(formatted_ready_intervals)
1239            if num_ready_intervals > 3:
1240                formatted_ready_intervals = formatted_ready_intervals[:3]
1241                formatted_ready_intervals.append(f"... and {num_ready_intervals - 3} more")
1242
1243            check = ", ".join(formatted_check_intervals)
1244            tree.add(f"Check: {check}")
1245
1246            ready = ", ".join(formatted_ready_intervals)
1247            tree.add(f"[{color}]{msg}: {ready}[/{color}]")
1248        else:
1249            check_tree = Tree("Check")
1250            tree.add(check_tree)
1251            for interval in formatted_check_intervals:
1252                check_tree.add(interval)
1253
1254            ready_tree = Tree(f"[{color}]{msg}[/{color}]")
1255            tree.add(ready_tree)
1256            for interval in formatted_ready_intervals:
1257                ready_tree.add(f"[{color}]{interval}[/{color}]")
1258
1259        if self.signal_status_tree is not None:
1260            self.signal_status_tree.add(tree)
1261
1262    def stop_signal_progress(self) -> None:
1263        """Indicates that signal checking has completed for a snapshot."""
1264        if self.signal_status_tree is not None:
1265            self._print(self.signal_status_tree)
1266            self.signal_status_tree = None
1267            self.signal_progress_logged = True
1268
1269    def start_creation_progress(
1270        self,
1271        snapshots: t.List[Snapshot],
1272        environment_naming_info: EnvironmentNamingInfo,
1273        default_catalog: t.Optional[str],
1274    ) -> None:
1275        """Indicates that a new creation progress has begun."""
1276        if self.creation_progress is None:
1277            self.creation_progress = make_progress_bar("Updating physical layer", self.console)
1278
1279            self._print("")
1280            self.creation_progress.start()
1281            self.creation_task = self.creation_progress.add_task(
1282                "Updating physical layer...",
1283                total=len(snapshots),
1284            )
1285
1286            # determine name column widths if we're printing name
1287            if self.verbosity >= Verbosity.VERBOSE:
1288                self.creation_column_widths["name"] = max(
1289                    len(
1290                        snapshot.display_name(
1291                            environment_naming_info,
1292                            default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1293                            dialect=self.dialect,
1294                        )
1295                    )
1296                    for snapshot in snapshots
1297                )
1298
1299            self.environment_naming_info = environment_naming_info
1300            self.default_catalog = default_catalog
1301
1302    def update_creation_progress(self, snapshot: SnapshotInfoLike) -> None:
1303        """Update the snapshot creation progress."""
1304        if self.creation_progress is not None and self.creation_task is not None:
1305            if self.verbosity >= Verbosity.VERBOSE:
1306                msg = snapshot.display_name(
1307                    self.environment_naming_info,
1308                    self.default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1309                    dialect=self.dialect,
1310                ).ljust(self.creation_column_widths["name"])
1311                self.creation_progress.live.console.print(msg + "  [green]created[/green]")
1312            self.creation_progress.update(self.creation_task, refresh=True, advance=1)
1313
1314    def stop_creation_progress(self, success: bool = True) -> None:
1315        """Stop the snapshot creation progress."""
1316        self.creation_task = None
1317        if self.creation_progress is not None:
1318            self.creation_progress.stop()
1319            self.creation_progress = None
1320            if success:
1321                self.log_success(f"\n{self.CHECK_MARK}Physical layer updated")
1322
1323        self.environment_naming_info = EnvironmentNamingInfo()
1324        self.default_catalog = None
1325        self.creation_column_widths = {}
1326
1327    def start_cleanup(self, ignore_ttl: bool) -> bool:
1328        if ignore_ttl:
1329            self._print(
1330                "Are you sure you want to delete all snapshots that are not referenced in any environment?"
1331            )
1332            self._print(
1333                "Note that this may cause a race condition if there are any concurrently running plans."
1334            )
1335            self._print(
1336                "It may also confuse users who were expecting to be able to rollback changes in their development environments."
1337            )
1338            if not self._confirm("Proceed?"):
1339                self.log_error("Cleanup aborted")
1340                return False
1341        return True
1342
1343    def update_cleanup_progress(self, object_name: str) -> None:
1344        """Update the snapshot cleanup progress."""
1345        self._print(f"Deleted object {object_name}")
1346
1347    def stop_cleanup(self, success: bool = False) -> None:
1348        if success:
1349            self.log_success("Cleanup complete.")
1350        else:
1351            self.log_error("Cleanup failed!")
1352
1353    def start_destroy(
1354        self,
1355        schemas_to_delete: t.Optional[t.Set[str]] = None,
1356        views_to_delete: t.Optional[t.Set[str]] = None,
1357        tables_to_delete: t.Optional[t.Set[str]] = None,
1358    ) -> bool:
1359        self.log_warning(
1360            "This will permanently delete all engine-managed objects, state tables and SQLMesh cache.\n"
1361            "The operation may disrupt any currently running or scheduled plans.\n"
1362        )
1363
1364        if schemas_to_delete or views_to_delete or tables_to_delete:
1365            if schemas_to_delete:
1366                self.log_error("Schemas to be deleted:")
1367                for schema in sorted(schemas_to_delete):
1368                    self.log_error(f"  • {schema}")
1369
1370            if views_to_delete:
1371                self.log_error("\nEnvironment views to be deleted:")
1372                for view in sorted(views_to_delete):
1373                    self.log_error(f"  • {view}")
1374
1375            if tables_to_delete:
1376                self.log_error("\nSnapshot tables to be deleted:")
1377                for table in sorted(tables_to_delete):
1378                    self.log_error(f"  • {table}")
1379
1380            self.log_error(
1381                "\nThis action will DELETE ALL the above resources managed by SQLMesh AND\n"
1382                "potentially external resources created by other tools in these schemas.\n"
1383            )
1384
1385        if not self._confirm("Are you ABSOLUTELY SURE you want to proceed with deletion?"):
1386            self.log_error("Destroy operation cancelled.")
1387            return False
1388        return True
1389
1390    def stop_destroy(self, success: bool = False) -> None:
1391        if success:
1392            self.log_success("Destroy completed successfully.")
1393        else:
1394            self.log_error("Destroy failed!")
1395
1396    def start_promotion_progress(
1397        self,
1398        snapshots: t.List[SnapshotTableInfo],
1399        environment_naming_info: EnvironmentNamingInfo,
1400        default_catalog: t.Optional[str],
1401    ) -> None:
1402        """Indicates that a new snapshot promotion progress has begun."""
1403        if snapshots and self.promotion_progress is None:
1404            self.promotion_progress = make_progress_bar(
1405                "Updating virtual layer ", self.console, justify="left"
1406            )
1407
1408            snapshots_with_virtual_views = [
1409                s for s in snapshots if s.is_model and not s.is_symbolic
1410            ]
1411            self.promotion_progress.start()
1412            self.promotion_task = self.promotion_progress.add_task(
1413                f"Virtually updating {environment_naming_info.name}...",
1414                total=len(snapshots_with_virtual_views),
1415            )
1416
1417            # determine name column widths if we're printing names
1418            if self.verbosity >= Verbosity.VERBOSE:
1419                self.promotion_column_widths["name"] = max(
1420                    len(
1421                        snapshot.display_name(
1422                            environment_naming_info,
1423                            default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1424                            dialect=self.dialect,
1425                        )
1426                    )
1427                    for snapshot in snapshots_with_virtual_views
1428                )
1429
1430            self.environment_naming_info = environment_naming_info
1431            self.default_catalog = default_catalog
1432
1433    def update_promotion_progress(self, snapshot: SnapshotInfoLike, promoted: bool) -> None:
1434        """Update the snapshot promotion progress."""
1435        if (
1436            self.promotion_progress is not None
1437            and self.promotion_task is not None
1438            and snapshot.is_model
1439            and not snapshot.is_symbolic
1440        ):
1441            if self.verbosity >= Verbosity.VERBOSE:
1442                display_name = snapshot.display_name(
1443                    self.environment_naming_info,
1444                    self.default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1445                    dialect=self.dialect,
1446                ).ljust(self.promotion_column_widths["name"])
1447                action_str = ""
1448                if promoted:
1449                    action_str = (
1450                        "[yellow]updated[/yellow]"
1451                        if snapshot.previous_version
1452                        else "[green]created[/green]"
1453                    )
1454                action_str = action_str or "[red]dropped[/red]"
1455                self.promotion_progress.live.console.print(f"{display_name}  {action_str}")
1456            self.promotion_progress.update(self.promotion_task, refresh=True, advance=1)
1457
1458    def stop_promotion_progress(self, success: bool = True) -> None:
1459        """Stop the snapshot promotion progress."""
1460        self.promotion_task = None
1461        if self.promotion_progress is not None:
1462            self.promotion_progress.stop()
1463            self.promotion_progress = None
1464            if success:
1465                self.log_success(f"\n{self.CHECK_MARK}Virtual layer updated")
1466
1467        self.environment_naming_info = EnvironmentNamingInfo()
1468        self.default_catalog = None
1469        self.promotion_column_widths = {}
1470
1471    def start_snapshot_migration_progress(self, total_tasks: int) -> None:
1472        """Indicates that a new snapshot migration progress has begun."""
1473        if self.migration_progress is None:
1474            self.migration_progress = make_progress_bar("Migrating snapshots", self.console)
1475
1476            self.migration_progress.start()
1477            self.migration_task = self.migration_progress.add_task(
1478                "Migrating snapshots...",
1479                total=total_tasks,
1480            )
1481
1482    def update_snapshot_migration_progress(self, num_tasks: int) -> None:
1483        """Update the migration progress."""
1484        if self.migration_progress is not None and self.migration_task is not None:
1485            self.migration_progress.update(self.migration_task, refresh=True, advance=num_tasks)
1486
1487    def log_migration_status(self, success: bool = True) -> None:
1488        """Log the migration status."""
1489        if self.migration_progress is not None:
1490            self.migration_progress = None
1491            if success:
1492                self.log_success("Migration completed successfully")
1493
1494    def stop_snapshot_migration_progress(self, success: bool = True) -> None:
1495        """Stop the migration progress."""
1496        self.migration_task = None
1497        if self.migration_progress is not None:
1498            self.migration_progress.stop()
1499            if success:
1500                self.log_success("Snapshots migrated successfully")
1501
1502    def start_env_migration_progress(self, total_tasks: int) -> None:
1503        """Indicates that a new environment migration has begun."""
1504        if self.env_migration_progress is None:
1505            self.env_migration_progress = make_progress_bar("Migrating environments", self.console)
1506            self.env_migration_progress.start()
1507            self.env_migration_task = self.env_migration_progress.add_task(
1508                "Migrating environments...",
1509                total=total_tasks,
1510            )
1511
1512    def update_env_migration_progress(self, num_tasks: int) -> None:
1513        """Update the environment migration progress."""
1514        if self.env_migration_progress is not None and self.env_migration_task is not None:
1515            self.env_migration_progress.update(
1516                self.env_migration_task, refresh=True, advance=num_tasks
1517            )
1518
1519    def stop_env_migration_progress(self, success: bool = True) -> None:
1520        """Stop the environment migration progress."""
1521        self.env_migration_task = None
1522        if self.env_migration_progress is not None:
1523            self.env_migration_progress.stop()
1524            self.env_migration_progress = None
1525            if success:
1526                self.log_success("Environments migrated successfully")
1527
1528    def start_state_export(
1529        self,
1530        output_file: Path,
1531        gateway: t.Optional[str] = None,
1532        state_connection_config: t.Optional[ConnectionConfig] = None,
1533        environment_names: t.Optional[t.List[str]] = None,
1534        local_only: bool = False,
1535        confirm: bool = True,
1536    ) -> bool:
1537        self.state_export_progress = None
1538
1539        if local_only:
1540            self.log_status_update(f"Exporting [b]local[/b] state to '{output_file.as_posix()}'\n")
1541            self.log_warning(
1542                "Local state exports just contain the model versions in your local context. Therefore, the resulting file cannot be imported."
1543            )
1544        else:
1545            self.log_status_update(
1546                f"Exporting state to '{output_file.as_posix()}' from the following connection:\n"
1547            )
1548            if gateway:
1549                self.log_status_update(f"[b]Gateway[/b]: [green]{gateway}[/green]")
1550            if state_connection_config:
1551                self.print_connection_config(state_connection_config, title="State Connection")
1552            if environment_names:
1553                heading = "Environments" if len(environment_names) > 1 else "Environment"
1554                self.log_status_update(
1555                    f"[b]{heading}[/b]: [yellow]{', '.join(environment_names)}[/yellow]"
1556                )
1557
1558        should_continue = True
1559        if confirm:
1560            should_continue = self._confirm("\nContinue?")
1561            self.log_status_update("")
1562
1563        if should_continue:
1564            self.state_export_progress = make_progress_bar("{task.description}", self.console)
1565            assert isinstance(self.state_export_progress, Progress)
1566
1567            self.state_export_version_task = self.state_export_progress.add_task(
1568                "Exporting versions", start=False
1569            )
1570            self.state_export_snapshot_task = self.state_export_progress.add_task(
1571                "Exporting snapshots", start=False
1572            )
1573            self.state_export_environment_task = self.state_export_progress.add_task(
1574                "Exporting environments", start=False
1575            )
1576
1577            self.state_export_progress.start()
1578
1579        return should_continue
1580
1581    def update_state_export_progress(
1582        self,
1583        version_count: t.Optional[int] = None,
1584        versions_complete: bool = False,
1585        snapshot_count: t.Optional[int] = None,
1586        snapshots_complete: bool = False,
1587        environment_count: t.Optional[int] = None,
1588        environments_complete: bool = False,
1589    ) -> None:
1590        if self.state_export_progress:
1591            if self.state_export_version_task is not None:
1592                if version_count is not None:
1593                    self.state_export_progress.start_task(self.state_export_version_task)
1594                    self.state_export_progress.update(
1595                        self.state_export_version_task,
1596                        total=version_count,
1597                        completed=version_count,
1598                        refresh=True,
1599                    )
1600                if versions_complete:
1601                    self.state_export_progress.stop_task(self.state_export_version_task)
1602
1603            if self.state_export_snapshot_task is not None:
1604                if snapshot_count is not None:
1605                    self.state_export_progress.start_task(self.state_export_snapshot_task)
1606                    self.state_export_progress.update(
1607                        self.state_export_snapshot_task,
1608                        total=snapshot_count,
1609                        completed=snapshot_count,
1610                        refresh=True,
1611                    )
1612                if snapshots_complete:
1613                    self.state_export_progress.stop_task(self.state_export_snapshot_task)
1614
1615            if self.state_export_environment_task is not None:
1616                if environment_count is not None:
1617                    self.state_export_progress.start_task(self.state_export_environment_task)
1618                    self.state_export_progress.update(
1619                        self.state_export_environment_task,
1620                        total=environment_count,
1621                        completed=environment_count,
1622                        refresh=True,
1623                    )
1624                if environments_complete:
1625                    self.state_export_progress.stop_task(self.state_export_environment_task)
1626
1627    def stop_state_export(self, success: bool, output_file: Path) -> None:
1628        if self.state_export_progress:
1629            self.state_export_progress.stop()
1630            self.state_export_progress = None
1631
1632            self.log_status_update("")
1633
1634            if success:
1635                self.log_success(f"State exported successfully to '{output_file.as_posix()}'")
1636            else:
1637                self.log_error("State export failed!")
1638
1639    def start_state_import(
1640        self,
1641        input_file: Path,
1642        gateway: str,
1643        state_connection_config: ConnectionConfig,
1644        clear: bool = False,
1645        confirm: bool = True,
1646    ) -> bool:
1647        self.log_status_update(
1648            f"Loading state from '{input_file.as_posix()}' into the following connection:\n"
1649        )
1650        self.log_status_update(f"[b]Gateway[/b]: [green]{gateway}[/green]")
1651        self.print_connection_config(state_connection_config, title="State Connection")
1652        self.log_status_update("")
1653
1654        if clear:
1655            self.log_warning(
1656                f"This [b]destructive[/b] operation will delete all existing state against the '{gateway}' gateway \n"
1657                f"and replace it with what's in the '{input_file.as_posix()}' file.\n"
1658            )
1659        else:
1660            self.log_warning(
1661                f"This operation will [b]merge[/b] the contents of the state file to the state located at the '{gateway}' gateway.\n"
1662                "Matching snapshots or environments will be replaced.\n"
1663                "Non-matching snapshots or environments will be ignored.\n"
1664            )
1665
1666        should_continue = True
1667        if confirm:
1668            should_continue = self._confirm("[red]Are you sure?[/red]")
1669            self.log_status_update("")
1670
1671        if should_continue:
1672            self.state_import_progress = make_progress_bar("{task.description}", self.console)
1673
1674            self.state_import_info = Tree("[bold]State File Information:")
1675
1676            self.state_import_version_task = self.state_import_progress.add_task(
1677                "Importing versions", start=False
1678            )
1679            self.state_import_snapshot_task = self.state_import_progress.add_task(
1680                "Importing snapshots", start=False
1681            )
1682            self.state_import_environment_task = self.state_import_progress.add_task(
1683                "Importing environments", start=False
1684            )
1685
1686            self.state_import_progress.start()
1687
1688        return should_continue
1689
1690    def update_state_import_progress(
1691        self,
1692        timestamp: t.Optional[str] = None,
1693        state_file_version: t.Optional[int] = None,
1694        versions: t.Optional[Versions] = None,
1695        snapshot_count: t.Optional[int] = None,
1696        snapshots_complete: bool = False,
1697        environment_count: t.Optional[int] = None,
1698        environments_complete: bool = False,
1699    ) -> None:
1700        if self.state_import_progress:
1701            if self.state_import_info:
1702                if timestamp:
1703                    self.state_import_info.add(f"Creation Timestamp: {timestamp}")
1704                if state_file_version:
1705                    self.state_import_info.add(f"File Version: {state_file_version}")
1706                if versions:
1707                    self.state_import_info.add(f"SQLMesh version: {versions.sqlmesh_version}")
1708                    self.state_import_info.add(
1709                        f"SQLMesh migration version: {versions.schema_version}"
1710                    )
1711                    self.state_import_info.add(f"SQLGlot version: {versions.sqlglot_version}\n")
1712
1713                    self._print(self.state_import_info)
1714
1715                    version_count = len(versions.model_dump())
1716
1717                    if self.state_import_version_task is not None:
1718                        self.state_import_progress.start_task(self.state_import_version_task)
1719                        self.state_import_progress.update(
1720                            self.state_import_version_task,
1721                            total=version_count,
1722                            completed=version_count,
1723                        )
1724                        self.state_import_progress.stop_task(self.state_import_version_task)
1725
1726            if self.state_import_snapshot_task is not None:
1727                if snapshot_count is not None:
1728                    self.state_import_progress.start_task(self.state_import_snapshot_task)
1729                    self.state_import_progress.update(
1730                        self.state_import_snapshot_task,
1731                        completed=snapshot_count,
1732                        total=snapshot_count,
1733                        refresh=True,
1734                    )
1735
1736                if snapshots_complete:
1737                    self.state_import_progress.stop_task(self.state_import_snapshot_task)
1738
1739            if self.state_import_environment_task is not None:
1740                if environment_count is not None:
1741                    self.state_import_progress.start_task(self.state_import_environment_task)
1742                    self.state_import_progress.update(
1743                        self.state_import_environment_task,
1744                        completed=environment_count,
1745                        total=environment_count,
1746                        refresh=True,
1747                    )
1748
1749                if environments_complete:
1750                    self.state_import_progress.stop_task(self.state_import_environment_task)
1751
1752    def stop_state_import(self, success: bool, input_file: Path) -> None:
1753        if self.state_import_progress:
1754            self.state_import_progress.stop()
1755            self.state_import_progress = None
1756
1757            self.log_status_update("")
1758
1759            if success:
1760                self.log_success(f"State imported successfully from '{input_file.as_posix()}'")
1761            else:
1762                self.log_error("State import failed!")
1763
1764    def show_environment_difference_summary(
1765        self,
1766        context_diff: ContextDiff,
1767        no_diff: bool = True,
1768    ) -> None:
1769        """Shows a summary of the environment differences.
1770
1771        Args:
1772            context_diff: The context diff to use to print the summary
1773            no_diff: Hide the actual environment statement differences.
1774        """
1775        if context_diff.is_new_environment:
1776            msg = (
1777                f"\n`{context_diff.environment}` environment will be initialized"
1778                if not context_diff.create_from_env_exists
1779                else f"\nNew environment `{context_diff.environment}` will be created from `{context_diff.create_from}`"
1780            )
1781            self._print(Tree(f"[bold]{msg}\n"))
1782            if not context_diff.has_snapshot_changes:
1783                return
1784
1785        if not context_diff.has_changes:
1786            # This is only reached when the plan is against an existing environment, so we use the environment
1787            #   name instead of the create_from name. The equivalent message for new environments happens in
1788            #   the PlanBuilder.
1789            self._print(
1790                Tree(
1791                    f"\n[bold]No changes to plan: project files match the `{context_diff.environment}` environment\n"
1792                )
1793            )
1794            return
1795
1796        if not context_diff.is_new_environment or (
1797            context_diff.is_new_environment and context_diff.create_from_env_exists
1798        ):
1799            self._print(
1800                Tree(
1801                    f"\n[bold]Differences from the `{context_diff.create_from if context_diff.is_new_environment else context_diff.environment}` environment:\n"
1802                )
1803            )
1804
1805        if context_diff.has_requirement_changes:
1806            self._print(f"[bold]Requirements:\n{context_diff.requirements_diff()}")
1807
1808        if context_diff.has_environment_statements_changes and not no_diff:
1809            self._print("[bold]Environment statements:\n")
1810            for type, diff in context_diff.environment_statements_diff(
1811                include_python_env=not context_diff.is_new_environment
1812            ):
1813                self._print(Syntax(diff, type, line_numbers=False))
1814
1815    def show_model_difference_summary(
1816        self,
1817        context_diff: ContextDiff,
1818        environment_naming_info: EnvironmentNamingInfo,
1819        default_catalog: t.Optional[str],
1820        no_diff: bool = True,
1821    ) -> None:
1822        """Shows a summary of the model differences.
1823
1824        Args:
1825            context_diff: The context diff to use to print the summary
1826            environment_naming_info: The environment naming info to reference when printing model names
1827            default_catalog: The default catalog to reference when deciding to remove catalog from display names
1828            no_diff: Hide the actual SQL differences.
1829        """
1830        self._show_summary_tree_for(
1831            context_diff,
1832            "Models",
1833            lambda x: x.is_model,
1834            environment_naming_info,
1835            default_catalog,
1836            no_diff=no_diff,
1837        )
1838        self._show_summary_tree_for(
1839            context_diff,
1840            "Standalone Audits",
1841            lambda x: x.is_audit,
1842            environment_naming_info,
1843            default_catalog,
1844            no_diff=no_diff,
1845        )
1846
1847    def plan(
1848        self,
1849        plan_builder: PlanBuilder,
1850        auto_apply: bool,
1851        default_catalog: t.Optional[str],
1852        no_diff: bool = False,
1853        no_prompts: bool = False,
1854    ) -> None:
1855        """The main plan flow.
1856
1857        The console should present the user with choices on how to backfill and version the snapshots
1858        of a plan.
1859
1860        Args:
1861            plan: The plan to make choices for.
1862            auto_apply: Whether to automatically apply the plan after all choices have been made.
1863            default_catalog: The default catalog to reference when deciding to remove catalog from display names
1864            no_diff: Hide text differences for changed models.
1865            no_prompts: Whether to disable interactive prompts for the backfill time range. Please note that
1866                if this flag is set to true and there are uncategorized changes the plan creation will
1867                fail. Default: False
1868        """
1869        self._prompt_categorize(
1870            plan_builder,
1871            auto_apply,
1872            no_diff=no_diff,
1873            no_prompts=no_prompts,
1874            default_catalog=default_catalog,
1875        )
1876
1877        self._show_options_after_categorization(
1878            plan_builder, auto_apply, default_catalog=default_catalog, no_prompts=no_prompts
1879        )
1880
1881        if auto_apply:
1882            plan_builder.apply()
1883
1884    def _show_summary_tree_for(
1885        self,
1886        context_diff: ContextDiff,
1887        header: str,
1888        snapshot_selector: t.Callable[[SnapshotInfoLike], bool],
1889        environment_naming_info: EnvironmentNamingInfo,
1890        default_catalog: t.Optional[str],
1891        no_diff: bool = True,
1892    ) -> None:
1893        added_snapshot_ids = {
1894            s_id for s_id in context_diff.added if snapshot_selector(context_diff.snapshots[s_id])
1895        }
1896        removed_snapshot_ids = {
1897            s_id
1898            for s_id, snapshot in context_diff.removed_snapshots.items()
1899            if snapshot_selector(snapshot)
1900        }
1901        modified_snapshot_ids = {
1902            current_snapshot.snapshot_id
1903            for _, (current_snapshot, _) in context_diff.modified_snapshots.items()
1904            if snapshot_selector(current_snapshot)
1905        }
1906
1907        tree_sets = (
1908            added_snapshot_ids,
1909            removed_snapshot_ids,
1910            modified_snapshot_ids,
1911        )
1912        if all(not s_ids for s_ids in tree_sets):
1913            return
1914
1915        tree = Tree(f"[bold]{header}:")
1916        if added_snapshot_ids:
1917            added_tree = Tree("[bold][added]Added:")
1918            for s_id in sorted(added_snapshot_ids):
1919                snapshot = context_diff.snapshots[s_id]
1920                added_tree.add(
1921                    f"[added]{snapshot.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}"
1922                )
1923            tree.add(self._limit_model_names(added_tree, self.verbosity))
1924        if removed_snapshot_ids:
1925            removed_tree = Tree("[bold][removed]Removed:")
1926            for s_id in sorted(removed_snapshot_ids):
1927                snapshot_table_info = context_diff.removed_snapshots[s_id]
1928                removed_tree.add(
1929                    f"[removed]{snapshot_table_info.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}"
1930                )
1931            tree.add(self._limit_model_names(removed_tree, self.verbosity))
1932        if modified_snapshot_ids:
1933            tree = self._add_modified_models(
1934                context_diff,
1935                modified_snapshot_ids,
1936                tree,
1937                environment_naming_info,
1938                default_catalog,
1939                no_diff,
1940            )
1941
1942        self._print(tree)
1943
1944    def _add_modified_models(
1945        self,
1946        context_diff: ContextDiff,
1947        modified_snapshot_ids: t.Set[SnapshotId],
1948        tree: Tree,
1949        environment_naming_info: EnvironmentNamingInfo,
1950        default_catalog: t.Optional[str] = None,
1951        no_diff: bool = True,
1952    ) -> Tree:
1953        direct = Tree("[bold][direct]Directly Modified:")
1954        indirect = Tree("[bold][indirect]Indirectly Modified:")
1955        metadata = Tree("[bold][metadata]Metadata Updated:")
1956        for s_id in modified_snapshot_ids:
1957            name = s_id.name
1958            display_name = context_diff.snapshots[s_id].display_name(
1959                environment_naming_info,
1960                default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1961                dialect=self.dialect,
1962            )
1963            if context_diff.directly_modified(name):
1964                direct.add(
1965                    f"[direct]{display_name}"
1966                    if no_diff
1967                    else Syntax(f"{display_name}\n{context_diff.text_diff(name)}", "sql")
1968                )
1969            elif context_diff.indirectly_modified(name):
1970                indirect.add(f"[indirect]{display_name}")
1971            elif context_diff.metadata_updated(name):
1972                metadata.add(
1973                    f"[metadata]{display_name}"
1974                    if no_diff
1975                    else Syntax(f"{display_name}\n{context_diff.text_diff(name)}", "sql")
1976                )
1977        if direct.children:
1978            tree.add(direct)
1979        if indirect.children:
1980            tree.add(self._limit_model_names(indirect, self.verbosity))
1981        if metadata.children:
1982            tree.add(metadata)
1983        return tree
1984
1985    def _show_options_after_categorization(
1986        self,
1987        plan_builder: PlanBuilder,
1988        auto_apply: bool,
1989        default_catalog: t.Optional[str],
1990        no_prompts: bool,
1991    ) -> None:
1992        plan = plan_builder.build()
1993        if not no_prompts and plan.forward_only and plan.new_snapshots:
1994            self._prompt_effective_from(plan_builder, auto_apply, default_catalog)
1995
1996        if plan.requires_backfill:
1997            self._show_missing_dates(plan_builder.build(), default_catalog)
1998
1999            if not no_prompts:
2000                self._prompt_backfill(plan_builder, auto_apply, default_catalog)
2001
2002            backfill_or_preview = "preview" if plan.is_dev and plan.forward_only else "backfill"
2003            if not auto_apply and self._confirm(
2004                f"Apply - {backfill_or_preview.capitalize()} Tables"
2005            ):
2006                plan_builder.apply()
2007        elif plan.has_changes and not auto_apply:
2008            self._prompt_promote(plan_builder)
2009        elif plan.has_unmodified_unpromoted and not auto_apply:
2010            self.log_status_update("\n[bold]Virtually updating unmodified models\n")
2011            self._prompt_promote(plan_builder)
2012
2013    def _prompt_categorize(
2014        self,
2015        plan_builder: PlanBuilder,
2016        auto_apply: bool,
2017        no_diff: bool,
2018        no_prompts: bool,
2019        default_catalog: t.Optional[str],
2020    ) -> None:
2021        """Get the user's change category for the directly modified models."""
2022        plan = plan_builder.build()
2023
2024        if plan.restatements:
2025            # A plan can have restatements for the following reasons:
2026            # - The user specifically called `sqlmesh plan` with --restate-model.
2027            #   This creates a "restatement plan" which disallows all other changes and simply force-backfills
2028            #   the selected models and their downstream dependencies using the versions of the models stored in state.
2029            # - There are no specific restatements (so changes are allowed) AND dev previews need to be computed.
2030            #   The "restatements" feature is currently reused for dev previews.
2031            if plan.selected_models_to_restate:
2032                # There were legitimate restatements, no dev previews
2033                tree = Tree(
2034                    "[bold]Models selected for restatement:[/bold]\n"
2035                    "This causes backfill of the model itself as well as affected downstream models"
2036                )
2037                model_fqn_to_snapshot = {s.name: s for s in plan.snapshots.values()}
2038                for model_fqn in plan.selected_models_to_restate:
2039                    snapshot = model_fqn_to_snapshot[model_fqn]
2040                    display_name = snapshot.display_name(
2041                        plan.environment_naming_info,
2042                        default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
2043                        dialect=self.dialect,
2044                    )
2045                    tree.add(
2046                        display_name
2047                    )  # note: we deliberately dont show any intervals here; they get shown in the backfill section
2048                self._print(tree)
2049            else:
2050                # We are computing dev previews, do not confuse the user by printing out something to do
2051                # with restatements. Dev previews are already highlighted in the backfill step
2052                pass
2053        else:
2054            self.show_environment_difference_summary(
2055                plan.context_diff,
2056                no_diff=no_diff,
2057            )
2058
2059        if plan.context_diff.has_changes:
2060            self.show_model_difference_summary(
2061                plan.context_diff,
2062                plan.environment_naming_info,
2063                default_catalog=default_catalog,
2064            )
2065
2066        if not no_diff:
2067            self._show_categorized_snapshots(plan, default_catalog)
2068
2069        for snapshot in plan.uncategorized:
2070            if snapshot.is_model and snapshot.model.forward_only:
2071                continue
2072            if not no_diff:
2073                self.show_sql(plan.context_diff.text_diff(snapshot.name))
2074            tree = Tree(
2075                f"[bold][direct]Directly Modified: {snapshot.display_name(plan.environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}"
2076            )
2077            indirect_tree = None
2078
2079            for child_sid in sorted(plan.indirectly_modified.get(snapshot.snapshot_id, set())):
2080                child_snapshot = plan.context_diff.snapshots[child_sid]
2081                if not indirect_tree:
2082                    indirect_tree = Tree("[indirect]Indirectly Modified Children:")
2083                    tree.add(indirect_tree)
2084                indirect_tree.add(
2085                    f"[indirect]{child_snapshot.display_name(plan.environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}"
2086                )
2087            if indirect_tree:
2088                indirect_tree = self._limit_model_names(indirect_tree, self.verbosity)
2089
2090            self._print(tree)
2091            if not no_prompts:
2092                self._get_snapshot_change_category(
2093                    snapshot, plan_builder, auto_apply, default_catalog
2094                )
2095
2096    def _show_categorized_snapshots(self, plan: Plan, default_catalog: t.Optional[str]) -> None:
2097        context_diff = plan.context_diff
2098
2099        for snapshot in plan.categorized:
2100            if context_diff.directly_modified(snapshot.name):
2101                category_str = SNAPSHOT_CHANGE_CATEGORY_STR[snapshot.change_category]
2102                tree = Tree(
2103                    f"\n[bold][direct]Directly Modified: {snapshot.display_name(plan.environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)} ({category_str})"
2104                )
2105                indirect_tree = None
2106                for child_sid in sorted(plan.indirectly_modified.get(snapshot.snapshot_id, set())):
2107                    child_snapshot = context_diff.snapshots[child_sid]
2108                    if not indirect_tree:
2109                        indirect_tree = Tree("[indirect]Indirectly Modified Children:")
2110                        tree.add(indirect_tree)
2111                    child_category_str = SNAPSHOT_CHANGE_CATEGORY_STR[
2112                        child_snapshot.change_category
2113                    ]
2114                    indirect_tree.add(
2115                        f"[indirect]{child_snapshot.display_name(plan.environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)} ({child_category_str})"
2116                    )
2117                if indirect_tree:
2118                    indirect_tree = self._limit_model_names(indirect_tree, self.verbosity)
2119            elif context_diff.metadata_updated(snapshot.name):
2120                tree = Tree(
2121                    f"\n[bold][metadata]Metadata Updated: {snapshot.display_name(plan.environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}"
2122                )
2123            else:
2124                continue
2125
2126            text_diff = context_diff.text_diff(snapshot.name)
2127            if text_diff:
2128                self._print("")
2129                self._print(Syntax(text_diff, "sql", word_wrap=True))
2130            self._print(tree)
2131
2132    def _show_missing_dates(self, plan: Plan, default_catalog: t.Optional[str]) -> None:
2133        """Displays the models with missing dates."""
2134        missing_intervals = plan.missing_intervals
2135        if not missing_intervals:
2136            return
2137        backfill = Tree("[bold]Models needing backfill:[/bold]")
2138        for missing in missing_intervals:
2139            snapshot = plan.context_diff.snapshots[missing.snapshot_id]
2140            if not snapshot.is_model:
2141                continue
2142
2143            preview_modifier = ""
2144            if not plan.deployability_index.is_deployable(snapshot):
2145                preview_modifier = " ([orange1]preview[/orange1])"
2146
2147            display_name = snapshot.display_name(
2148                plan.environment_naming_info,
2149                default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
2150                dialect=self.dialect,
2151            )
2152            backfill.add(
2153                f"{display_name}: \\[{_format_missing_intervals(snapshot, missing)}]{preview_modifier}"
2154            )
2155
2156        if backfill:
2157            backfill = self._limit_model_names(backfill, self.verbosity)
2158        self._print(backfill)
2159
2160    def _prompt_effective_from(
2161        self, plan_builder: PlanBuilder, auto_apply: bool, default_catalog: t.Optional[str]
2162    ) -> None:
2163        if not plan_builder.build().effective_from:
2164            effective_from = self._prompt(
2165                "Enter the effective date (eg. '1 year', '2020-01-01') to apply forward-only changes retroactively or blank to only apply them going forward once changes are deployed to prod"
2166            )
2167            if effective_from:
2168                plan_builder.set_effective_from(effective_from)
2169
2170    def _prompt_backfill(
2171        self, plan_builder: PlanBuilder, auto_apply: bool, default_catalog: t.Optional[str]
2172    ) -> None:
2173        plan = plan_builder.build()
2174        is_forward_only_dev = plan.is_dev and plan.forward_only
2175        backfill_or_preview = "preview" if is_forward_only_dev else "backfill"
2176
2177        if plan_builder.is_start_and_end_allowed:
2178            if not plan_builder.override_start:
2179                if is_forward_only_dev:
2180                    if plan.effective_from:
2181                        blank_meaning = f"to preview starting from the effective date ('{time_like_to_str(plan.effective_from)}')"
2182                        default_start = plan.effective_from
2183                    else:
2184                        blank_meaning = "to preview starting from yesterday"
2185                        default_start = yesterday_ds()
2186                else:
2187                    if plan.provided_start:
2188                        blank_meaning = f"starting from '{time_like_to_str(plan.provided_start)}'"
2189                    else:
2190                        blank_meaning = "from the beginning of history"
2191                    default_start = None
2192
2193                start = self._prompt(
2194                    f"Enter the {backfill_or_preview} start date (eg. '1 year', '2020-01-01') or blank to backfill {blank_meaning}",
2195                )
2196                if start:
2197                    plan_builder.set_start(start)
2198                elif default_start:
2199                    plan_builder.set_start(default_start)
2200
2201            if not plan_builder.override_end:
2202                if plan.provided_end:
2203                    blank_meaning = f"'{time_like_to_str(plan.provided_end)}'"
2204                elif plan.end_override_per_model:
2205                    max_end = max(plan.end_override_per_model.values())
2206                    blank_meaning = f"'{time_like_to_str(max_end)}'"
2207                else:
2208                    blank_meaning = "now"
2209                end = self._prompt(
2210                    f"Enter the {backfill_or_preview} end date (eg. '1 month ago', '2020-01-01') or blank to {backfill_or_preview} up until {blank_meaning}",
2211                )
2212                if end:
2213                    plan_builder.set_end(end)
2214
2215            plan = plan_builder.build()
2216
2217    def _prompt_promote(self, plan_builder: PlanBuilder) -> None:
2218        if self._confirm(
2219            "Apply - Virtual Update",
2220        ):
2221            plan_builder.apply()
2222
2223    def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
2224        # We don't log the test results if no tests were ran
2225        if not result.testsRun:
2226            return
2227
2228        divider_length = 70
2229
2230        self._log_test_details(result)
2231
2232        message = (
2233            f"Ran {result.testsRun} tests against {target_dialect} in {result.duration} seconds."
2234        )
2235        if result.wasSuccessful():
2236            self._print("=" * divider_length)
2237            self._print(
2238                f"Successfully {message}",
2239                style="green",
2240            )
2241            self._print("-" * divider_length)
2242        else:
2243            self._print("-" * divider_length)
2244            self._print("Test Failure Summary", style="red")
2245            self._print("=" * divider_length)
2246            fail_and_error_tests = result.get_fail_and_error_tests()
2247            self._print(f"{message} \n")
2248
2249            self._print(f"Failed tests ({len(fail_and_error_tests)}):")
2250            for test in fail_and_error_tests:
2251                self._print(f" • {test.path}::{test.test_name}")
2252            self._print("=" * divider_length, end="\n\n")
2253
2254    def _captured_unit_test_results(self, result: ModelTextTestResult) -> str:
2255        with self.console.capture() as capture:
2256            self._log_test_details(result)
2257        return strip_ansi_codes(capture.get())
2258
2259    def show_sql(self, sql: str) -> None:
2260        self._print(Syntax(sql, "sql", word_wrap=True), crop=False)
2261
2262    def log_status_update(self, message: str) -> None:
2263        self._print(message)
2264
2265    def log_skipped_models(self, snapshot_names: t.Set[str]) -> None:
2266        if snapshot_names:
2267            msg = "  " + "\n  ".join(snapshot_names)
2268            self._print(f"[dark_orange3]Skipped models[/dark_orange3]\n\n{msg}")
2269
2270    def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None:
2271        if errors:
2272            self._print("\n[red]Failed models[/red]\n")
2273
2274            error_messages = _format_node_errors(errors)
2275
2276            for node_name, msg in error_messages.items():
2277                self._print(f"  [red]{node_name}[/red]\n\n{msg}")
2278
2279    def log_models_updated_during_restatement(
2280        self,
2281        snapshots: t.List[t.Tuple[SnapshotTableInfo, SnapshotTableInfo]],
2282        environment_naming_info: EnvironmentNamingInfo,
2283        default_catalog: t.Optional[str] = None,
2284    ) -> None:
2285        if snapshots:
2286            tree = Tree(
2287                f"[yellow]The following models had new versions deployed while data was being restated:[/yellow]"
2288            )
2289
2290            for restated_snapshot, updated_snapshot in snapshots:
2291                display_name = restated_snapshot.display_name(
2292                    environment_naming_info,
2293                    default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
2294                    dialect=self.dialect,
2295                )
2296                current_branch = tree.add(display_name)
2297                current_branch.add(f"restated version: '{restated_snapshot.version}'")
2298                current_branch.add(f"currently active version: '{updated_snapshot.version}'")
2299
2300            self._print(tree)
2301            self._print("")  # newline spacer
2302
2303    def log_destructive_change(
2304        self,
2305        snapshot_name: str,
2306        alter_operations: t.List[TableAlterOperation],
2307        dialect: str,
2308        error: bool = True,
2309    ) -> None:
2310        if error:
2311            self._print(format_destructive_change_msg(snapshot_name, alter_operations, dialect))
2312        else:
2313            self.log_warning(
2314                format_destructive_change_msg(snapshot_name, alter_operations, dialect, error)
2315            )
2316
2317    def log_additive_change(
2318        self,
2319        snapshot_name: str,
2320        alter_operations: t.List[TableAlterOperation],
2321        dialect: str,
2322        error: bool = True,
2323    ) -> None:
2324        if error:
2325            self._print(format_additive_change_msg(snapshot_name, alter_operations, dialect))
2326        else:
2327            self.log_warning(
2328                format_additive_change_msg(snapshot_name, alter_operations, dialect, error)
2329            )
2330
2331    def log_error(self, message: str) -> None:
2332        self._print(f"[red]{message}[/red]")
2333
2334    def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None:
2335        logger.warning(long_message or short_message)
2336        if not self.ignore_warnings:
2337            if long_message:
2338                file_path = None
2339                for handler in logger.root.handlers:
2340                    if isinstance(handler, logging.FileHandler):
2341                        file_path = handler.baseFilename
2342                        break
2343                file_path_msg = f" Learn more in logs: {file_path}\n" if file_path else ""
2344                short_message = f"{short_message}{file_path_msg}"
2345            message_lstrip = short_message.lstrip()
2346            leading_ws = short_message[: -len(message_lstrip)]
2347            message_formatted = f"{leading_ws}[yellow]\\[WARNING] {message_lstrip}[/yellow]"
2348            self._print(message_formatted)
2349
2350    def log_success(self, message: str) -> None:
2351        self._print(f"[green]{message}[/green]\n")
2352
2353    def loading_start(self, message: t.Optional[str] = None) -> uuid.UUID:
2354        id = uuid.uuid4()
2355        self.loading_status[id] = Status(message or "", console=self.console, spinner="line")
2356        self.loading_status[id].start()
2357        return id
2358
2359    def loading_stop(self, id: uuid.UUID) -> None:
2360        self.loading_status[id].stop()
2361        del self.loading_status[id]
2362
2363    def show_table_diff_details(
2364        self,
2365        models_to_diff: t.List[str],
2366    ) -> None:
2367        """Display information about which tables are going to be diffed"""
2368
2369        if models_to_diff:
2370            m_tree = Tree("\n[b]Models to compare:")
2371            for m in models_to_diff:
2372                m_tree.add(f"[{self.TABLE_DIFF_SOURCE_BLUE}]{m}[/{self.TABLE_DIFF_SOURCE_BLUE}]")
2373            self._print(m_tree)
2374            self._print("")
2375
2376    def start_table_diff_progress(self, models_to_diff: int) -> None:
2377        if not self.table_diff_progress:
2378            self.table_diff_progress = make_progress_bar(
2379                "Calculating model differences", self.console
2380            )
2381            self.table_diff_model_progress = Progress(
2382                TextColumn("{task.fields[view_name]}", justify="right"),
2383                SpinnerColumn(spinner_name="simpleDots"),
2384                console=self.console,
2385            )
2386
2387            progress_table = Table.grid()
2388            progress_table.add_row(self.table_diff_progress)
2389            progress_table.add_row(self.table_diff_model_progress)
2390
2391            self.table_diff_progress_live = Live(progress_table, refresh_per_second=10)
2392            self.table_diff_progress_live.start()
2393
2394            self.table_diff_model_task = self.table_diff_progress.add_task(
2395                "Diffing", total=models_to_diff
2396            )
2397
2398    def start_table_diff_model_progress(self, model: str) -> None:
2399        if self.table_diff_model_progress and model not in self.table_diff_model_tasks:
2400            self.table_diff_model_tasks[model] = self.table_diff_model_progress.add_task(
2401                f"Diffing {model}...",
2402                view_name=model,
2403                total=1,
2404            )
2405
2406    def update_table_diff_progress(self, model: str) -> None:
2407        if self.table_diff_progress:
2408            self.table_diff_progress.update(self.table_diff_model_task, refresh=True, advance=1)
2409        if self.table_diff_model_progress and model in self.table_diff_model_tasks:
2410            model_task_id = self.table_diff_model_tasks[model]
2411            self.table_diff_model_progress.remove_task(model_task_id)
2412
2413    def stop_table_diff_progress(self, success: bool) -> None:
2414        if self.table_diff_progress_live:
2415            self.table_diff_progress_live.stop()
2416            self.table_diff_progress_live = None
2417            self.log_status_update("")
2418
2419            if success:
2420                self.log_success(f"Table diff completed successfully!")
2421            else:
2422                self.log_error("Table diff failed!")
2423
2424        self.table_diff_progress = None
2425        self.table_diff_model_progress = None
2426        self.table_diff_model_tasks = {}
2427
2428    def show_table_diff_summary(self, table_diff: TableDiff) -> None:
2429        tree = Tree("\n[b]Table Diff")
2430
2431        if table_diff.model_name:
2432            model = Tree("Model:")
2433            model.add(f"[blue]{table_diff.model_name}[/blue]")
2434
2435            tree.add(model)
2436
2437            envs = Tree("Environment:")
2438            source = Tree(
2439                f"Source: [{self.TABLE_DIFF_SOURCE_BLUE}]{table_diff.source_alias}[/{self.TABLE_DIFF_SOURCE_BLUE}]"
2440            )
2441            envs.add(source)
2442
2443            target = Tree(
2444                f"Target: [{self.TABLE_DIFF_TARGET_GREEN}]{table_diff.target_alias}[/{self.TABLE_DIFF_TARGET_GREEN}]"
2445            )
2446            envs.add(target)
2447
2448            tree.add(envs)
2449
2450        tables = Tree("Tables:")
2451
2452        tables.add(
2453            f"Source: [{self.TABLE_DIFF_SOURCE_BLUE}]{table_diff.source}[/{self.TABLE_DIFF_SOURCE_BLUE}]"
2454        )
2455        tables.add(
2456            f"Target: [{self.TABLE_DIFF_TARGET_GREEN}]{table_diff.target}[/{self.TABLE_DIFF_TARGET_GREEN}]"
2457        )
2458
2459        tree.add(tables)
2460
2461        join = Tree("Join On:")
2462        _, _, key_column_names = table_diff.key_columns
2463        for col_name in key_column_names:
2464            join.add(f"[yellow]{col_name}[/yellow]")
2465
2466        tree.add(join)
2467
2468        self._print(tree)
2469
2470    def show_schema_diff(self, schema_diff: SchemaDiff) -> None:
2471        source_name = schema_diff.source
2472        if schema_diff.source_alias:
2473            source_name = schema_diff.source_alias.upper()
2474        target_name = schema_diff.target
2475        if schema_diff.target_alias:
2476            target_name = schema_diff.target_alias.upper()
2477
2478        first_line = f"\n[b]Schema Diff Between '[{self.TABLE_DIFF_SOURCE_BLUE}]{source_name}[/{self.TABLE_DIFF_SOURCE_BLUE}]' and '[{self.TABLE_DIFF_TARGET_GREEN}]{target_name}[/{self.TABLE_DIFF_TARGET_GREEN}]'"
2479        if schema_diff.model_name:
2480            first_line = (
2481                first_line + f" environments for model '[blue]{schema_diff.model_name}[/blue]'"
2482            )
2483
2484        tree = Tree(first_line + ":")
2485
2486        if any([schema_diff.added, schema_diff.removed, schema_diff.modified]):
2487            if schema_diff.added:
2488                added = Tree("[green]Added Columns:")
2489                for c, t in schema_diff.added:
2490                    added.add(f"[green]{c} ({t})")
2491                tree.add(added)
2492
2493            if schema_diff.removed:
2494                removed = Tree("[red]Removed Columns:")
2495                for c, t in schema_diff.removed:
2496                    removed.add(f"[red]{c} ({t})")
2497                tree.add(removed)
2498
2499            if schema_diff.modified:
2500                modified = Tree("[magenta]Modified Columns:")
2501                for c, (ft, tt) in schema_diff.modified.items():
2502                    modified.add(f"[magenta]{c} ({ft} -> {tt})")
2503                tree.add(modified)
2504        else:
2505            tree.add("[b]Schemas match")
2506
2507        self.console.print(tree)
2508
2509    def show_row_diff(
2510        self, row_diff: RowDiff, show_sample: bool = True, skip_grain_check: bool = False
2511    ) -> None:
2512        if row_diff.empty:
2513            self.console.print(
2514                "\n[b][red]Neither the source nor the target table contained any records[/red][/b]"
2515            )
2516            return
2517
2518        source_name = row_diff.source
2519        if row_diff.source_alias:
2520            source_name = row_diff.source_alias.upper()
2521        target_name = row_diff.target
2522        if row_diff.target_alias:
2523            target_name = row_diff.target_alias.upper()
2524
2525        if row_diff.stats["null_grain_count"] > 0 or (
2526            not skip_grain_check
2527            and (
2528                row_diff.stats["distinct_count_s"] != row_diff.stats["s_count"]
2529                or row_diff.stats["distinct_count_t"] != row_diff.stats["t_count"]
2530            )
2531        ):
2532            self.console.print(
2533                "[b][red]\nGrain should have unique and not-null audits for accurate results.[/red][/b]"
2534            )
2535
2536        tree = Tree("[b]Row Counts:[/b]")
2537        if row_diff.full_match_count:
2538            tree.add(
2539                f" [b][cyan]FULL MATCH[/cyan]:[/b] {row_diff.full_match_count} rows ({row_diff.full_match_pct}%)"
2540            )
2541        if row_diff.partial_match_count:
2542            tree.add(
2543                f" [b][blue]PARTIAL MATCH[/blue]:[/b] {row_diff.partial_match_count} rows ({row_diff.partial_match_pct}%)"
2544            )
2545        if row_diff.s_only_count:
2546            tree.add(
2547                f" [b][yellow]{source_name} ONLY[/yellow]:[/b] {row_diff.s_only_count} rows ({row_diff.s_only_pct}%)"
2548            )
2549        if row_diff.t_only_count:
2550            tree.add(
2551                f" [b][green]{target_name} ONLY[/green]:[/b] {row_diff.t_only_count} rows ({row_diff.t_only_pct}%)"
2552            )
2553        self.console.print("\n", tree)
2554
2555        self.console.print("\n[b][blue]COMMON ROWS[/blue] column comparison stats:[/b]")
2556        if row_diff.column_stats.shape[0] > 0:
2557            self.console.print(row_diff.column_stats.to_string(index=True), end="\n\n")
2558        else:
2559            self.console.print("  No columns with same name and data type in both tables")
2560
2561        if show_sample:
2562            sample = row_diff.joined_sample
2563            self.console.print("\n[b][blue]COMMON ROWS[/blue] sample data differences:[/b]")
2564            if sample.shape[0] > 0:
2565                keys: list[str] = []
2566                columns: dict[str, list[str]] = {}
2567                source_prefix, source_name = (
2568                    (f"{source_name}__", source_name)
2569                    if source_name.lower() != row_diff.source.lower()
2570                    else ("s__", "SOURCE")
2571                )
2572                target_prefix, target_name = (
2573                    (f"{target_name}__", target_name)
2574                    if target_name.lower() != row_diff.target.lower()
2575                    else ("t__", "TARGET")
2576                )
2577
2578                # Extract key and column names from the joined sample
2579                for column in row_diff.joined_sample.columns:
2580                    if source_prefix in column:
2581                        column_name = "__".join(column.split(source_prefix)[1:])
2582                        columns[column_name] = [column, target_prefix + column_name]
2583                    elif target_prefix not in column:
2584                        keys.append(column)
2585
2586                column_styles = {
2587                    source_name: self.TABLE_DIFF_SOURCE_BLUE,
2588                    target_name: self.TABLE_DIFF_TARGET_GREEN,
2589                }
2590
2591                for column, [source_column, target_column] in columns.items():
2592                    # Create a table with the joined keys and comparison columns
2593                    column_table = row_diff.joined_sample[keys + [source_column, target_column]]
2594
2595                    # Filter to retain non identical-valued rows
2596                    column_table = column_table[
2597                        column_table.apply(
2598                            lambda row: not _cells_match(row[source_column], row[target_column]),
2599                            axis=1,
2600                        )
2601                    ]
2602
2603                    # Rename the column headers for readability
2604                    column_table = column_table.rename(
2605                        columns={
2606                            source_column: source_name,
2607                            target_column: target_name,
2608                        }
2609                    )
2610
2611                    table = Table(show_header=True)
2612                    for column_name in column_table.columns:
2613                        style = column_styles.get(column_name, "")
2614                        table.add_column(column_name, style=style, header_style=style)
2615
2616                    for _, row in column_table.iterrows():
2617                        table.add_row(
2618                            *[
2619                                str(
2620                                    round(cell, row_diff.decimals)
2621                                    if isinstance(cell, float)
2622                                    else cell
2623                                )
2624                                for cell in row
2625                            ]
2626                        )
2627
2628                    self.console.print(
2629                        f"Column: [underline][bold cyan]{column}[/bold cyan][/underline]",
2630                        table,
2631                        end="\n",
2632                    )
2633
2634            else:
2635                self.console.print("  All joined rows match")
2636
2637            if row_diff.s_sample.shape[0] > 0:
2638                self.console.print(f"\n[b][yellow]{source_name} ONLY[/yellow] sample rows:[/b]")
2639                self.console.print(row_diff.s_sample.to_string(index=False), end="\n\n")
2640
2641            if row_diff.t_sample.shape[0] > 0:
2642                self.console.print(f"\n[b][green]{target_name} ONLY[/green] sample rows:[/b]")
2643                self.console.print(row_diff.t_sample.to_string(index=False), end="\n\n")
2644
2645    def show_table_diff(
2646        self,
2647        table_diffs: t.List[TableDiff],
2648        show_sample: bool = True,
2649        skip_grain_check: bool = False,
2650        temp_schema: t.Optional[str] = None,
2651    ) -> None:
2652        """
2653        Display the table diff between all mismatched tables.
2654        """
2655        if len(table_diffs) > 1:
2656            mismatched_tables = []
2657            fully_matched = []
2658            for table_diff in table_diffs:
2659                if (
2660                    table_diff.schema_diff().source_schema == table_diff.schema_diff().target_schema
2661                ) and (
2662                    table_diff.row_diff(
2663                        temp_schema=temp_schema, skip_grain_check=skip_grain_check
2664                    ).full_match_pct
2665                    == 100
2666                ):
2667                    fully_matched.append(table_diff)
2668                else:
2669                    mismatched_tables.append(table_diff)
2670            table_diffs = mismatched_tables if mismatched_tables else []
2671            if fully_matched:
2672                m_tree = Tree("\n[b]Identical Tables")
2673                for m in fully_matched:
2674                    m_tree.add(
2675                        f"[{self.TABLE_DIFF_SOURCE_BLUE}]{m.source}[/{self.TABLE_DIFF_SOURCE_BLUE}] - [{self.TABLE_DIFF_TARGET_GREEN}]{m.target}[/{self.TABLE_DIFF_TARGET_GREEN}]"
2676                    )
2677                self._print(m_tree)
2678
2679            if mismatched_tables:
2680                m_tree = Tree("\n[b]Mismatched Tables")
2681                for m in mismatched_tables:
2682                    m_tree.add(
2683                        f"[{self.TABLE_DIFF_SOURCE_BLUE}]{m.source}[/{self.TABLE_DIFF_SOURCE_BLUE}] - [{self.TABLE_DIFF_TARGET_GREEN}]{m.target}[/{self.TABLE_DIFF_TARGET_GREEN}]"
2684                    )
2685                self._print(m_tree)
2686
2687        for table_diff in table_diffs:
2688            self.show_table_diff_summary(table_diff)
2689            self.show_schema_diff(table_diff.schema_diff())
2690            self.show_row_diff(
2691                table_diff.row_diff(temp_schema=temp_schema, skip_grain_check=skip_grain_check),
2692                show_sample=show_sample,
2693                skip_grain_check=skip_grain_check,
2694            )
2695
2696    def print_environments(self, environments_summary: t.List[EnvironmentSummary]) -> None:
2697        """Prints all environment names along with expiry datetime."""
2698        output = [
2699            f"{summary.name} - {time_like_to_str(summary.expiration_ts)}"
2700            if summary.expiration_ts
2701            else f"{summary.name} - No Expiry"
2702            for summary in environments_summary
2703        ]
2704        output_str = "\n".join([str(len(output)), *output])
2705        self.log_status_update(f"Number of SQLMesh environments are: {output_str}")
2706
2707    def show_intervals(self, snapshot_intervals: t.Dict[Snapshot, SnapshotIntervals]) -> None:
2708        complete = Tree(f"[b]Complete Intervals[/b]")
2709        incomplete = Tree(f"[b]Missing Intervals[/b]")
2710
2711        for snapshot, intervals in sorted(snapshot_intervals.items(), key=lambda s: s[0].node.name):
2712            if intervals.intervals:
2713                incomplete.add(
2714                    f"{snapshot.node.name}: [{intervals.format_intervals(snapshot.node.interval_unit)}]"
2715                )
2716            else:
2717                complete.add(snapshot.node.name)
2718
2719        if complete.children:
2720            self._print(complete)
2721
2722        if incomplete.children:
2723            self._print(incomplete)
2724
2725    def print_connection_config(self, config: ConnectionConfig, title: str = "Connection") -> None:
2726        tree = Tree(f"[b]{title}:[/b]")
2727        tree.add(f"Type: [bold cyan]{config.type_}[/bold cyan]")
2728        tree.add(f"Catalog: [bold cyan]{config.get_catalog()}[/bold cyan]")
2729
2730        try:
2731            engine_adapter_type = config._engine_adapter
2732            tree.add(f"Dialect: [bold cyan]{engine_adapter_type.DIALECT}[/bold cyan]")
2733        except NotImplementedError:
2734            # not all ConnectionConfig's have an engine adapter associated. The CloudConnectionConfig has a HTTP client instead
2735            pass
2736
2737        self._print(tree)
2738
2739    def _get_snapshot_change_category(
2740        self,
2741        snapshot: Snapshot,
2742        plan_builder: PlanBuilder,
2743        auto_apply: bool,
2744        default_catalog: t.Optional[str],
2745    ) -> None:
2746        choices = self._snapshot_change_choices(
2747            snapshot, plan_builder.environment_naming_info, default_catalog
2748        )
2749        response = self._prompt(
2750            "\n".join([f"[{i + 1}] {choice}" for i, choice in enumerate(choices.values())]),
2751            show_choices=False,
2752            choices=[f"{i + 1}" for i in range(len(choices))],
2753        )
2754        choice = list(choices)[int(response) - 1]
2755        plan_builder.set_choice(snapshot, choice)
2756
2757    def _snapshot_change_choices(
2758        self,
2759        snapshot: Snapshot,
2760        environment_naming_info: EnvironmentNamingInfo,
2761        default_catalog: t.Optional[str],
2762        use_rich_formatting: bool = True,
2763    ) -> t.Dict[SnapshotChangeCategory, str]:
2764        direct = snapshot.display_name(
2765            environment_naming_info,
2766            default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
2767            dialect=self.dialect,
2768        )
2769        if use_rich_formatting:
2770            direct = f"[direct]{direct}[/direct]"
2771        indirect = "indirectly modified children"
2772        if use_rich_formatting:
2773            indirect = f"[indirect]{indirect}[/indirect]"
2774        if snapshot.is_view:
2775            choices = {
2776                SnapshotChangeCategory.BREAKING: f"Update {direct} and backfill {indirect}",
2777                SnapshotChangeCategory.NON_BREAKING: f"Update {direct} but don't backfill {indirect}",
2778            }
2779        elif snapshot.is_symbolic:
2780            choices = {
2781                SnapshotChangeCategory.BREAKING: f"Backfill {indirect}",
2782                SnapshotChangeCategory.NON_BREAKING: f"Don't backfill {indirect}",
2783            }
2784        else:
2785            choices = {
2786                SnapshotChangeCategory.BREAKING: f"Backfill {direct} and {indirect}",
2787                SnapshotChangeCategory.NON_BREAKING: f"Backfill {direct} but not {indirect}",
2788            }
2789        labeled_choices = {
2790            k: f"[{SNAPSHOT_CHANGE_CATEGORY_STR[k]}] {v}" for k, v in choices.items()
2791        }
2792        return labeled_choices
2793
2794    def show_linter_violations(
2795        self, violations: t.List[RuleViolation], model: Model, is_error: bool = False
2796    ) -> None:
2797        severity = "errors" if is_error else "warnings"
2798
2799        # Sort violations by line, then alphabetically the name of the violation
2800        # Violations with no range go first
2801        sorted_violations = sorted(
2802            violations,
2803            key=lambda v: (
2804                v.violation_range.start.line if v.violation_range else -1,
2805                v.rule.name.lower(),
2806            ),
2807        )
2808        violations_text = [
2809            (
2810                f" - Line {v.violation_range.start.line + 1}: {v.rule.name} - {v.violation_msg}"
2811                if v.violation_range
2812                else f" - {v.rule.name}: {v.violation_msg}"
2813            )
2814            for v in sorted_violations
2815        ]
2816        violations_msg = "\n".join(violations_text)
2817        msg = f"Linter {severity} for {model._path}:\n{violations_msg}"
2818
2819        if is_error:
2820            self.log_error(msg)
2821        else:
2822            self.log_warning(msg)
2823
2824    def _log_test_details(
2825        self, result: ModelTextTestResult, unittest_char_separator: bool = True
2826    ) -> None:
2827        """
2828        This is a helper method that encapsulates the logic for logging the relevant unittest for the result.
2829        The top level method (`log_test_results`) reuses `_log_test_details` differently based on the console.
2830
2831        Args:
2832            result: The unittest test result that contains metrics like num success, fails, ect.
2833        """
2834        if result.wasSuccessful():
2835            self._print("\n", end="")
2836            return
2837
2838        if unittest_char_separator:
2839            self._print(f"\n{unittest.TextTestResult.separator1}\n\n", end="")
2840
2841        for (test_case, failure), test_failure_tables in zip_longest(  # type: ignore
2842            result.failures, result.failure_tables
2843        ):
2844            self._print(unittest.TextTestResult.separator2)
2845            self._print(f"FAIL: {test_case}")
2846
2847            if test_description := test_case.shortDescription():
2848                self._print(test_description)
2849            self._print(f"{unittest.TextTestResult.separator2}")
2850
2851            if not test_failure_tables:
2852                self._print(failure)
2853            else:
2854                for failure_table in test_failure_tables:
2855                    self._print(failure_table)
2856                    self._print("\n", end="")
2857
2858        for test_case, error in result.errors:
2859            self._print(unittest.TextTestResult.separator2)
2860            self._print(f"ERROR: {test_case}")
2861            self._print(f"{unittest.TextTestResult.separator2}")
2862            self._print(error)
2863
2864
2865def _cells_match(x: t.Any, y: t.Any) -> bool:
2866    """Helper function to compare two cells and returns true if they're equal, handling array objects."""
2867    import pandas as pd
2868    import numpy as np
2869
2870    # Convert array-like objects to list for consistent comparison
2871    def _normalize(val: t.Any) -> t.Any:
2872        # Convert Pandas null to Python null for the purposes of comparison to prevent errors like the following on boolean fields:
2873        # - TypeError: boolean value of NA is ambiguous
2874        # note pd.isnull() returns either a bool or a ndarray[bool] depending on if the input
2875        # is scalar or an array
2876        isnull = pd.isnull(val)
2877
2878        if isinstance(isnull, bool):  # scalar
2879            if isnull:
2880                val = None
2881        elif all(isnull):  # array
2882            val = None
2883
2884        return list(val) if isinstance(val, (pd.Series, np.ndarray)) else val
2885
2886    return _normalize(x) == _normalize(y)
2887
2888
2889def add_to_layout_widget(target_widget: LayoutWidget, *widgets: widgets.Widget) -> LayoutWidget:
2890    """Helper function to add a widget to a layout widget.
2891
2892    Args:
2893        target_widget: The layout widget to add the other widget(s) to.
2894        *widgets: The widgets to add to the layout widget.
2895
2896    Returns:
2897        The layout widget with the children added.
2898    """
2899    target_widget.children += tuple(widgets)
2900    return target_widget
2901
2902
2903class NotebookMagicConsole(TerminalConsole):
2904    """
2905    Console to be used when using the magic notebook interface (`%<command>`).
2906    Generally reuses the Terminal console when possible by either directly outputing what it provides
2907    or capturing it and converting it into a widget.
2908    """
2909
2910    def __init__(
2911        self,
2912        display: t.Optional[t.Callable] = None,
2913        console: t.Optional[RichConsole] = None,
2914        dialect: DialectType = None,
2915        **kwargs: t.Any,
2916    ) -> None:
2917        import ipywidgets as widgets
2918        from IPython import get_ipython
2919        from IPython.display import display as ipython_display
2920
2921        super().__init__(console, **kwargs)
2922
2923        self.display = display or get_ipython().user_ns.get("display", ipython_display)
2924        self.missing_dates_output = widgets.Output()
2925        self.dynamic_options_after_categorization_output = widgets.VBox()
2926
2927        self.dialect = dialect
2928
2929    def _show_missing_dates(self, plan: Plan, default_catalog: t.Optional[str]) -> None:
2930        self._add_to_dynamic_options(self.missing_dates_output)
2931        self.missing_dates_output.outputs = ()
2932        with self.missing_dates_output:
2933            super()._show_missing_dates(plan, default_catalog)
2934
2935    def _apply(self, button: widgets.Button) -> None:
2936        button.disabled = True
2937        with button.output:
2938            button.plan_builder.apply()
2939
2940    def _prompt_promote(self, plan_builder: PlanBuilder) -> None:
2941        import ipywidgets as widgets
2942
2943        button = widgets.Button(
2944            description="Apply - Virtual Update",
2945            disabled=False,
2946            button_style="success",
2947            # Auto will make the button really large.
2948            # Likely changing this soon anyways to be just `Apply` with description above
2949            layout={"width": "10rem"},
2950        )
2951        self._add_to_dynamic_options(button)
2952        output = widgets.Output()
2953        self._add_to_dynamic_options(output)
2954
2955        button.plan_builder = plan_builder
2956        button.on_click(self._apply)
2957        button.output = output
2958
2959    def _prompt_effective_from(
2960        self, plan_builder: PlanBuilder, auto_apply: bool, default_catalog: t.Optional[str]
2961    ) -> None:
2962        import ipywidgets as widgets
2963
2964        prompt = widgets.VBox()
2965
2966        def effective_from_change_callback(change: t.Dict[str, datetime.datetime]) -> None:
2967            plan_builder.set_effective_from(change["new"])
2968            self._show_options_after_categorization(
2969                plan_builder, auto_apply, default_catalog, no_prompts=False
2970            )
2971
2972        def going_forward_change_callback(change: t.Dict[str, bool]) -> None:
2973            checked = change["new"]
2974            plan_builder.set_effective_from(None if checked else yesterday_ds())
2975            self._show_options_after_categorization(
2976                plan_builder,
2977                auto_apply=auto_apply,
2978                default_catalog=default_catalog,
2979                no_prompts=False,
2980            )
2981
2982        date_picker = widgets.DatePicker(
2983            disabled=plan_builder.build().effective_from is None,
2984            value=to_date(plan_builder.build().effective_from or yesterday_ds()),
2985            layout={"width": "auto"},
2986        )
2987        date_picker.observe(effective_from_change_callback, "value")
2988
2989        going_forward_checkbox = widgets.Checkbox(
2990            value=plan_builder.build().effective_from is None,
2991            description="Apply Going Forward Once Deployed To Prod",
2992            disabled=False,
2993            indent=False,
2994        )
2995        going_forward_checkbox.observe(going_forward_change_callback, "value")
2996
2997        add_to_layout_widget(
2998            prompt,
2999            widgets.HBox(
3000                [
3001                    widgets.Label("Effective From Date:", layout={"width": "8rem"}),
3002                    date_picker,
3003                    going_forward_checkbox,
3004                ]
3005            ),
3006        )
3007
3008        self._add_to_dynamic_options(prompt)
3009
3010    def _prompt_backfill(
3011        self, plan_builder: PlanBuilder, auto_apply: bool, default_catalog: t.Optional[str]
3012    ) -> None:
3013        import ipywidgets as widgets
3014
3015        prompt = widgets.VBox()
3016
3017        backfill_or_preview = (
3018            "Preview"
3019            if plan_builder.build().is_dev and plan_builder.build().forward_only
3020            else "Backfill"
3021        )
3022
3023        def _date_picker(
3024            plan_builder: PlanBuilder, value: t.Any, on_change: t.Callable, disabled: bool = False
3025        ) -> widgets.DatePicker:
3026            picker = widgets.DatePicker(
3027                disabled=disabled,
3028                value=value,
3029                layout={"width": "auto"},
3030            )
3031
3032            picker.observe(on_change, "value")
3033            return picker
3034
3035        def start_change_callback(change: t.Dict[str, datetime.datetime]) -> None:
3036            plan_builder.set_start(change["new"])
3037            self._show_options_after_categorization(
3038                plan_builder, auto_apply, default_catalog, no_prompts=False
3039            )
3040
3041        def end_change_callback(change: t.Dict[str, datetime.datetime]) -> None:
3042            plan_builder.set_end(change["new"])
3043            self._show_options_after_categorization(
3044                plan_builder, auto_apply, default_catalog, no_prompts=False
3045            )
3046
3047        if plan_builder.is_start_and_end_allowed:
3048            add_to_layout_widget(
3049                prompt,
3050                widgets.HBox(
3051                    [
3052                        widgets.Label(
3053                            f"Start {backfill_or_preview} Date:", layout={"width": "8rem"}
3054                        ),
3055                        _date_picker(
3056                            plan_builder, to_date(plan_builder.build().start), start_change_callback
3057                        ),
3058                    ]
3059                ),
3060            )
3061
3062            add_to_layout_widget(
3063                prompt,
3064                widgets.HBox(
3065                    [
3066                        widgets.Label(f"End {backfill_or_preview} Date:", layout={"width": "8rem"}),
3067                        _date_picker(
3068                            plan_builder,
3069                            to_date(plan_builder.build().end),
3070                            end_change_callback,
3071                        ),
3072                    ]
3073                ),
3074            )
3075
3076        self._add_to_dynamic_options(prompt)
3077
3078        if not auto_apply:
3079            button = widgets.Button(
3080                description=f"Apply - {backfill_or_preview} Tables",
3081                disabled=False,
3082                button_style="success",
3083            )
3084            self._add_to_dynamic_options(button)
3085            output = widgets.Output()
3086            self._add_to_dynamic_options(output)
3087
3088            button.plan_builder = plan_builder
3089            button.on_click(self._apply)
3090            button.output = output
3091
3092    def _show_options_after_categorization(
3093        self,
3094        plan_builder: PlanBuilder,
3095        auto_apply: bool,
3096        default_catalog: t.Optional[str],
3097        no_prompts: bool,
3098    ) -> None:
3099        self.dynamic_options_after_categorization_output.children = ()
3100        self.display(self.dynamic_options_after_categorization_output)
3101        super()._show_options_after_categorization(
3102            plan_builder, auto_apply, default_catalog, no_prompts
3103        )
3104
3105    def _add_to_dynamic_options(self, *widgets: widgets.Widget) -> None:
3106        add_to_layout_widget(self.dynamic_options_after_categorization_output, *widgets)
3107
3108    def _get_snapshot_change_category(
3109        self,
3110        snapshot: Snapshot,
3111        plan_builder: PlanBuilder,
3112        auto_apply: bool,
3113        default_catalog: t.Optional[str],
3114    ) -> None:
3115        import ipywidgets as widgets
3116
3117        choice_mapping = self._snapshot_change_choices(
3118            snapshot,
3119            plan_builder.environment_naming_info,
3120            default_catalog,
3121            use_rich_formatting=False,
3122        )
3123        choices = list(choice_mapping)
3124        plan_builder.set_choice(snapshot, choices[0])
3125
3126        def radio_button_selected(change: t.Dict[str, t.Any]) -> None:
3127            plan_builder.set_choice(snapshot, choices[change["owner"].index])
3128            self._show_options_after_categorization(
3129                plan_builder, auto_apply, default_catalog, no_prompts=False
3130            )
3131
3132        radio = widgets.RadioButtons(
3133            options=choice_mapping.values(),
3134            layout={"width": "max-content"},
3135            disabled=False,
3136        )
3137        radio.observe(
3138            radio_button_selected,
3139            "value",
3140        )
3141        self.display(radio)
3142
3143    def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
3144        # We don't log the test results if no tests were ran
3145        if not result.testsRun:
3146            return
3147
3148        import ipywidgets as widgets
3149
3150        divider_length = 70
3151        shared_style = {
3152            "font-size": "11px",
3153            "font-weight": "bold",
3154            "font-family": "Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace",
3155        }
3156
3157        message = (
3158            f"Ran {result.testsRun} tests against {target_dialect} in {result.duration} seconds."
3159        )
3160
3161        if result.wasSuccessful():
3162            success_color = {"color": "#008000"}
3163            header = str(h("span", {"style": shared_style}, "-" * divider_length))
3164            message = str(
3165                h(
3166                    "span",
3167                    {"style": {**shared_style, **success_color}},
3168                    f"Successfully {message}",
3169                )
3170            )
3171            footer = str(h("span", {"style": shared_style}, "=" * divider_length))
3172            self.display(widgets.HTML("<br>".join([header, message, footer])))
3173        else:
3174            output = self._captured_unit_test_results(result)
3175
3176            fail_color = {"color": "#db3737"}
3177            fail_shared_style = {**shared_style, **fail_color}
3178            header = str(h("span", {"style": fail_shared_style}, "-" * divider_length))
3179            message = str(h("span", {"style": fail_shared_style}, "Test Failure Summary"))
3180            fail_and_error_tests = result.get_fail_and_error_tests()
3181            failed_tests = [
3182                str(
3183                    h(
3184                        "span",
3185                        {"style": fail_shared_style},
3186                        f"Failed tests ({len(fail_and_error_tests)}):",
3187                    )
3188                )
3189            ]
3190
3191            for test in fail_and_error_tests:
3192                failed_tests.append(
3193                    str(
3194                        h(
3195                            "span",
3196                            {"style": fail_shared_style},
3197                            f" • {test.model.name}::{test.test_name}",
3198                        )
3199                    )
3200                )
3201            failures = "<br>".join(failed_tests)
3202            footer = str(h("span", {"style": fail_shared_style}, "=" * divider_length))
3203            error_output = widgets.Textarea(output, layout={"height": "300px", "width": "100%"})
3204            test_info = widgets.HTML("<br>".join([header, message, footer, failures, footer]))
3205            self.display(widgets.VBox(children=[test_info, error_output], layout={"width": "100%"}))
3206
3207
3208class CaptureTerminalConsole(TerminalConsole):
3209    """
3210    Captures the output of the terminal console so that it can be extracted out and displayed within other interfaces.
3211    The captured output is cleared out after it is retrieved.
3212
3213    Note: `_prompt` and `_confirm` need to also be overriden to work with the custom interface if you want to use
3214    this console interactively.
3215    """
3216
3217    def __init__(self, console: t.Optional[RichConsole] = None, **kwargs: t.Any) -> None:
3218        super().__init__(console=console, **kwargs)
3219        self._captured_outputs: t.List[str] = []
3220        self._warnings: t.List[str] = []
3221        self._errors: t.List[str] = []
3222
3223    @property
3224    def captured_output(self) -> str:
3225        return "".join(self._captured_outputs)
3226
3227    @property
3228    def captured_warnings(self) -> str:
3229        return "".join(self._warnings)
3230
3231    @property
3232    def captured_errors(self) -> str:
3233        return "".join(self._errors)
3234
3235    def consume_captured_output(self) -> str:
3236        try:
3237            return self.captured_output
3238        finally:
3239            self._captured_outputs = []
3240
3241    def consume_captured_warnings(self) -> str:
3242        try:
3243            return self.captured_warnings
3244        finally:
3245            self._warnings = []
3246
3247    def consume_captured_errors(self) -> str:
3248        try:
3249            return self.captured_errors
3250        finally:
3251            self._errors = []
3252
3253    def log_warning(
3254        self,
3255        short_message: str,
3256        long_message: t.Optional[str] = None,
3257        *args: t.Any,
3258        **kwargs: t.Any,
3259    ) -> None:
3260        if short_message not in self._warnings:
3261            self._warnings.append(short_message)
3262        if kwargs.pop("print", True):
3263            super().log_warning(short_message, long_message)
3264
3265    def log_error(self, message: str, *args: t.Any, **kwargs: t.Any) -> None:
3266        if message not in self._errors:
3267            self._errors.append(message)
3268        if kwargs.pop("print", True):
3269            super().log_error(message)
3270
3271    def log_skipped_models(self, snapshot_names: t.Set[str]) -> None:
3272        if snapshot_names:
3273            self._captured_outputs.append(
3274                "\n".join([f"SKIPPED snapshot {skipped}\n" for skipped in snapshot_names])
3275            )
3276            super().log_skipped_models(snapshot_names)
3277
3278    def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None:
3279        self._errors.extend([str(ex) for ex in errors if str(ex) not in self._errors])
3280        super().log_failed_models(errors)
3281
3282    def _print(self, value: t.Any, **kwargs: t.Any) -> None:
3283        with self.console.capture() as capture:
3284            self.console.print(value, **kwargs)
3285        self._captured_outputs.append(capture.get())
3286
3287
3288class MarkdownConsole(CaptureTerminalConsole):
3289    """
3290    A console that outputs markdown. Currently this is only configured for non-interactive use so for use cases
3291    where you want to display a plan or test results in markdown.
3292    """
3293
3294    CHECK_MARK = ""
3295    AUDIT_PASS_MARK = "passed "
3296    GREEN_AUDIT_PASS_MARK = AUDIT_PASS_MARK
3297    AUDIT_FAIL_MARK = "failed "
3298    AUDIT_PADDING = 7
3299
3300    def __init__(self, **kwargs: t.Any) -> None:
3301        self.alert_block_max_content_length = int(kwargs.pop("alert_block_max_content_length", 500))
3302        self.alert_block_collapsible_threshold = int(
3303            kwargs.pop("alert_block_collapsible_threshold", 200)
3304        )
3305
3306        # capture_only = True: capture but dont print to console
3307        # capture_only = False: capture and also print to console
3308        self.warning_capture_only = kwargs.pop("warning_capture_only", False)
3309        self.error_capture_only = kwargs.pop("error_capture_only", False)
3310
3311        super().__init__(
3312            **{**kwargs, "console": RichConsole(no_color=True, width=kwargs.pop("width", None))}
3313        )
3314
3315    def show_environment_difference_summary(
3316        self,
3317        context_diff: ContextDiff,
3318        no_diff: bool = True,
3319    ) -> None:
3320        """Shows a summary of the environment differences.
3321
3322        Args:
3323            context_diff: The context diff to use to print the summary.
3324            no_diff: Hide the actual environment statements differences.
3325        """
3326        if context_diff.is_new_environment:
3327            msg = (
3328                f"\n**`{context_diff.environment}` environment will be initialized**"
3329                if not context_diff.create_from_env_exists
3330                else f"\n**New environment `{context_diff.environment}` will be created from `{context_diff.create_from}`**"
3331            )
3332            self._print(msg)
3333            if not context_diff.has_snapshot_changes:
3334                return
3335
3336        if not context_diff.has_changes:
3337            self._print(
3338                f"\n**No changes to plan: project files match the `{context_diff.environment}` environment**\n"
3339            )
3340            return
3341
3342        self._print(f"\n**Summary of differences from `{context_diff.environment}`:**")
3343
3344        if context_diff.has_requirement_changes:
3345            self._print(f"\nRequirements:\n{context_diff.requirements_diff()}")
3346
3347        if context_diff.has_environment_statements_changes and not no_diff:
3348            self._print("\nEnvironment statements:\n")
3349            for _, diff in context_diff.environment_statements_diff(
3350                include_python_env=not context_diff.is_new_environment
3351            ):
3352                self._print(diff)
3353
3354    def show_model_difference_summary(
3355        self,
3356        context_diff: ContextDiff,
3357        environment_naming_info: EnvironmentNamingInfo,
3358        default_catalog: t.Optional[str],
3359        no_diff: bool = True,
3360    ) -> None:
3361        """Shows a summary of the model differences.
3362
3363        Args:
3364            context_diff: The context diff to use to print the summary.
3365            environment_naming_info: The environment naming info to reference when printing model names
3366            default_catalog: The default catalog to reference when deciding to remove catalog from display names
3367            no_diff: Hide the actual SQL differences.
3368        """
3369        added_snapshots = {context_diff.snapshots[s_id] for s_id in context_diff.added}
3370        if added_snapshots:
3371            self._print("\n**Added Models:**")
3372            self._print_models_with_threshold(
3373                environment_naming_info, {s for s in added_snapshots if s.is_model}, default_catalog
3374            )
3375
3376        added_snapshot_audits = {s for s in added_snapshots if s.is_audit}
3377        if added_snapshot_audits:
3378            self._print("\n**Added Standalone Audits:**")
3379            for snapshot in sorted(added_snapshot_audits):
3380                self._print(
3381                    f"- `{snapshot.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`"
3382                )
3383
3384        removed_snapshot_table_infos = set(context_diff.removed_snapshots.values())
3385        if removed_snapshot_table_infos:
3386            self._print("\n**Removed Models:**")
3387            self._print_models_with_threshold(
3388                environment_naming_info,
3389                {s for s in removed_snapshot_table_infos if s.is_model},
3390                default_catalog,
3391            )
3392
3393        removed_audit_snapshot_table_infos = {s for s in removed_snapshot_table_infos if s.is_audit}
3394        if removed_audit_snapshot_table_infos:
3395            self._print("\n**Removed Standalone Audits:**")
3396            for snapshot_table_info in sorted(removed_audit_snapshot_table_infos):
3397                self._print(
3398                    f"- `{snapshot_table_info.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`"
3399                )
3400
3401        modified_snapshots = {
3402            current_snapshot for current_snapshot, _ in context_diff.modified_snapshots.values()
3403        }
3404        if modified_snapshots:
3405            self._print_modified_models(
3406                context_diff, modified_snapshots, environment_naming_info, default_catalog, no_diff
3407            )
3408
3409    def _print_models_with_threshold(
3410        self,
3411        environment_naming_info: EnvironmentNamingInfo,
3412        snapshot_table_infos: t.Set[SnapshotInfoLike],
3413        default_catalog: t.Optional[str] = None,
3414    ) -> None:
3415        models = sorted(snapshot_table_infos)
3416        list_length = len(models)
3417        if (
3418            self.verbosity < Verbosity.VERY_VERBOSE
3419            and list_length > self.INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD
3420        ):
3421            self._print(
3422                f"- `{models[0].display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`"
3423            )
3424            self._print(f"- `.... {list_length - 2} more ....`\n")
3425            self._print(
3426                f"- `{models[-1].display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`"
3427            )
3428        else:
3429            for snapshot_table_info in models:
3430                category_str = SNAPSHOT_CHANGE_CATEGORY_STR[snapshot_table_info.change_category]
3431                self._print(
3432                    f"- `{snapshot_table_info.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}` ({category_str})"
3433                )
3434
3435    def _print_modified_models(
3436        self,
3437        context_diff: ContextDiff,
3438        modified_snapshots: t.Set[Snapshot],
3439        environment_naming_info: EnvironmentNamingInfo,
3440        default_catalog: t.Optional[str] = None,
3441        no_diff: bool = True,
3442    ) -> None:
3443        directly_modified = []
3444        indirectly_modified: t.List[Snapshot] = []
3445        metadata_modified = []
3446        for snapshot in modified_snapshots:
3447            if context_diff.directly_modified(snapshot.name):
3448                directly_modified.append(snapshot)
3449            elif context_diff.indirectly_modified(snapshot.name):
3450                indirectly_modified.append(snapshot)
3451            elif context_diff.metadata_updated(snapshot.name):
3452                metadata_modified.append(snapshot)
3453        if directly_modified:
3454            self._print("\n**Directly Modified:**")
3455            for snapshot in sorted(directly_modified):
3456                category_str = SNAPSHOT_CHANGE_CATEGORY_STR[snapshot.change_category]
3457                self._print(
3458                    f"* `{snapshot.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}` ({category_str})"
3459                )
3460
3461                indirectly_modified_children = sorted(
3462                    [s for s in indirectly_modified if snapshot.snapshot_id in s.parents]
3463                )
3464
3465                if not no_diff:
3466                    diff_text = context_diff.text_diff(snapshot.name)
3467                    # sometimes there is no text_diff, like on a seed model where the data has been updated
3468                    if diff_text:
3469                        diff_text = f"\n```diff\n{diff_text}\n```"
3470                        # these are part of a Markdown list, so indent them by 2 spaces to relate them to the current list item
3471                        diff_text_indented = "\n".join(
3472                            [f"  {line}" for line in diff_text.splitlines()]
3473                        )
3474                        self._print(diff_text_indented)
3475                    else:
3476                        if indirectly_modified_children:
3477                            self._print("\n")
3478
3479                if indirectly_modified_children:
3480                    self._print("  Indirectly Modified Children:")
3481                    for child_snapshot in indirectly_modified_children:
3482                        child_category_str = SNAPSHOT_CHANGE_CATEGORY_STR[
3483                            child_snapshot.change_category
3484                        ]
3485                        self._print(
3486                            f"    - `{child_snapshot.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}` ({child_category_str})"
3487                        )
3488                    self._print("\n")
3489
3490        if indirectly_modified:
3491            self._print("\n**Indirectly Modified:**")
3492            self._print_models_with_threshold(
3493                environment_naming_info, set(indirectly_modified), default_catalog
3494            )
3495        if metadata_modified:
3496            self._print("\n**Metadata Updated:**")
3497            for snapshot in sorted(metadata_modified):
3498                self._print(
3499                    f"- `{snapshot.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`"
3500                )
3501
3502    def _show_missing_dates(self, plan: Plan, default_catalog: t.Optional[str]) -> None:
3503        """Displays the models with missing dates."""
3504        missing_intervals = plan.missing_intervals
3505        if not missing_intervals:
3506            return
3507        self._print("\n**Models needing backfill:**")
3508        snapshots = []
3509        for missing in missing_intervals:
3510            snapshot = plan.context_diff.snapshots[missing.snapshot_id]
3511            if not snapshot.is_model:
3512                continue
3513
3514            preview_modifier = ""
3515            if not plan.deployability_index.is_deployable(snapshot):
3516                preview_modifier = " (**preview**)"
3517
3518            display_name = snapshot.display_name(
3519                plan.environment_naming_info,
3520                default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
3521                dialect=self.dialect,
3522            )
3523            snapshots.append(
3524                f"* `{display_name}`: \\[{_format_missing_intervals(snapshot, missing)}]{preview_modifier}"
3525            )
3526
3527        length = len(snapshots)
3528        if (
3529            self.verbosity < Verbosity.VERY_VERBOSE
3530            and length > self.INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD
3531        ):
3532            self._print(snapshots[0])
3533            self._print(f"- `.... {length - 2} more ....`\n")
3534            self._print(snapshots[-1])
3535        else:
3536            for snap in snapshots:
3537                self._print(snap)
3538
3539    def _show_categorized_snapshots(self, plan: Plan, default_catalog: t.Optional[str]) -> None:
3540        context_diff = plan.context_diff
3541        for snapshot in plan.categorized:
3542            if context_diff.directly_modified(snapshot.name):
3543                category_str = SNAPSHOT_CHANGE_CATEGORY_STR[snapshot.change_category]
3544                tree = Tree(
3545                    f"[bold][direct]Directly Modified: {snapshot.display_name(plan.environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)} ({category_str})"
3546                )
3547                indirect_tree = None
3548                for child_sid in sorted(plan.indirectly_modified.get(snapshot.snapshot_id, set())):
3549                    child_snapshot = context_diff.snapshots[child_sid]
3550                    if not indirect_tree:
3551                        indirect_tree = Tree("[indirect]Indirectly Modified Children:")
3552                        tree.add(indirect_tree)
3553                    child_category_str = SNAPSHOT_CHANGE_CATEGORY_STR[
3554                        child_snapshot.change_category
3555                    ]
3556                    indirect_tree.add(
3557                        f"[indirect]{child_snapshot.display_name(plan.environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)} ({child_category_str})"
3558                    )
3559                if indirect_tree:
3560                    indirect_tree = self._limit_model_names(indirect_tree, self.verbosity)
3561            elif context_diff.metadata_updated(snapshot.name):
3562                tree = Tree(
3563                    f"[bold][metadata]Metadata Updated: {snapshot.display_name(plan.environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}"
3564                )
3565            else:
3566                continue
3567
3568            self._print(f"```diff\n{context_diff.text_diff(snapshot.name)}\n```\n")
3569            self._print("```\n")
3570            self._print(tree)
3571            self._print("\n```")
3572
3573    def stop_evaluation_progress(self, success: bool = True) -> None:
3574        super().stop_evaluation_progress(success)
3575        self._print("\n")
3576
3577    def stop_creation_progress(self, success: bool = True) -> None:
3578        super().stop_creation_progress(success)
3579        self._print("\n")
3580
3581    def stop_promotion_progress(self, success: bool = True) -> None:
3582        super().stop_promotion_progress(success)
3583        self._print("\n")
3584
3585    def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None:
3586        super().log_warning(short_message, long_message, print=not self.warning_capture_only)
3587
3588    def log_error(self, message: str) -> None:
3589        super().log_error(message, print=not self.error_capture_only)
3590
3591    def log_success(self, message: str) -> None:
3592        self._print(message)
3593
3594    def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
3595        # We don't log the test results if no tests were ran
3596        if not result.testsRun:
3597            return
3598
3599        message = f"Ran `{result.testsRun}` Tests Against `{target_dialect}`"
3600
3601        if result.wasSuccessful():
3602            self._print(f"**Successfully {message}**\n\n")
3603        else:
3604            self._print("```")
3605            self._log_test_details(result, unittest_char_separator=False)
3606            self._print("```\n\n")
3607
3608            fail_and_error_tests = result.get_fail_and_error_tests()
3609            self._print(f"**{message}**\n")
3610            self._print(f"**Failed tests ({len(fail_and_error_tests)}):**")
3611            for test in fail_and_error_tests:
3612                if isinstance(test, ModelTest):
3613                    self._print(f" • `{test.model.name}`::`{test.test_name}`\n\n")
3614
3615    def log_skipped_models(self, snapshot_names: t.Set[str]) -> None:
3616        if snapshot_names:
3617            self._print(f"**Skipped models**")
3618            for snapshot_name in snapshot_names:
3619                self._print(f"* `{snapshot_name}`")
3620            self._print("")
3621
3622    def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None:
3623        if errors:
3624            self._print("**Failed models**")
3625
3626            error_messages = _format_node_errors(errors)
3627
3628            for node_name, msg in error_messages.items():
3629                self._print(f"* `{node_name}`\n")
3630                self._print("  ```")
3631                self._print(msg)
3632                self._print("  ```")
3633
3634            self._print("")
3635
3636    def show_linter_violations(
3637        self, violations: t.List[RuleViolation], model: Model, is_error: bool = False
3638    ) -> None:
3639        severity = "**errors**" if is_error else "warnings"
3640        violations_msg = "\n".join(f" - {violation}" for violation in violations)
3641        msg = f"\nLinter {severity} for `{model._path}`:\n{violations_msg}\n"
3642
3643        self._print(msg)
3644        if is_error:
3645            self._errors.append(msg)
3646        else:
3647            self._warnings.append(msg)
3648
3649    @property
3650    def captured_warnings(self) -> str:
3651        return self._render_alert_block("WARNING", self._warnings)
3652
3653    @property
3654    def captured_errors(self) -> str:
3655        return self._render_alert_block("CAUTION", self._errors)
3656
3657    def _render_alert_block(self, block_type: str, items: t.List[str]) -> str:
3658        # GitHub Markdown alert syntax, https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts
3659        if items:
3660            item_contents = ""
3661            list_indicator = "- " if len(items) > 1 else ""
3662
3663            for item in items:
3664                item = item.replace("\n", "\n> ")
3665                item_contents += f">\n> {list_indicator}{item}\n"
3666
3667                if len(item_contents) > self.alert_block_max_content_length:
3668                    truncation_msg = (
3669                        "...\n>\n> Truncated. Please check the console for full information.\n"
3670                    )
3671                    item_contents = item_contents[
3672                        0 : self.alert_block_max_content_length - len(truncation_msg)
3673                    ]
3674                    item_contents += truncation_msg
3675                    break
3676
3677            if len(item_contents) > self.alert_block_collapsible_threshold:
3678                item_contents = f"> <details>\n{item_contents}> </details>"
3679
3680            return f"> [!{block_type}]\n{item_contents}\n"
3681
3682        return ""
3683
3684    def _print(self, value: t.Any, **kwargs: t.Any) -> None:
3685        self.console.print(value, **kwargs)
3686        with self.console.capture() as capture:
3687            self.console.print(value, **kwargs)
3688        self._captured_outputs.append(capture.get())
3689
3690
3691class DatabricksMagicConsole(CaptureTerminalConsole):
3692    """
3693    Note: Databricks Magic Console currently does not support progress bars while a plan is being applied. The
3694    NotebookMagicConsole does support progress bars, but they will time out after 5 minutes of execution
3695    and it makes it difficult to see the progress of the plan.
3696    """
3697
3698    def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
3699        super().__init__(*args, **kwargs)
3700        self.evaluation_batch_progress: t.Dict[SnapshotId, t.Tuple[str, int]] = {}
3701        self.promotion_status: t.Tuple[int, int] = (0, 0)
3702        self.model_creation_status: t.Tuple[int, int] = (0, 0)
3703        self.migration_status: t.Tuple[int, int] = (0, 0)
3704
3705    def _print(self, value: t.Any, **kwargs: t.Any) -> None:
3706        super()._print(value, **kwargs)
3707        for captured_output in self._captured_outputs:
3708            print(captured_output)
3709        self.consume_captured_output()
3710
3711    def _prompt(self, message: str, **kwargs: t.Any) -> t.Any:
3712        self._print(message)
3713        return super()._prompt("", **kwargs)
3714
3715    def _confirm(self, message: str, **kwargs: t.Any) -> bool:
3716        message = f"{message} [y/n]"
3717        self._print(message)
3718        return super()._confirm("", **kwargs)
3719
3720    def start_evaluation_progress(
3721        self,
3722        batched_intervals: t.Dict[Snapshot, Intervals],
3723        environment_naming_info: EnvironmentNamingInfo,
3724        default_catalog: t.Optional[str],
3725        audit_only: bool = False,
3726    ) -> None:
3727        self.evaluation_model_batch_sizes = {
3728            snapshot: len(intervals) for snapshot, intervals in batched_intervals.items()
3729        }
3730        self.evaluation_environment_naming_info = environment_naming_info
3731        self.default_catalog = default_catalog
3732
3733    def start_snapshot_evaluation_progress(
3734        self, snapshot: Snapshot, audit_only: bool = False
3735    ) -> None:
3736        if not self.evaluation_batch_progress.get(snapshot.snapshot_id):
3737            display_name = snapshot.display_name(
3738                self.evaluation_environment_naming_info,
3739                self.default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
3740                dialect=self.dialect,
3741            )
3742            self.evaluation_batch_progress[snapshot.snapshot_id] = (display_name, 0)
3743            print(
3744                f"Starting '{display_name}', Total batches: {self.evaluation_model_batch_sizes[snapshot]}"
3745            )
3746
3747    def update_snapshot_evaluation_progress(
3748        self,
3749        snapshot: Snapshot,
3750        interval: Interval,
3751        batch_idx: int,
3752        duration_ms: t.Optional[int],
3753        num_audits_passed: int,
3754        num_audits_failed: int,
3755        audit_only: bool = False,
3756        execution_stats: t.Optional[QueryExecutionStats] = None,
3757        auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None,
3758    ) -> None:
3759        view_name, loaded_batches = self.evaluation_batch_progress[snapshot.snapshot_id]
3760
3761        if audit_only:
3762            print(f"Completed Auditing {view_name}")
3763            return
3764
3765        total_batches = self.evaluation_model_batch_sizes[snapshot]
3766
3767        loaded_batches += 1
3768        self.evaluation_batch_progress[snapshot.snapshot_id] = (view_name, loaded_batches)
3769
3770        finished_loading = loaded_batches == total_batches
3771        status = "Loaded" if finished_loading else "Loading"
3772        print(f"{status} '{view_name}', Completed Batches: {loaded_batches}/{total_batches}")
3773        if finished_loading:
3774            total_finished_loading = len(
3775                [
3776                    s
3777                    for s, total in self.evaluation_model_batch_sizes.items()
3778                    if self.evaluation_batch_progress.get(s.snapshot_id, (None, -1))[1] == total
3779                ]
3780            )
3781            total = len(self.evaluation_batch_progress)
3782            print(f"Completed Loading {total_finished_loading}/{total} Models")
3783
3784    def stop_evaluation_progress(self, success: bool = True) -> None:
3785        self.evaluation_batch_progress = {}
3786        super().stop_evaluation_progress(success)
3787        print(f"Loading {'succeeded' if success else 'failed'}")
3788
3789    def start_creation_progress(
3790        self,
3791        snapshots: t.List[Snapshot],
3792        environment_naming_info: EnvironmentNamingInfo,
3793        default_catalog: t.Optional[str],
3794    ) -> None:
3795        """Indicates that a new creation progress has begun."""
3796        self.model_creation_status = (0, len(snapshots))
3797        print("Starting Creating New Model Versions")
3798
3799    def update_creation_progress(self, snapshot: SnapshotInfoLike) -> None:
3800        """Update the snapshot creation progress."""
3801        num_creations, total_creations = self.model_creation_status
3802        num_creations += 1
3803        self.model_creation_status = (num_creations, total_creations)
3804        if num_creations % 5 == 0:
3805            print(f"Created New Model Versions: {num_creations}/{total_creations}")
3806
3807    def stop_creation_progress(self, success: bool = True) -> None:
3808        """Stop the snapshot creation progress."""
3809        self.model_creation_status = (0, 0)
3810        print(f"New Model Creation {'succeeded' if success else 'failed'}")
3811
3812    def start_promotion_progress(
3813        self,
3814        snapshots: t.List[SnapshotTableInfo],
3815        environment_naming_info: EnvironmentNamingInfo,
3816        default_catalog: t.Optional[str],
3817    ) -> None:
3818        """Indicates that a new snapshot promotion progress has begun."""
3819        self.promotion_status = (0, len(snapshots))
3820        print(f"Virtually Updating '{environment_naming_info.name}'")
3821
3822    def update_promotion_progress(self, snapshot: SnapshotInfoLike, promoted: bool) -> None:
3823        """Update the snapshot promotion progress."""
3824        num_promotions, total_promotions = self.promotion_status
3825        num_promotions += 1
3826        self.promotion_status = (num_promotions, total_promotions)
3827        if num_promotions % 5 == 0:
3828            print(f"Virtually Updated {num_promotions}/{total_promotions}")
3829
3830    def stop_promotion_progress(self, success: bool = True) -> None:
3831        """Stop the snapshot promotion progress."""
3832        self.promotion_status = (0, 0)
3833        print(f"Virtual Update {'succeeded' if success else 'failed'}")
3834
3835    def start_snapshot_migration_progress(self, total_tasks: int) -> None:
3836        """Indicates that a new migration progress has begun."""
3837        self.migration_status = (0, total_tasks)
3838        print("Starting Migration")
3839
3840    def update_snapshot_migration_progress(self, num_tasks: int) -> None:
3841        """Update the migration progress."""
3842        num_migrations, total_migrations = self.migration_status
3843        num_migrations += num_tasks
3844        self.migration_status = (num_migrations, total_migrations)
3845        if num_migrations % 5 == 0:
3846            print(f"Migration Updated {num_migrations}/{total_migrations}")
3847
3848    def log_migration_status(self, success: bool = True) -> None:
3849        """Log the migration status."""
3850        print(f"Migration {'succeeded' if success else 'failed'}")
3851
3852    def stop_snapshot_migration_progress(self, success: bool = True) -> None:
3853        """Stop the migration progress."""
3854        self.migration_status = (0, 0)
3855        print(f"Snapshot migration {'succeeded' if success else 'failed'}")
3856
3857    def start_env_migration_progress(self, total_tasks: int) -> None:
3858        """Indicates that a new migration progress has begun."""
3859        self.env_migration_status = (0, total_tasks)
3860        print("Starting Environment migration")
3861
3862    def update_env_migration_progress(self, num_tasks: int) -> None:
3863        """Update the migration progress."""
3864        num_migrations, total_migrations = self.env_migration_status
3865        num_migrations += num_tasks
3866        self.env_migration_status = (num_migrations, total_migrations)
3867        if num_migrations % 5 == 0:
3868            print(f"Environment migration Updated {num_migrations}/{total_migrations}")
3869
3870    def stop_env_migration_progress(self, success: bool = True) -> None:
3871        """Stop the migration progress."""
3872        self.env_migration_status = (0, 0)
3873        print(f"Environment migration {'succeeded' if success else 'failed'}")
3874
3875
3876class DebuggerTerminalConsole(TerminalConsole):
3877    """A terminal console to use while debugging with no fluff, progress bars, etc."""
3878
3879    def __init__(
3880        self,
3881        console: t.Optional[RichConsole],
3882        *args: t.Any,
3883        dialect: DialectType = None,
3884        ignore_warnings: bool = False,
3885        **kwargs: t.Any,
3886    ) -> None:
3887        self.console: RichConsole = console or srich.console
3888        self.dialect = dialect
3889        self.verbosity = Verbosity.DEFAULT
3890        self.ignore_warnings = ignore_warnings
3891
3892    def _write(self, msg: t.Any, *args: t.Any, **kwargs: t.Any) -> None:
3893        self.console.log(msg, *args, **kwargs)
3894
3895    def start_plan_evaluation(self, plan: EvaluatablePlan) -> None:
3896        self._write("Starting plan", plan.plan_id)
3897
3898    def stop_plan_evaluation(self) -> None:
3899        self._write("Stopping plan")
3900
3901    def start_evaluation_progress(
3902        self,
3903        batched_intervals: t.Dict[Snapshot, Intervals],
3904        environment_naming_info: EnvironmentNamingInfo,
3905        default_catalog: t.Optional[str],
3906        audit_only: bool = False,
3907    ) -> None:
3908        message = "evaluation" if not audit_only else "auditing"
3909        self._write(
3910            f"Starting {message} for {sum(len(intervals) for intervals in batched_intervals.values())} snapshots"
3911        )
3912
3913    def start_snapshot_evaluation_progress(
3914        self, snapshot: Snapshot, audit_only: bool = False
3915    ) -> None:
3916        self._write(f"{'Evaluating' if not audit_only else 'Auditing'} {snapshot.name}")
3917
3918    def update_snapshot_evaluation_progress(
3919        self,
3920        snapshot: Snapshot,
3921        interval: Interval,
3922        batch_idx: int,
3923        duration_ms: t.Optional[int],
3924        num_audits_passed: int,
3925        num_audits_failed: int,
3926        audit_only: bool = False,
3927        execution_stats: t.Optional[QueryExecutionStats] = None,
3928        auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None,
3929    ) -> None:
3930        message = f"Evaluated {snapshot.name} | batch={batch_idx} | duration={duration_ms}ms | num_audits_passed={num_audits_passed} | num_audits_failed={num_audits_failed}"
3931
3932        if auto_restatement_triggers:
3933            message += f" | auto_restatement_triggers=[{', '.join(trigger.name for trigger in auto_restatement_triggers)}]"
3934
3935        if audit_only:
3936            message = f"Audited {snapshot.name} | duration={duration_ms}ms | num_audits_passed={num_audits_passed} | num_audits_failed={num_audits_failed}"
3937
3938        self._write(message)
3939
3940    def stop_evaluation_progress(self, success: bool = True) -> None:
3941        self._write(f"Stopping evaluation with success={success}")
3942
3943    def start_creation_progress(
3944        self,
3945        snapshots: t.List[Snapshot],
3946        environment_naming_info: EnvironmentNamingInfo,
3947        default_catalog: t.Optional[str],
3948    ) -> None:
3949        self._write(f"Starting creation for {len(snapshots)} snapshots")
3950
3951    def update_creation_progress(self, snapshot: SnapshotInfoLike) -> None:
3952        self._write(f"Creating {snapshot.name}")
3953
3954    def stop_creation_progress(self, success: bool = True) -> None:
3955        self._write(f"Stopping creation with success={success}")
3956
3957    def update_cleanup_progress(self, object_name: str) -> None:
3958        self._write(f"Cleaning up {object_name}")
3959
3960    def start_promotion_progress(
3961        self,
3962        snapshots: t.List[SnapshotTableInfo],
3963        environment_naming_info: EnvironmentNamingInfo,
3964        default_catalog: t.Optional[str],
3965    ) -> None:
3966        if snapshots:
3967            self._write(f"Starting promotion for {len(snapshots)} snapshots")
3968
3969    def update_promotion_progress(self, snapshot: SnapshotInfoLike, promoted: bool) -> None:
3970        self._write(f"Promoting {snapshot.name}")
3971
3972    def stop_promotion_progress(self, success: bool = True) -> None:
3973        self._write(f"Stopping promotion with success={success}")
3974
3975    def start_snapshot_migration_progress(self, total_tasks: int) -> None:
3976        self._write(f"Starting migration for {total_tasks} snapshots")
3977
3978    def update_snapshot_migration_progress(self, num_tasks: int) -> None:
3979        self._write(f"Migration {num_tasks}")
3980
3981    def log_migration_status(self, success: bool = True) -> None:
3982        self._write(f"Migration finished with success={success}")
3983
3984    def stop_snapshot_migration_progress(self, success: bool = True) -> None:
3985        self._write(f"Stopping snapshot migration with success={success}")
3986
3987    def start_env_migration_progress(self, total_tasks: int) -> None:
3988        self._write(f"Starting migration for {total_tasks} environments")
3989
3990    def update_env_migration_progress(self, num_tasks: int) -> None:
3991        self._write(f"Environment migration {num_tasks}")
3992
3993    def stop_env_migration_progress(self, success: bool = True) -> None:
3994        self._write(f"Stopping environment migration with success={success}")
3995
3996    def show_environment_difference_summary(
3997        self,
3998        context_diff: ContextDiff,
3999        no_diff: bool = True,
4000    ) -> None:
4001        self._write("Environment Difference Summary:")
4002
4003        if context_diff.has_requirement_changes:
4004            self._write(f"Requirements:\n{context_diff.requirements_diff()}")
4005
4006        if context_diff.has_environment_statements_changes and not no_diff:
4007            self._write("Environment statements:\n")
4008            for _, diff in context_diff.environment_statements_diff(
4009                include_python_env=not context_diff.is_new_environment
4010            ):
4011                self._write(diff)
4012
4013    def show_model_difference_summary(
4014        self,
4015        context_diff: ContextDiff,
4016        environment_naming_info: EnvironmentNamingInfo,
4017        default_catalog: t.Optional[str],
4018        no_diff: bool = True,
4019    ) -> None:
4020        self._write("Model Difference Summary:")
4021
4022        for added in context_diff.new_snapshots:
4023            self._write(f"  Added: {added}")
4024        for removed in context_diff.removed_snapshots:
4025            self._write(f"  Removed: {removed}")
4026        for modified in context_diff.modified_snapshots:
4027            self._write(f"  Modified: {modified}")
4028
4029    def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
4030        self._write("Test Results:", result)
4031
4032    def show_sql(self, sql: str) -> None:
4033        self._write(sql)
4034
4035    def log_status_update(self, message: str) -> None:
4036        self._write(message, style="bold blue")
4037
4038    def log_error(self, message: str) -> None:
4039        self._write(message, style="bold red")
4040
4041    def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None:
4042        logger.warning(long_message or short_message)
4043        if not self.ignore_warnings:
4044            self._write(short_message, style="bold yellow")
4045
4046    def log_success(self, message: str) -> None:
4047        self._write(message, style="bold green")
4048
4049    def loading_start(self, message: t.Optional[str] = None) -> uuid.UUID:
4050        self._write(message)
4051        return uuid.uuid4()
4052
4053    def loading_stop(self, id: uuid.UUID) -> None:
4054        self._write("Done")
4055
4056    def show_schema_diff(self, schema_diff: SchemaDiff) -> None:
4057        self._write(schema_diff)
4058
4059    def show_row_diff(
4060        self, row_diff: RowDiff, show_sample: bool = True, skip_grain_check: bool = False
4061    ) -> None:
4062        self._write(row_diff)
4063
4064    def show_table_diff(
4065        self,
4066        table_diffs: t.List[TableDiff],
4067        show_sample: bool = True,
4068        skip_grain_check: bool = False,
4069        temp_schema: t.Optional[str] = None,
4070    ) -> None:
4071        for table_diff in table_diffs:
4072            self.show_table_diff_summary(table_diff)
4073            self.show_schema_diff(table_diff.schema_diff())
4074            self.show_row_diff(
4075                table_diff.row_diff(temp_schema=temp_schema, skip_grain_check=skip_grain_check),
4076                show_sample=show_sample,
4077                skip_grain_check=skip_grain_check,
4078            )
4079
4080    def update_table_diff_progress(self, model: str) -> None:
4081        self._write(f"Finished table diff for: {model}")
4082
4083    def start_table_diff_progress(self, models_to_diff: int) -> None:
4084        self._write("Table diff started")
4085
4086    def start_table_diff_model_progress(self, model: str) -> None:
4087        self._write(f"Calculating differences for: {model}")
4088
4089    def stop_table_diff_progress(self, success: bool) -> None:
4090        self._write(f"Table diff finished with success={success}")
4091
4092    def show_table_diff_details(
4093        self,
4094        models_to_diff: t.List[str],
4095    ) -> None:
4096        if models_to_diff:
4097            models = "\n".join(models_to_diff)
4098            self._write(f"Models to compare: {models}")
4099
4100    def show_table_diff_summary(self, table_diff: TableDiff) -> None:
4101        if table_diff.model_name:
4102            self._write(f"Model: {table_diff.model_name}")
4103            self._write(f"Source env: {table_diff.source_alias}")
4104            self._write(f"Target env: {table_diff.target_alias}")
4105        self._write(f"Source table: {table_diff.source}")
4106        self._write(f"Target table: {table_diff.target}")
4107        _, _, key_column_names = table_diff.key_columns
4108        keys = ", ".join(key_column_names)
4109        self._write(f"Join On: {keys}")
4110
4111
4112_CONSOLE: Console = NoopConsole()
4113
4114
4115def set_console(console: Console) -> None:
4116    """Sets the console instance."""
4117    global _CONSOLE
4118    _CONSOLE = console
4119
4120
4121def configure_console(**kwargs: t.Any) -> None:
4122    """Configures the console instance."""
4123    global _CONSOLE
4124    _CONSOLE = create_console(**kwargs)
4125
4126
4127def get_console() -> Console:
4128    """Returns the console instance or creates a new one if it hasn't been created yet."""
4129    return _CONSOLE
4130
4131
4132def create_console(
4133    **kwargs: t.Any,
4134) -> TerminalConsole | DatabricksMagicConsole | NotebookMagicConsole:
4135    """
4136    Creates a new console instance that is appropriate for the current runtime environment.
4137
4138    Note: Google Colab environment is untested and currently assumes is compatible with the base
4139    NotebookMagicConsole.
4140    """
4141    from sqlmesh import RuntimeEnv
4142
4143    runtime_env = RuntimeEnv.get()
4144
4145    runtime_env_mapping = {
4146        RuntimeEnv.DATABRICKS: DatabricksMagicConsole,
4147        RuntimeEnv.JUPYTER: NotebookMagicConsole,
4148        RuntimeEnv.TERMINAL: TerminalConsole,
4149        RuntimeEnv.GOOGLE_COLAB: NotebookMagicConsole,
4150        RuntimeEnv.DEBUGGER: DebuggerTerminalConsole,
4151        RuntimeEnv.CI: MarkdownConsole,
4152    }
4153    rich_console_kwargs: t.Dict[str, t.Any] = {"theme": srich.theme}
4154    if runtime_env.is_jupyter or runtime_env.is_google_colab:
4155        rich_console_kwargs["force_jupyter"] = True
4156    return runtime_env_mapping[runtime_env](
4157        **{**{"console": RichConsole(**rich_console_kwargs)}, **kwargs}
4158    )
4159
4160
4161def _format_missing_intervals(snapshot: Snapshot, missing: SnapshotIntervals) -> str:
4162    return (
4163        missing.format_intervals(snapshot.node.interval_unit)
4164        if snapshot.is_incremental
4165        else "recreate view"
4166        if snapshot.is_view
4167        else "full refresh"
4168    )
4169
4170
4171def _format_node_errors(errors: t.List[NodeExecutionFailedError]) -> t.Dict[str, str]:
4172    """Formats a list of node execution errors for display."""
4173
4174    def _format_node_error(ex: NodeExecutionFailedError) -> str:
4175        cause = ex.__cause__ if ex.__cause__ else ex
4176
4177        error_msg = str(cause)
4178
4179        if isinstance(cause, NodeAuditsErrors):
4180            error_msg = _format_audits_errors(cause)
4181        elif not isinstance(cause, (NodeExecutionFailedError, PythonModelEvalError)):
4182            error_msg = "  " + error_msg.replace("\n", "\n  ")
4183            error_msg = (
4184                f"  {cause.__class__.__name__}:\n{error_msg}"  # include error class name in msg
4185            )
4186        error_msg = error_msg.replace("\n", "\n  ")
4187        error_msg = error_msg + "\n" if not error_msg.rstrip(" ").endswith("\n") else error_msg
4188
4189        return error_msg
4190
4191    error_messages = {}
4192
4193    num_fails = len(errors)
4194    for i, error in enumerate(errors):
4195        node_name = ""
4196        if isinstance(error.node, SnapshotId):
4197            node_name = error.node.name
4198        elif hasattr(error.node, "snapshot_name"):
4199            node_name = error.node.snapshot_name
4200
4201        msg = _format_node_error(error)
4202        msg = "  " + msg.replace("\n", "\n  ")
4203        if i == (num_fails - 1):
4204            msg = msg if msg.rstrip(" ").endswith("\n") else msg + "\n"
4205
4206        error_messages[node_name] = msg
4207
4208    return error_messages
4209
4210
4211def _format_audits_errors(error: NodeAuditsErrors) -> str:
4212    error_messages = []
4213    for err in error.errors:
4214        audit_args_sql = []
4215        for arg_name, arg_value in err.audit_args.items():
4216            audit_args_sql.append(f"{arg_name} := {arg_value.sql(dialect=err.adapter_dialect)}")
4217        audit_args_sql_msg = ("\n".join(audit_args_sql) + "\n\n") if audit_args_sql else ""
4218
4219        err_msg = f"'{err.audit_name}' audit error: {err.count} {'row' if err.count == 1 else 'rows'} failed"
4220
4221        query = "\n  ".join(textwrap.wrap(err.sql(err.adapter_dialect), width=LINE_WRAP_WIDTH))
4222        msg = f"{err_msg}\n\nAudit arguments\n  {audit_args_sql_msg}Audit query\n  {query}\n\n"
4223        msg = msg.replace("\n", "\n  ")
4224        error_messages.append(msg)
4225    return "  " + "\n".join(error_messages)
4226
4227
4228def _format_interval(snapshot: Snapshot, interval: Interval) -> str:
4229    """Format an interval with an optional prefix."""
4230    inclusive_interval = make_inclusive(interval[0], interval[1])
4231    if snapshot.model.interval_unit.is_date_granularity:
4232        return f"{to_ds(inclusive_interval[0])} - {to_ds(inclusive_interval[1])}"
4233
4234    if inclusive_interval[0].date() == inclusive_interval[1].date():
4235        # omit end date if interval start/end on same day
4236        return f"{to_ds(inclusive_interval[0])} {inclusive_interval[0].strftime('%H:%M:%S')}-{inclusive_interval[1].strftime('%H:%M:%S')}"
4237
4238    return f"{inclusive_interval[0].strftime('%Y-%m-%d %H:%M:%S')} - {inclusive_interval[1].strftime('%Y-%m-%d %H:%M:%S')}"
4239
4240
4241def _format_signal_interval(snapshot: Snapshot, interval: Interval) -> str:
4242    """Format an interval for signal output (without 'insert' prefix)."""
4243    return _format_interval(snapshot, interval)
4244
4245
4246def _format_evaluation_model_interval(snapshot: Snapshot, interval: Interval) -> str:
4247    """Format an interval for evaluation output (with 'insert' prefix)."""
4248    if snapshot.is_model and (
4249        snapshot.model.kind.is_incremental
4250        or snapshot.model.kind.is_managed
4251        or snapshot.model.kind.is_custom
4252    ):
4253        formatted_interval = _format_interval(snapshot, interval)
4254        return f"insert {formatted_interval}"
4255
4256    return ""
4257
4258
4259def _create_evaluation_model_annotation(
4260    snapshot: Snapshot,
4261    interval_info: t.Optional[str],
4262    execution_stats: t.Optional[QueryExecutionStats],
4263) -> str:
4264    annotation = None
4265    execution_stats_str = ""
4266    if execution_stats:
4267        rows_processed = execution_stats.total_rows_processed
4268        if rows_processed:
4269            # 1.00 and 1.0 to 1
4270            rows_processed_str = metric(rows_processed).replace(".00", "").replace(".0", "")
4271            execution_stats_str += f"{rows_processed_str} row{'s' if rows_processed > 1 else ''}"
4272
4273        bytes_processed = execution_stats.total_bytes_processed
4274        execution_stats_str += (
4275            f"{', ' if execution_stats_str else ''}{naturalsize(bytes_processed, binary=True)}"
4276            if bytes_processed
4277            else ""
4278        )
4279    execution_stats_str = f" ({execution_stats_str})" if execution_stats_str else ""
4280
4281    if snapshot.is_audit:
4282        annotation = "run standalone audit"
4283    if snapshot.is_model:
4284        if snapshot.model.kind.is_external:
4285            annotation = "run external audits"
4286        if snapshot.model.kind.is_view:
4287            annotation = "recreate view"
4288        if snapshot.model.kind.is_seed:
4289            annotation = f"insert seed file{execution_stats_str}"
4290        if snapshot.model.kind.is_full:
4291            annotation = f"full refresh{execution_stats_str}"
4292        if snapshot.model.kind.is_incremental_by_unique_key:
4293            annotation = f"insert/update rows{execution_stats_str}"
4294        if snapshot.model.kind.is_incremental_by_partition:
4295            annotation = f"insert partitions{execution_stats_str}"
4296
4297    if annotation:
4298        return annotation
4299
4300    return f"{interval_info}{execution_stats_str}" if interval_info else ""
4301
4302
4303def _calculate_interval_str_len(
4304    snapshot: Snapshot,
4305    intervals: t.List[Interval],
4306    execution_stats: t.Optional[QueryExecutionStats] = None,
4307) -> int:
4308    interval_str_len = 0
4309    for interval in intervals:
4310        interval_str_len = max(
4311            interval_str_len,
4312            len(
4313                _create_evaluation_model_annotation(
4314                    snapshot, _format_evaluation_model_interval(snapshot, interval), execution_stats
4315                )
4316            ),
4317        )
4318    return interval_str_len
4319
4320
4321def _calculate_audit_str_len(snapshot: Snapshot, audit_padding: int = 0) -> int:
4322    # The annotation includes audit results. We cannot build the audits result string
4323    # until after evaluation occurs, but we must determine the annotation column width here.
4324    # Therefore, we add enough padding for the longest possible audits result string.
4325    audit_str_len = 0
4326    audit_base_str_len = len(f", audits ") + 1  # +1 for check/X
4327    if snapshot.is_audit:
4328        # +1 for "1" audit count, +1 for red X
4329        audit_str_len = max(
4330            audit_str_len, audit_base_str_len + (2 if not snapshot.audit.blocking else 1)
4331        )
4332    if snapshot.is_model and snapshot.model.audits:
4333        num_audits = len(snapshot.model.audits_with_args)
4334        num_nonblocking_audits = sum(
4335            1
4336            for audit in snapshot.model.audits_with_args
4337            if not audit[0].blocking
4338            or ("blocking" in audit[1] and audit[1]["blocking"] == exp.false())
4339        )
4340        if num_audits == 1:
4341            # +1 for "1" audit count, +1 for red X
4342            # if audit_padding is > 0 we're using "failed" instead of red X
4343            audit_len = (
4344                audit_base_str_len
4345                + (2 if num_nonblocking_audits else 1)
4346                + (
4347                    audit_padding - 1
4348                    if num_nonblocking_audits and audit_padding > 0
4349                    else audit_padding
4350                )
4351            )
4352        else:
4353            audit_len = audit_base_str_len + len(str(num_audits)) + audit_padding
4354            if num_nonblocking_audits:
4355                # +1 for space, +1 for red X
4356                # if audit_padding is > 0 we're using "failed" instead of red X
4357                audit_len += (
4358                    len(str(num_nonblocking_audits))
4359                    + 2
4360                    + (audit_padding - 1 if audit_padding > 0 else audit_padding)
4361                )
4362        audit_str_len = max(audit_str_len, audit_len)
4363    return audit_str_len
4364
4365
4366def _calculate_annotation_str_len(
4367    batched_intervals: t.Dict[Snapshot, t.List[Interval]],
4368    audit_padding: int = 0,
4369    execution_stats_len: int = 0,
4370) -> int:
4371    annotation_str_len = 0
4372    for snapshot, intervals in batched_intervals.items():
4373        annotation_str_len = max(
4374            annotation_str_len,
4375            _calculate_interval_str_len(snapshot, intervals)
4376            + _calculate_audit_str_len(snapshot, audit_padding)
4377            + execution_stats_len,
4378        )
4379    return annotation_str_len
logger = <Logger sqlmesh.core.console (WARNING)>
SNAPSHOT_CHANGE_CATEGORY_STR = {None: 'Unknown', BREAKING: 'Breaking', NON_BREAKING: 'Non-breaking', FORWARD_ONLY: 'Forward-only', INDIRECT_BREAKING: 'Indirect Breaking', INDIRECT_NON_BREAKING: 'Indirect Non-breaking', METADATA: 'Metadata'}
PROGRESS_BAR_WIDTH = 40
LINE_WRAP_WIDTH = 100
class LinterConsole(abc.ABC):
89class LinterConsole(abc.ABC):
90    """Console for displaying linter violations"""
91
92    @abc.abstractmethod
93    def show_linter_violations(
94        self, violations: t.List[RuleViolation], model: Model, is_error: bool = False
95    ) -> None:
96        """Prints all linter violations depending on their severity"""

Console for displaying linter violations

@abc.abstractmethod
def show_linter_violations( self, violations: List[sqlmesh.core.linter.rule.RuleViolation], model: Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel], is_error: bool = False) -> None:
92    @abc.abstractmethod
93    def show_linter_violations(
94        self, violations: t.List[RuleViolation], model: Model, is_error: bool = False
95    ) -> None:
96        """Prints all linter violations depending on their severity"""

Prints all linter violations depending on their severity

class StateExporterConsole(abc.ABC):
 99class StateExporterConsole(abc.ABC):
100    """Console for describing a state export"""
101
102    @abc.abstractmethod
103    def start_state_export(
104        self,
105        output_file: Path,
106        gateway: t.Optional[str] = None,
107        state_connection_config: t.Optional[ConnectionConfig] = None,
108        environment_names: t.Optional[t.List[str]] = None,
109        local_only: bool = False,
110        confirm: bool = True,
111    ) -> bool:
112        """State a state export"""
113
114    @abc.abstractmethod
115    def update_state_export_progress(
116        self,
117        version_count: t.Optional[int] = None,
118        versions_complete: bool = False,
119        snapshot_count: t.Optional[int] = None,
120        snapshots_complete: bool = False,
121        environment_count: t.Optional[int] = None,
122        environments_complete: bool = False,
123    ) -> None:
124        """Update the state export progress"""
125
126    @abc.abstractmethod
127    def stop_state_export(self, success: bool, output_file: Path) -> None:
128        """Finish a state export"""

Console for describing a state export

@abc.abstractmethod
def start_state_export( self, output_file: pathlib.Path, gateway: Optional[str] = None, state_connection_config: Optional[sqlmesh.core.config.connection.ConnectionConfig] = None, environment_names: Optional[List[str]] = None, local_only: bool = False, confirm: bool = True) -> bool:
102    @abc.abstractmethod
103    def start_state_export(
104        self,
105        output_file: Path,
106        gateway: t.Optional[str] = None,
107        state_connection_config: t.Optional[ConnectionConfig] = None,
108        environment_names: t.Optional[t.List[str]] = None,
109        local_only: bool = False,
110        confirm: bool = True,
111    ) -> bool:
112        """State a state export"""

State a state export

@abc.abstractmethod
def update_state_export_progress( self, version_count: Optional[int] = None, versions_complete: bool = False, snapshot_count: Optional[int] = None, snapshots_complete: bool = False, environment_count: Optional[int] = None, environments_complete: bool = False) -> None:
114    @abc.abstractmethod
115    def update_state_export_progress(
116        self,
117        version_count: t.Optional[int] = None,
118        versions_complete: bool = False,
119        snapshot_count: t.Optional[int] = None,
120        snapshots_complete: bool = False,
121        environment_count: t.Optional[int] = None,
122        environments_complete: bool = False,
123    ) -> None:
124        """Update the state export progress"""

Update the state export progress

@abc.abstractmethod
def stop_state_export(self, success: bool, output_file: pathlib.Path) -> None:
126    @abc.abstractmethod
127    def stop_state_export(self, success: bool, output_file: Path) -> None:
128        """Finish a state export"""

Finish a state export

class StateImporterConsole(abc.ABC):
131class StateImporterConsole(abc.ABC):
132    """Console for describing a state import"""
133
134    @abc.abstractmethod
135    def start_state_import(
136        self,
137        input_file: Path,
138        gateway: str,
139        state_connection_config: ConnectionConfig,
140        clear: bool = False,
141        confirm: bool = True,
142    ) -> bool:
143        """Start a state import"""
144
145    @abc.abstractmethod
146    def update_state_import_progress(
147        self,
148        timestamp: t.Optional[str] = None,
149        state_file_version: t.Optional[int] = None,
150        versions: t.Optional[Versions] = None,
151        snapshot_count: t.Optional[int] = None,
152        snapshots_complete: bool = False,
153        environment_count: t.Optional[int] = None,
154        environments_complete: bool = False,
155    ) -> None:
156        """Update the state import process"""
157
158    @abc.abstractmethod
159    def stop_state_import(self, success: bool, input_file: Path) -> None:
160        """Finish a state import"""

Console for describing a state import

@abc.abstractmethod
def start_state_import( self, input_file: pathlib.Path, gateway: str, state_connection_config: sqlmesh.core.config.connection.ConnectionConfig, clear: bool = False, confirm: bool = True) -> bool:
134    @abc.abstractmethod
135    def start_state_import(
136        self,
137        input_file: Path,
138        gateway: str,
139        state_connection_config: ConnectionConfig,
140        clear: bool = False,
141        confirm: bool = True,
142    ) -> bool:
143        """Start a state import"""

Start a state import

@abc.abstractmethod
def update_state_import_progress( self, timestamp: Optional[str] = None, state_file_version: Optional[int] = None, versions: Optional[sqlmesh.core.state_sync.base.Versions] = None, snapshot_count: Optional[int] = None, snapshots_complete: bool = False, environment_count: Optional[int] = None, environments_complete: bool = False) -> None:
145    @abc.abstractmethod
146    def update_state_import_progress(
147        self,
148        timestamp: t.Optional[str] = None,
149        state_file_version: t.Optional[int] = None,
150        versions: t.Optional[Versions] = None,
151        snapshot_count: t.Optional[int] = None,
152        snapshots_complete: bool = False,
153        environment_count: t.Optional[int] = None,
154        environments_complete: bool = False,
155    ) -> None:
156        """Update the state import process"""

Update the state import process

@abc.abstractmethod
def stop_state_import(self, success: bool, input_file: pathlib.Path) -> None:
158    @abc.abstractmethod
159    def stop_state_import(self, success: bool, input_file: Path) -> None:
160        """Finish a state import"""

Finish a state import

class JanitorConsole(abc.ABC):
163class JanitorConsole(abc.ABC):
164    """Console for describing a janitor / snapshot cleanup run"""
165
166    @abc.abstractmethod
167    def start_cleanup(self, ignore_ttl: bool) -> bool:
168        """Start a janitor / snapshot cleanup run.
169
170        Args:
171            ignore_ttl: Indicates that the user wants to ignore the snapshot TTL and clean up everything not promoted to an environment
172
173        Returns:
174            Whether or not the cleanup run should proceed
175        """
176
177    @abc.abstractmethod
178    def update_cleanup_progress(self, object_name: str) -> None:
179        """Update the snapshot cleanup progress."""
180
181    @abc.abstractmethod
182    def stop_cleanup(self, success: bool = True) -> None:
183        """Indicates the janitor / snapshot cleanup run has ended
184
185        Args:
186            success: Whether or not the cleanup completed successfully
187        """

Console for describing a janitor / snapshot cleanup run

@abc.abstractmethod
def start_cleanup(self, ignore_ttl: bool) -> bool:
166    @abc.abstractmethod
167    def start_cleanup(self, ignore_ttl: bool) -> bool:
168        """Start a janitor / snapshot cleanup run.
169
170        Args:
171            ignore_ttl: Indicates that the user wants to ignore the snapshot TTL and clean up everything not promoted to an environment
172
173        Returns:
174            Whether or not the cleanup run should proceed
175        """

Start a janitor / snapshot cleanup run.

Arguments:
  • ignore_ttl: Indicates that the user wants to ignore the snapshot TTL and clean up everything not promoted to an environment
Returns:

Whether or not the cleanup run should proceed

@abc.abstractmethod
def update_cleanup_progress(self, object_name: str) -> None:
177    @abc.abstractmethod
178    def update_cleanup_progress(self, object_name: str) -> None:
179        """Update the snapshot cleanup progress."""

Update the snapshot cleanup progress.

@abc.abstractmethod
def stop_cleanup(self, success: bool = True) -> None:
181    @abc.abstractmethod
182    def stop_cleanup(self, success: bool = True) -> None:
183        """Indicates the janitor / snapshot cleanup run has ended
184
185        Args:
186            success: Whether or not the cleanup completed successfully
187        """

Indicates the janitor / snapshot cleanup run has ended

Arguments:
  • success: Whether or not the cleanup completed successfully
class DestroyConsole(abc.ABC):
190class DestroyConsole(abc.ABC):
191    """Console for describing a destroy operation"""
192
193    @abc.abstractmethod
194    def start_destroy(
195        self,
196        schemas_to_delete: t.Optional[t.Set[str]] = None,
197        views_to_delete: t.Optional[t.Set[str]] = None,
198        tables_to_delete: t.Optional[t.Set[str]] = None,
199    ) -> bool:
200        """Start a destroy operation.
201
202        Args:
203            schemas_to_delete: Set of schemas that will be deleted
204            views_to_delete: Set of views that will be deleted
205            tables_to_delete: Set of tables that will be deleted
206
207        Returns:
208            Whether or not the destroy operation should proceed
209        """
210
211    @abc.abstractmethod
212    def stop_destroy(self, success: bool = True) -> None:
213        """Indicates the destroy operation has ended
214
215        Args:
216            success: Whether or not the cleanup completed successfully
217        """

Console for describing a destroy operation

@abc.abstractmethod
def start_destroy( self, schemas_to_delete: Optional[Set[str]] = None, views_to_delete: Optional[Set[str]] = None, tables_to_delete: Optional[Set[str]] = None) -> bool:
193    @abc.abstractmethod
194    def start_destroy(
195        self,
196        schemas_to_delete: t.Optional[t.Set[str]] = None,
197        views_to_delete: t.Optional[t.Set[str]] = None,
198        tables_to_delete: t.Optional[t.Set[str]] = None,
199    ) -> bool:
200        """Start a destroy operation.
201
202        Args:
203            schemas_to_delete: Set of schemas that will be deleted
204            views_to_delete: Set of views that will be deleted
205            tables_to_delete: Set of tables that will be deleted
206
207        Returns:
208            Whether or not the destroy operation should proceed
209        """

Start a destroy operation.

Arguments:
  • schemas_to_delete: Set of schemas that will be deleted
  • views_to_delete: Set of views that will be deleted
  • tables_to_delete: Set of tables that will be deleted
Returns:

Whether or not the destroy operation should proceed

@abc.abstractmethod
def stop_destroy(self, success: bool = True) -> None:
211    @abc.abstractmethod
212    def stop_destroy(self, success: bool = True) -> None:
213        """Indicates the destroy operation has ended
214
215        Args:
216            success: Whether or not the cleanup completed successfully
217        """

Indicates the destroy operation has ended

Arguments:
  • success: Whether or not the cleanup completed successfully
class EnvironmentsConsole(abc.ABC):
220class EnvironmentsConsole(abc.ABC):
221    """Console for displaying environments"""
222
223    @abc.abstractmethod
224    def print_environments(self, environments_summary: t.List[EnvironmentSummary]) -> None:
225        """Prints all environment names along with expiry datetime."""
226
227    @abc.abstractmethod
228    def show_intervals(self, snapshot_intervals: t.Dict[Snapshot, SnapshotIntervals]) -> None:
229        """Show ready intervals"""

Console for displaying environments

@abc.abstractmethod
def print_environments( self, environments_summary: List[sqlmesh.core.environment.EnvironmentSummary]) -> None:
223    @abc.abstractmethod
224    def print_environments(self, environments_summary: t.List[EnvironmentSummary]) -> None:
225        """Prints all environment names along with expiry datetime."""

Prints all environment names along with expiry datetime.

@abc.abstractmethod
def show_intervals( self, snapshot_intervals: Dict[sqlmesh.core.snapshot.definition.Snapshot, sqlmesh.core.plan.definition.SnapshotIntervals]) -> None:
227    @abc.abstractmethod
228    def show_intervals(self, snapshot_intervals: t.Dict[Snapshot, SnapshotIntervals]) -> None:
229        """Show ready intervals"""

Show ready intervals

class DifferenceConsole(abc.ABC):
232class DifferenceConsole(abc.ABC):
233    """Console for displaying environment differences"""
234
235    @abc.abstractmethod
236    def show_environment_difference_summary(
237        self,
238        context_diff: ContextDiff,
239        no_diff: bool = True,
240    ) -> None:
241        """Displays a summary of differences for the environment."""
242
243    @abc.abstractmethod
244    def show_model_difference_summary(
245        self,
246        context_diff: ContextDiff,
247        environment_naming_info: EnvironmentNamingInfo,
248        default_catalog: t.Optional[str],
249        no_diff: bool = True,
250    ) -> None:
251        """Displays a summary of differences for the given models."""

Console for displaying environment differences

@abc.abstractmethod
def show_environment_difference_summary( self, context_diff: sqlmesh.core.context_diff.ContextDiff, no_diff: bool = True) -> None:
235    @abc.abstractmethod
236    def show_environment_difference_summary(
237        self,
238        context_diff: ContextDiff,
239        no_diff: bool = True,
240    ) -> None:
241        """Displays a summary of differences for the environment."""

Displays a summary of differences for the environment.

@abc.abstractmethod
def show_model_difference_summary( self, context_diff: sqlmesh.core.context_diff.ContextDiff, environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str], no_diff: bool = True) -> None:
243    @abc.abstractmethod
244    def show_model_difference_summary(
245        self,
246        context_diff: ContextDiff,
247        environment_naming_info: EnvironmentNamingInfo,
248        default_catalog: t.Optional[str],
249        no_diff: bool = True,
250    ) -> None:
251        """Displays a summary of differences for the given models."""

Displays a summary of differences for the given models.

class TableDiffConsole(abc.ABC):
254class TableDiffConsole(abc.ABC):
255    """Console for displaying table differences"""
256
257    @abc.abstractmethod
258    def show_table_diff(
259        self,
260        table_diffs: t.List[TableDiff],
261        show_sample: bool = True,
262        skip_grain_check: bool = False,
263        temp_schema: t.Optional[str] = None,
264    ) -> None:
265        """Display the table diff between two or multiple tables."""
266
267    @abc.abstractmethod
268    def update_table_diff_progress(self, model: str) -> None:
269        """Update table diff progress bar"""
270
271    @abc.abstractmethod
272    def start_table_diff_progress(self, models_to_diff: int) -> None:
273        """Start table diff progress bar"""
274
275    @abc.abstractmethod
276    def start_table_diff_model_progress(self, model: str) -> None:
277        """Start table diff model progress"""
278
279    @abc.abstractmethod
280    def stop_table_diff_progress(self, success: bool) -> None:
281        """Stop table diff progress bar"""
282
283    @abc.abstractmethod
284    def show_table_diff_details(
285        self,
286        models_to_diff: t.List[str],
287    ) -> None:
288        """Display information about which tables are going to be diffed"""
289
290    @abc.abstractmethod
291    def show_table_diff_summary(self, table_diff: TableDiff) -> None:
292        """Display information about the tables being diffed and how they are being joined"""
293
294    @abc.abstractmethod
295    def show_schema_diff(self, schema_diff: SchemaDiff) -> None:
296        """Show table schema diff."""
297
298    @abc.abstractmethod
299    def show_row_diff(
300        self, row_diff: RowDiff, show_sample: bool = True, skip_grain_check: bool = False
301    ) -> None:
302        """Show table summary diff."""

Console for displaying table differences

@abc.abstractmethod
def show_table_diff( self, table_diffs: List[sqlmesh.core.table_diff.TableDiff], show_sample: bool = True, skip_grain_check: bool = False, temp_schema: Optional[str] = None) -> None:
257    @abc.abstractmethod
258    def show_table_diff(
259        self,
260        table_diffs: t.List[TableDiff],
261        show_sample: bool = True,
262        skip_grain_check: bool = False,
263        temp_schema: t.Optional[str] = None,
264    ) -> None:
265        """Display the table diff between two or multiple tables."""

Display the table diff between two or multiple tables.

@abc.abstractmethod
def update_table_diff_progress(self, model: str) -> None:
267    @abc.abstractmethod
268    def update_table_diff_progress(self, model: str) -> None:
269        """Update table diff progress bar"""

Update table diff progress bar

@abc.abstractmethod
def start_table_diff_progress(self, models_to_diff: int) -> None:
271    @abc.abstractmethod
272    def start_table_diff_progress(self, models_to_diff: int) -> None:
273        """Start table diff progress bar"""

Start table diff progress bar

@abc.abstractmethod
def start_table_diff_model_progress(self, model: str) -> None:
275    @abc.abstractmethod
276    def start_table_diff_model_progress(self, model: str) -> None:
277        """Start table diff model progress"""

Start table diff model progress

@abc.abstractmethod
def stop_table_diff_progress(self, success: bool) -> None:
279    @abc.abstractmethod
280    def stop_table_diff_progress(self, success: bool) -> None:
281        """Stop table diff progress bar"""

Stop table diff progress bar

@abc.abstractmethod
def show_table_diff_details(self, models_to_diff: List[str]) -> None:
283    @abc.abstractmethod
284    def show_table_diff_details(
285        self,
286        models_to_diff: t.List[str],
287    ) -> None:
288        """Display information about which tables are going to be diffed"""

Display information about which tables are going to be diffed

@abc.abstractmethod
def show_table_diff_summary(self, table_diff: sqlmesh.core.table_diff.TableDiff) -> None:
290    @abc.abstractmethod
291    def show_table_diff_summary(self, table_diff: TableDiff) -> None:
292        """Display information about the tables being diffed and how they are being joined"""

Display information about the tables being diffed and how they are being joined

@abc.abstractmethod
def show_schema_diff(self, schema_diff: sqlmesh.core.table_diff.SchemaDiff) -> None:
294    @abc.abstractmethod
295    def show_schema_diff(self, schema_diff: SchemaDiff) -> None:
296        """Show table schema diff."""

Show table schema diff.

@abc.abstractmethod
def show_row_diff( self, row_diff: sqlmesh.core.table_diff.RowDiff, show_sample: bool = True, skip_grain_check: bool = False) -> None:
298    @abc.abstractmethod
299    def show_row_diff(
300        self, row_diff: RowDiff, show_sample: bool = True, skip_grain_check: bool = False
301    ) -> None:
302        """Show table summary diff."""

Show table summary diff.

class BaseConsole(abc.ABC):
305class BaseConsole(abc.ABC):
306    @abc.abstractmethod
307    def log_error(self, message: str, *args: t.Any, **kwargs: t.Any) -> None:
308        """Display error info to the user."""
309
310    @abc.abstractmethod
311    def log_warning(
312        self,
313        short_message: str,
314        long_message: t.Optional[str] = None,
315        *args: t.Any,
316        **kwargs: t.Any,
317    ) -> None:
318        """Display warning info to the user.
319
320        Args:
321            short_message: The warning message to print to console.
322            long_message: The warning message to log to file. If not provided, `short_message` is used.
323        """
324
325    @abc.abstractmethod
326    def log_success(self, message: str) -> None:
327        """Display a general successful message to the user."""

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

@abc.abstractmethod
def log_error(self, message: str, *args: Any, **kwargs: Any) -> None:
306    @abc.abstractmethod
307    def log_error(self, message: str, *args: t.Any, **kwargs: t.Any) -> None:
308        """Display error info to the user."""

Display error info to the user.

@abc.abstractmethod
def log_warning( self, short_message: str, long_message: Optional[str] = None, *args: Any, **kwargs: Any) -> None:
310    @abc.abstractmethod
311    def log_warning(
312        self,
313        short_message: str,
314        long_message: t.Optional[str] = None,
315        *args: t.Any,
316        **kwargs: t.Any,
317    ) -> None:
318        """Display warning info to the user.
319
320        Args:
321            short_message: The warning message to print to console.
322            long_message: The warning message to log to file. If not provided, `short_message` is used.
323        """

Display warning info to the user.

Arguments:
  • short_message: The warning message to print to console.
  • long_message: The warning message to log to file. If not provided, short_message is used.
@abc.abstractmethod
def log_success(self, message: str) -> None:
325    @abc.abstractmethod
326    def log_success(self, message: str) -> None:
327        """Display a general successful message to the user."""

Display a general successful message to the user.

class PlanBuilderConsole(BaseConsole, abc.ABC):
330class PlanBuilderConsole(BaseConsole, abc.ABC):
331    @abc.abstractmethod
332    def log_destructive_change(
333        self,
334        snapshot_name: str,
335        alter_operations: t.List[TableAlterOperation],
336        dialect: str,
337        error: bool = True,
338    ) -> None:
339        """Display a destructive change error or warning to the user."""
340
341    @abc.abstractmethod
342    def log_additive_change(
343        self,
344        snapshot_name: str,
345        alter_operations: t.List[TableAlterOperation],
346        dialect: str,
347        error: bool = True,
348    ) -> None:
349        """Display an additive change error or warning to the user."""

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

@abc.abstractmethod
def log_destructive_change( self, snapshot_name: str, alter_operations: List[sqlmesh.core.schema_diff.TableAlterOperation], dialect: str, error: bool = True) -> None:
331    @abc.abstractmethod
332    def log_destructive_change(
333        self,
334        snapshot_name: str,
335        alter_operations: t.List[TableAlterOperation],
336        dialect: str,
337        error: bool = True,
338    ) -> None:
339        """Display a destructive change error or warning to the user."""

Display a destructive change error or warning to the user.

@abc.abstractmethod
def log_additive_change( self, snapshot_name: str, alter_operations: List[sqlmesh.core.schema_diff.TableAlterOperation], dialect: str, error: bool = True) -> None:
341    @abc.abstractmethod
342    def log_additive_change(
343        self,
344        snapshot_name: str,
345        alter_operations: t.List[TableAlterOperation],
346        dialect: str,
347        error: bool = True,
348    ) -> None:
349        """Display an additive change error or warning to the user."""

Display an additive change error or warning to the user.

class UnitTestConsole(abc.ABC):
352class UnitTestConsole(abc.ABC):
353    @abc.abstractmethod
354    def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
355        """Display the test result and output.
356
357        Args:
358            result: The unittest test result that contains metrics like num success, fails, ect.
359            target_dialect: The dialect that tests were run against. Assumes all tests run against the same dialect.
360        """

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

@abc.abstractmethod
def log_test_results( self, result: sqlmesh.core.test.result.ModelTextTestResult, target_dialect: str) -> None:
353    @abc.abstractmethod
354    def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
355        """Display the test result and output.
356
357        Args:
358            result: The unittest test result that contains metrics like num success, fails, ect.
359            target_dialect: The dialect that tests were run against. Assumes all tests run against the same dialect.
360        """

Display the test result and output.

Arguments:
  • result: The unittest test result that contains metrics like num success, fails, ect.
  • target_dialect: The dialect that tests were run against. Assumes all tests run against the same dialect.
class SignalConsole(abc.ABC):
363class SignalConsole(abc.ABC):
364    @abc.abstractmethod
365    def start_signal_progress(
366        self,
367        snapshot: Snapshot,
368        default_catalog: t.Optional[str],
369        environment_naming_info: EnvironmentNamingInfo,
370    ) -> None:
371        """Indicates that signal checking has begun for a snapshot."""
372
373    @abc.abstractmethod
374    def update_signal_progress(
375        self,
376        snapshot: Snapshot,
377        signal_name: str,
378        signal_idx: int,
379        total_signals: int,
380        ready_intervals: Intervals,
381        check_intervals: Intervals,
382        duration: float,
383    ) -> None:
384        """Updates the signal checking progress."""
385
386    @abc.abstractmethod
387    def stop_signal_progress(self) -> None:
388        """Indicates that signal checking has completed for a snapshot."""

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

@abc.abstractmethod
def start_signal_progress( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, default_catalog: Optional[str], environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo) -> None:
364    @abc.abstractmethod
365    def start_signal_progress(
366        self,
367        snapshot: Snapshot,
368        default_catalog: t.Optional[str],
369        environment_naming_info: EnvironmentNamingInfo,
370    ) -> None:
371        """Indicates that signal checking has begun for a snapshot."""

Indicates that signal checking has begun for a snapshot.

@abc.abstractmethod
def update_signal_progress( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, signal_name: str, signal_idx: int, total_signals: int, ready_intervals: List[Tuple[int, int]], check_intervals: List[Tuple[int, int]], duration: float) -> None:
373    @abc.abstractmethod
374    def update_signal_progress(
375        self,
376        snapshot: Snapshot,
377        signal_name: str,
378        signal_idx: int,
379        total_signals: int,
380        ready_intervals: Intervals,
381        check_intervals: Intervals,
382        duration: float,
383    ) -> None:
384        """Updates the signal checking progress."""

Updates the signal checking progress.

@abc.abstractmethod
def stop_signal_progress(self) -> None:
386    @abc.abstractmethod
387    def stop_signal_progress(self) -> None:
388        """Indicates that signal checking has completed for a snapshot."""

Indicates that signal checking has completed for a snapshot.

391class Console(
392    SignalConsole,
393    PlanBuilderConsole,
394    LinterConsole,
395    StateExporterConsole,
396    StateImporterConsole,
397    JanitorConsole,
398    DestroyConsole,
399    EnvironmentsConsole,
400    DifferenceConsole,
401    TableDiffConsole,
402    BaseConsole,
403    UnitTestConsole,
404    abc.ABC,
405):
406    """Abstract base class for defining classes used for displaying information to the user and also interact
407    with them when their input is needed."""
408
409    INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD = 10
410
411    @abc.abstractmethod
412    def start_plan_evaluation(self, plan: EvaluatablePlan) -> None:
413        """Indicates that a new evaluation has begun."""
414
415    @abc.abstractmethod
416    def stop_plan_evaluation(self) -> None:
417        """Indicates that the evaluation has ended."""
418
419    @abc.abstractmethod
420    def start_evaluation_progress(
421        self,
422        batched_intervals: t.Dict[Snapshot, Intervals],
423        environment_naming_info: EnvironmentNamingInfo,
424        default_catalog: t.Optional[str],
425        audit_only: bool = False,
426    ) -> None:
427        """Indicates that a new snapshot evaluation/auditing progress has begun."""
428
429    @abc.abstractmethod
430    def start_snapshot_evaluation_progress(
431        self, snapshot: Snapshot, audit_only: bool = False
432    ) -> None:
433        """Starts the snapshot evaluation progress."""
434
435    @abc.abstractmethod
436    def update_snapshot_evaluation_progress(
437        self,
438        snapshot: Snapshot,
439        interval: Interval,
440        batch_idx: int,
441        duration_ms: t.Optional[int],
442        num_audits_passed: int,
443        num_audits_failed: int,
444        audit_only: bool = False,
445        execution_stats: t.Optional[QueryExecutionStats] = None,
446        auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None,
447    ) -> None:
448        """Updates the snapshot evaluation progress."""
449
450    @abc.abstractmethod
451    def stop_evaluation_progress(self, success: bool = True) -> None:
452        """Stops the snapshot evaluation progress."""
453
454    @abc.abstractmethod
455    def start_creation_progress(
456        self,
457        snapshots: t.List[Snapshot],
458        environment_naming_info: EnvironmentNamingInfo,
459        default_catalog: t.Optional[str],
460    ) -> None:
461        """Indicates that a new snapshot creation progress has begun."""
462
463    @abc.abstractmethod
464    def update_creation_progress(self, snapshot: SnapshotInfoLike) -> None:
465        """Update the snapshot creation progress."""
466
467    @abc.abstractmethod
468    def stop_creation_progress(self, success: bool = True) -> None:
469        """Stop the snapshot creation progress."""
470
471    @abc.abstractmethod
472    def start_promotion_progress(
473        self,
474        snapshots: t.List[SnapshotTableInfo],
475        environment_naming_info: EnvironmentNamingInfo,
476        default_catalog: t.Optional[str],
477    ) -> None:
478        """Indicates that a new snapshot promotion progress has begun."""
479
480    @abc.abstractmethod
481    def update_promotion_progress(self, snapshot: SnapshotInfoLike, promoted: bool) -> None:
482        """Update the snapshot promotion progress."""
483
484    @abc.abstractmethod
485    def stop_promotion_progress(self, success: bool = True) -> None:
486        """Stop the snapshot promotion progress."""
487
488    @abc.abstractmethod
489    def start_snapshot_migration_progress(self, total_tasks: int) -> None:
490        """Indicates that a new snapshot migration progress has begun."""
491
492    @abc.abstractmethod
493    def update_snapshot_migration_progress(self, num_tasks: int) -> None:
494        """Update the snapshot migration progress."""
495
496    @abc.abstractmethod
497    def log_migration_status(self, success: bool = True) -> None:
498        """Log the finished migration status."""
499
500    @abc.abstractmethod
501    def stop_snapshot_migration_progress(self, success: bool = True) -> None:
502        """Stop the snapshot migration progress."""
503
504    @abc.abstractmethod
505    def start_env_migration_progress(self, total_tasks: int) -> None:
506        """Indicates that a new environment migration progress has begun."""
507
508    @abc.abstractmethod
509    def update_env_migration_progress(self, num_tasks: int) -> None:
510        """Update the environment migration progress."""
511
512    @abc.abstractmethod
513    def stop_env_migration_progress(self, success: bool = True) -> None:
514        """Stop the environment migration progress."""
515
516    @abc.abstractmethod
517    def plan(
518        self,
519        plan_builder: PlanBuilder,
520        auto_apply: bool,
521        default_catalog: t.Optional[str],
522        no_diff: bool = False,
523        no_prompts: bool = False,
524    ) -> None:
525        """The main plan flow.
526
527        The console should present the user with choices on how to backfill and version the snapshots
528        of a plan.
529
530        Args:
531            plan: The plan to make choices for.
532            auto_apply: Whether to automatically apply the plan after all choices have been made.
533            no_diff: Hide text differences for changed models.
534            no_prompts: Whether to disable interactive prompts for the backfill time range. Please note that
535                if this flag is set to true and there are uncategorized changes the plan creation will
536                fail. Default: False
537        """
538
539    @abc.abstractmethod
540    def show_sql(self, sql: str) -> None:
541        """Display to the user SQL."""
542
543    @abc.abstractmethod
544    def log_status_update(self, message: str) -> None:
545        """Display general status update to the user."""
546
547    @abc.abstractmethod
548    def log_skipped_models(self, snapshot_names: t.Set[str]) -> None:
549        """Display list of models skipped during evaluation to the user."""
550
551    @abc.abstractmethod
552    def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None:
553        """Display list of models that failed during evaluation to the user."""
554
555    @abc.abstractmethod
556    def log_models_updated_during_restatement(
557        self,
558        snapshots: t.List[t.Tuple[SnapshotTableInfo, SnapshotTableInfo]],
559        environment_naming_info: EnvironmentNamingInfo,
560        default_catalog: t.Optional[str],
561    ) -> None:
562        """Display a list of models where new versions got deployed to the specified :environment while we were restating data the old versions
563
564        Args:
565            snapshots: a list of (snapshot_we_restated, snapshot_it_got_replaced_with_during_restatement) tuples
566            environment: which environment got updated while we were restating models
567            environment_naming_info: how snapshots are named in that :environment (for display name purposes)
568            default_catalog: the configured default catalog (for display name purposes)
569        """
570
571    @abc.abstractmethod
572    def loading_start(self, message: t.Optional[str] = None) -> uuid.UUID:
573        """Starts loading and returns a unique ID that can be used to stop the loading. Optionally can display a message."""
574
575    @abc.abstractmethod
576    def loading_stop(self, id: uuid.UUID) -> None:
577        """Stop loading for the given id."""

Abstract base class for defining classes used for displaying information to the user and also interact with them when their input is needed.

INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD = 10
@abc.abstractmethod
def start_plan_evaluation(self, plan: sqlmesh.core.plan.definition.EvaluatablePlan) -> None:
411    @abc.abstractmethod
412    def start_plan_evaluation(self, plan: EvaluatablePlan) -> None:
413        """Indicates that a new evaluation has begun."""

Indicates that a new evaluation has begun.

@abc.abstractmethod
def stop_plan_evaluation(self) -> None:
415    @abc.abstractmethod
416    def stop_plan_evaluation(self) -> None:
417        """Indicates that the evaluation has ended."""

Indicates that the evaluation has ended.

@abc.abstractmethod
def start_evaluation_progress( self, batched_intervals: Dict[sqlmesh.core.snapshot.definition.Snapshot, List[Tuple[int, int]]], environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str], audit_only: bool = False) -> None:
419    @abc.abstractmethod
420    def start_evaluation_progress(
421        self,
422        batched_intervals: t.Dict[Snapshot, Intervals],
423        environment_naming_info: EnvironmentNamingInfo,
424        default_catalog: t.Optional[str],
425        audit_only: bool = False,
426    ) -> None:
427        """Indicates that a new snapshot evaluation/auditing progress has begun."""

Indicates that a new snapshot evaluation/auditing progress has begun.

@abc.abstractmethod
def start_snapshot_evaluation_progress( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, audit_only: bool = False) -> None:
429    @abc.abstractmethod
430    def start_snapshot_evaluation_progress(
431        self, snapshot: Snapshot, audit_only: bool = False
432    ) -> None:
433        """Starts the snapshot evaluation progress."""

Starts the snapshot evaluation progress.

@abc.abstractmethod
def update_snapshot_evaluation_progress( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, interval: Tuple[int, int], batch_idx: int, duration_ms: Optional[int], num_audits_passed: int, num_audits_failed: int, audit_only: bool = False, execution_stats: Optional[sqlmesh.core.snapshot.execution_tracker.QueryExecutionStats] = None, auto_restatement_triggers: Optional[List[sqlmesh.core.snapshot.definition.SnapshotId]] = None) -> None:
435    @abc.abstractmethod
436    def update_snapshot_evaluation_progress(
437        self,
438        snapshot: Snapshot,
439        interval: Interval,
440        batch_idx: int,
441        duration_ms: t.Optional[int],
442        num_audits_passed: int,
443        num_audits_failed: int,
444        audit_only: bool = False,
445        execution_stats: t.Optional[QueryExecutionStats] = None,
446        auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None,
447    ) -> None:
448        """Updates the snapshot evaluation progress."""

Updates the snapshot evaluation progress.

@abc.abstractmethod
def stop_evaluation_progress(self, success: bool = True) -> None:
450    @abc.abstractmethod
451    def stop_evaluation_progress(self, success: bool = True) -> None:
452        """Stops the snapshot evaluation progress."""

Stops the snapshot evaluation progress.

@abc.abstractmethod
def start_creation_progress( self, snapshots: List[sqlmesh.core.snapshot.definition.Snapshot], environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str]) -> None:
454    @abc.abstractmethod
455    def start_creation_progress(
456        self,
457        snapshots: t.List[Snapshot],
458        environment_naming_info: EnvironmentNamingInfo,
459        default_catalog: t.Optional[str],
460    ) -> None:
461        """Indicates that a new snapshot creation progress has begun."""

Indicates that a new snapshot creation progress has begun.

@abc.abstractmethod
def update_creation_progress( self, snapshot: Union[sqlmesh.core.snapshot.definition.SnapshotTableInfo, sqlmesh.core.snapshot.definition.Snapshot]) -> None:
463    @abc.abstractmethod
464    def update_creation_progress(self, snapshot: SnapshotInfoLike) -> None:
465        """Update the snapshot creation progress."""

Update the snapshot creation progress.

@abc.abstractmethod
def stop_creation_progress(self, success: bool = True) -> None:
467    @abc.abstractmethod
468    def stop_creation_progress(self, success: bool = True) -> None:
469        """Stop the snapshot creation progress."""

Stop the snapshot creation progress.

@abc.abstractmethod
def start_promotion_progress( self, snapshots: List[sqlmesh.core.snapshot.definition.SnapshotTableInfo], environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str]) -> None:
471    @abc.abstractmethod
472    def start_promotion_progress(
473        self,
474        snapshots: t.List[SnapshotTableInfo],
475        environment_naming_info: EnvironmentNamingInfo,
476        default_catalog: t.Optional[str],
477    ) -> None:
478        """Indicates that a new snapshot promotion progress has begun."""

Indicates that a new snapshot promotion progress has begun.

@abc.abstractmethod
def update_promotion_progress( self, snapshot: Union[sqlmesh.core.snapshot.definition.SnapshotTableInfo, sqlmesh.core.snapshot.definition.Snapshot], promoted: bool) -> None:
480    @abc.abstractmethod
481    def update_promotion_progress(self, snapshot: SnapshotInfoLike, promoted: bool) -> None:
482        """Update the snapshot promotion progress."""

Update the snapshot promotion progress.

@abc.abstractmethod
def stop_promotion_progress(self, success: bool = True) -> None:
484    @abc.abstractmethod
485    def stop_promotion_progress(self, success: bool = True) -> None:
486        """Stop the snapshot promotion progress."""

Stop the snapshot promotion progress.

@abc.abstractmethod
def start_snapshot_migration_progress(self, total_tasks: int) -> None:
488    @abc.abstractmethod
489    def start_snapshot_migration_progress(self, total_tasks: int) -> None:
490        """Indicates that a new snapshot migration progress has begun."""

Indicates that a new snapshot migration progress has begun.

@abc.abstractmethod
def update_snapshot_migration_progress(self, num_tasks: int) -> None:
492    @abc.abstractmethod
493    def update_snapshot_migration_progress(self, num_tasks: int) -> None:
494        """Update the snapshot migration progress."""

Update the snapshot migration progress.

@abc.abstractmethod
def log_migration_status(self, success: bool = True) -> None:
496    @abc.abstractmethod
497    def log_migration_status(self, success: bool = True) -> None:
498        """Log the finished migration status."""

Log the finished migration status.

@abc.abstractmethod
def stop_snapshot_migration_progress(self, success: bool = True) -> None:
500    @abc.abstractmethod
501    def stop_snapshot_migration_progress(self, success: bool = True) -> None:
502        """Stop the snapshot migration progress."""

Stop the snapshot migration progress.

@abc.abstractmethod
def start_env_migration_progress(self, total_tasks: int) -> None:
504    @abc.abstractmethod
505    def start_env_migration_progress(self, total_tasks: int) -> None:
506        """Indicates that a new environment migration progress has begun."""

Indicates that a new environment migration progress has begun.

@abc.abstractmethod
def update_env_migration_progress(self, num_tasks: int) -> None:
508    @abc.abstractmethod
509    def update_env_migration_progress(self, num_tasks: int) -> None:
510        """Update the environment migration progress."""

Update the environment migration progress.

@abc.abstractmethod
def stop_env_migration_progress(self, success: bool = True) -> None:
512    @abc.abstractmethod
513    def stop_env_migration_progress(self, success: bool = True) -> None:
514        """Stop the environment migration progress."""

Stop the environment migration progress.

@abc.abstractmethod
def plan( self, plan_builder: sqlmesh.core.plan.builder.PlanBuilder, auto_apply: bool, default_catalog: Optional[str], no_diff: bool = False, no_prompts: bool = False) -> None:
516    @abc.abstractmethod
517    def plan(
518        self,
519        plan_builder: PlanBuilder,
520        auto_apply: bool,
521        default_catalog: t.Optional[str],
522        no_diff: bool = False,
523        no_prompts: bool = False,
524    ) -> None:
525        """The main plan flow.
526
527        The console should present the user with choices on how to backfill and version the snapshots
528        of a plan.
529
530        Args:
531            plan: The plan to make choices for.
532            auto_apply: Whether to automatically apply the plan after all choices have been made.
533            no_diff: Hide text differences for changed models.
534            no_prompts: Whether to disable interactive prompts for the backfill time range. Please note that
535                if this flag is set to true and there are uncategorized changes the plan creation will
536                fail. Default: False
537        """

The main plan flow.

The console should present the user with choices on how to backfill and version the snapshots of a plan.

Arguments:
  • plan: The plan to make choices for.
  • auto_apply: Whether to automatically apply the plan after all choices have been made.
  • no_diff: Hide text differences for changed models.
  • no_prompts: Whether to disable interactive prompts for the backfill time range. Please note that if this flag is set to true and there are uncategorized changes the plan creation will fail. Default: False
@abc.abstractmethod
def show_sql(self, sql: str) -> None:
539    @abc.abstractmethod
540    def show_sql(self, sql: str) -> None:
541        """Display to the user SQL."""

Display to the user SQL.

@abc.abstractmethod
def log_status_update(self, message: str) -> None:
543    @abc.abstractmethod
544    def log_status_update(self, message: str) -> None:
545        """Display general status update to the user."""

Display general status update to the user.

@abc.abstractmethod
def log_skipped_models(self, snapshot_names: Set[str]) -> None:
547    @abc.abstractmethod
548    def log_skipped_models(self, snapshot_names: t.Set[str]) -> None:
549        """Display list of models skipped during evaluation to the user."""

Display list of models skipped during evaluation to the user.

@abc.abstractmethod
def log_failed_models( self, errors: List[sqlmesh.utils.concurrency.NodeExecutionFailedError]) -> None:
551    @abc.abstractmethod
552    def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None:
553        """Display list of models that failed during evaluation to the user."""

Display list of models that failed during evaluation to the user.

@abc.abstractmethod
def log_models_updated_during_restatement( self, snapshots: List[Tuple[sqlmesh.core.snapshot.definition.SnapshotTableInfo, sqlmesh.core.snapshot.definition.SnapshotTableInfo]], environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str]) -> None:
555    @abc.abstractmethod
556    def log_models_updated_during_restatement(
557        self,
558        snapshots: t.List[t.Tuple[SnapshotTableInfo, SnapshotTableInfo]],
559        environment_naming_info: EnvironmentNamingInfo,
560        default_catalog: t.Optional[str],
561    ) -> None:
562        """Display a list of models where new versions got deployed to the specified :environment while we were restating data the old versions
563
564        Args:
565            snapshots: a list of (snapshot_we_restated, snapshot_it_got_replaced_with_during_restatement) tuples
566            environment: which environment got updated while we were restating models
567            environment_naming_info: how snapshots are named in that :environment (for display name purposes)
568            default_catalog: the configured default catalog (for display name purposes)
569        """

Display a list of models where new versions got deployed to the specified :environment while we were restating data the old versions

Arguments:
  • snapshots: a list of (snapshot_we_restated, snapshot_it_got_replaced_with_during_restatement) tuples
  • environment: which environment got updated while we were restating models
  • environment_naming_info: how snapshots are named in that :environment (for display name purposes)
  • default_catalog: the configured default catalog (for display name purposes)
@abc.abstractmethod
def loading_start(self, message: Optional[str] = None) -> uuid.UUID:
571    @abc.abstractmethod
572    def loading_start(self, message: t.Optional[str] = None) -> uuid.UUID:
573        """Starts loading and returns a unique ID that can be used to stop the loading. Optionally can display a message."""

Starts loading and returns a unique ID that can be used to stop the loading. Optionally can display a message.

@abc.abstractmethod
def loading_stop(self, id: uuid.UUID) -> None:
575    @abc.abstractmethod
576    def loading_stop(self, id: uuid.UUID) -> None:
577        """Stop loading for the given id."""

Stop loading for the given id.

class NoopConsole(Console):
580class NoopConsole(Console):
581    def start_plan_evaluation(self, plan: EvaluatablePlan) -> None:
582        pass
583
584    def stop_plan_evaluation(self) -> None:
585        pass
586
587    def start_evaluation_progress(
588        self,
589        batched_intervals: t.Dict[Snapshot, Intervals],
590        environment_naming_info: EnvironmentNamingInfo,
591        default_catalog: t.Optional[str],
592        audit_only: bool = False,
593    ) -> None:
594        pass
595
596    def start_snapshot_evaluation_progress(
597        self, snapshot: Snapshot, audit_only: bool = False
598    ) -> None:
599        pass
600
601    def update_snapshot_evaluation_progress(
602        self,
603        snapshot: Snapshot,
604        interval: Interval,
605        batch_idx: int,
606        duration_ms: t.Optional[int],
607        num_audits_passed: int,
608        num_audits_failed: int,
609        audit_only: bool = False,
610        execution_stats: t.Optional[QueryExecutionStats] = None,
611        auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None,
612    ) -> None:
613        pass
614
615    def stop_evaluation_progress(self, success: bool = True) -> None:
616        pass
617
618    def start_signal_progress(
619        self,
620        snapshot: Snapshot,
621        default_catalog: t.Optional[str],
622        environment_naming_info: EnvironmentNamingInfo,
623    ) -> None:
624        pass
625
626    def update_signal_progress(
627        self,
628        snapshot: Snapshot,
629        signal_name: str,
630        signal_idx: int,
631        total_signals: int,
632        ready_intervals: Intervals,
633        check_intervals: Intervals,
634        duration: float,
635    ) -> None:
636        pass
637
638    def stop_signal_progress(self) -> None:
639        pass
640
641    def start_creation_progress(
642        self,
643        snapshots: t.List[Snapshot],
644        environment_naming_info: EnvironmentNamingInfo,
645        default_catalog: t.Optional[str],
646    ) -> None:
647        pass
648
649    def update_creation_progress(self, snapshot: SnapshotInfoLike) -> None:
650        pass
651
652    def stop_creation_progress(self, success: bool = True) -> None:
653        pass
654
655    def start_cleanup(self, ignore_ttl: bool) -> bool:
656        return True
657
658    def update_cleanup_progress(self, object_name: str) -> None:
659        pass
660
661    def stop_cleanup(self, success: bool = True) -> None:
662        pass
663
664    def start_promotion_progress(
665        self,
666        snapshots: t.List[SnapshotTableInfo],
667        environment_naming_info: EnvironmentNamingInfo,
668        default_catalog: t.Optional[str],
669    ) -> None:
670        pass
671
672    def update_promotion_progress(self, snapshot: SnapshotInfoLike, promoted: bool) -> None:
673        pass
674
675    def stop_promotion_progress(self, success: bool = True) -> None:
676        pass
677
678    def start_snapshot_migration_progress(self, total_tasks: int) -> None:
679        pass
680
681    def update_snapshot_migration_progress(self, num_tasks: int) -> None:
682        pass
683
684    def log_migration_status(self, success: bool = True) -> None:
685        pass
686
687    def stop_snapshot_migration_progress(self, success: bool = True) -> None:
688        pass
689
690    def start_env_migration_progress(self, total_tasks: int) -> None:
691        pass
692
693    def update_env_migration_progress(self, num_tasks: int) -> None:
694        pass
695
696    def stop_env_migration_progress(self, success: bool = True) -> None:
697        pass
698
699    def start_state_export(
700        self,
701        output_file: Path,
702        gateway: t.Optional[str] = None,
703        state_connection_config: t.Optional[ConnectionConfig] = None,
704        environment_names: t.Optional[t.List[str]] = None,
705        local_only: bool = False,
706        confirm: bool = True,
707    ) -> bool:
708        return confirm
709
710    def update_state_export_progress(
711        self,
712        version_count: t.Optional[int] = None,
713        versions_complete: bool = False,
714        snapshot_count: t.Optional[int] = None,
715        snapshots_complete: bool = False,
716        environment_count: t.Optional[int] = None,
717        environments_complete: bool = False,
718    ) -> None:
719        pass
720
721    def stop_state_export(self, success: bool, output_file: Path) -> None:
722        pass
723
724    def start_state_import(
725        self,
726        input_file: Path,
727        gateway: str,
728        state_connection_config: ConnectionConfig,
729        clear: bool = False,
730        confirm: bool = True,
731    ) -> bool:
732        return confirm
733
734    def update_state_import_progress(
735        self,
736        timestamp: t.Optional[str] = None,
737        state_file_version: t.Optional[int] = None,
738        versions: t.Optional[Versions] = None,
739        snapshot_count: t.Optional[int] = None,
740        snapshots_complete: bool = False,
741        environment_count: t.Optional[int] = None,
742        environments_complete: bool = False,
743    ) -> None:
744        pass
745
746    def stop_state_import(self, success: bool, input_file: Path) -> None:
747        pass
748
749    def show_environment_difference_summary(
750        self,
751        context_diff: ContextDiff,
752        no_diff: bool = True,
753    ) -> None:
754        pass
755
756    def show_model_difference_summary(
757        self,
758        context_diff: ContextDiff,
759        environment_naming_info: EnvironmentNamingInfo,
760        default_catalog: t.Optional[str],
761        no_diff: bool = True,
762    ) -> None:
763        pass
764
765    def plan(
766        self,
767        plan_builder: PlanBuilder,
768        auto_apply: bool,
769        default_catalog: t.Optional[str],
770        no_diff: bool = False,
771        no_prompts: bool = False,
772    ) -> None:
773        if auto_apply:
774            plan_builder.apply()
775
776    def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
777        pass
778
779    def show_sql(self, sql: str) -> None:
780        pass
781
782    def log_status_update(self, message: str) -> None:
783        pass
784
785    def log_skipped_models(self, snapshot_names: t.Set[str]) -> None:
786        pass
787
788    def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None:
789        pass
790
791    def log_models_updated_during_restatement(
792        self,
793        snapshots: t.List[t.Tuple[SnapshotTableInfo, SnapshotTableInfo]],
794        environment_naming_info: EnvironmentNamingInfo,
795        default_catalog: t.Optional[str],
796    ) -> None:
797        pass
798
799    def log_destructive_change(
800        self,
801        snapshot_name: str,
802        alter_operations: t.List[TableAlterOperation],
803        dialect: str,
804        error: bool = True,
805    ) -> None:
806        pass
807
808    def log_additive_change(
809        self,
810        snapshot_name: str,
811        alter_operations: t.List[TableAlterOperation],
812        dialect: str,
813        error: bool = True,
814    ) -> None:
815        pass
816
817    def log_error(self, message: str) -> None:
818        pass
819
820    def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None:
821        logger.warning(long_message or short_message)
822
823    def log_success(self, message: str) -> None:
824        pass
825
826    def loading_start(self, message: t.Optional[str] = None) -> uuid.UUID:
827        return uuid.uuid4()
828
829    def loading_stop(self, id: uuid.UUID) -> None:
830        pass
831
832    def show_table_diff(
833        self,
834        table_diffs: t.List[TableDiff],
835        show_sample: bool = True,
836        skip_grain_check: bool = False,
837        temp_schema: t.Optional[str] = None,
838    ) -> None:
839        for table_diff in table_diffs:
840            self.show_table_diff_summary(table_diff)
841            self.show_schema_diff(table_diff.schema_diff())
842            self.show_row_diff(
843                table_diff.row_diff(temp_schema=temp_schema, skip_grain_check=skip_grain_check),
844                show_sample=show_sample,
845                skip_grain_check=skip_grain_check,
846            )
847
848    def update_table_diff_progress(self, model: str) -> None:
849        pass
850
851    def start_table_diff_progress(self, models_to_diff: int) -> None:
852        pass
853
854    def start_table_diff_model_progress(self, model: str) -> None:
855        pass
856
857    def stop_table_diff_progress(self, success: bool) -> None:
858        pass
859
860    def show_table_diff_details(
861        self,
862        models_to_diff: t.List[str],
863    ) -> None:
864        pass
865
866    def show_table_diff_summary(self, table_diff: TableDiff) -> None:
867        pass
868
869    def show_schema_diff(self, schema_diff: SchemaDiff) -> None:
870        pass
871
872    def show_row_diff(
873        self, row_diff: RowDiff, show_sample: bool = True, skip_grain_check: bool = False
874    ) -> None:
875        pass
876
877    def print_environments(self, environments_summary: t.List[EnvironmentSummary]) -> None:
878        pass
879
880    def show_intervals(self, snapshot_intervals: t.Dict[Snapshot, SnapshotIntervals]) -> None:
881        pass
882
883    def show_linter_violations(
884        self, violations: t.List[RuleViolation], model: Model, is_error: bool = False
885    ) -> None:
886        pass
887
888    def print_connection_config(
889        self, config: ConnectionConfig, title: t.Optional[str] = "Connection"
890    ) -> None:
891        pass
892
893    def start_destroy(
894        self,
895        schemas_to_delete: t.Optional[t.Set[str]] = None,
896        views_to_delete: t.Optional[t.Set[str]] = None,
897        tables_to_delete: t.Optional[t.Set[str]] = None,
898    ) -> bool:
899        return True
900
901    def stop_destroy(self, success: bool = True) -> None:
902        pass

Abstract base class for defining classes used for displaying information to the user and also interact with them when their input is needed.

def start_plan_evaluation(self, plan: sqlmesh.core.plan.definition.EvaluatablePlan) -> None:
581    def start_plan_evaluation(self, plan: EvaluatablePlan) -> None:
582        pass

Indicates that a new evaluation has begun.

def stop_plan_evaluation(self) -> None:
584    def stop_plan_evaluation(self) -> None:
585        pass

Indicates that the evaluation has ended.

def start_evaluation_progress( self, batched_intervals: Dict[sqlmesh.core.snapshot.definition.Snapshot, List[Tuple[int, int]]], environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str], audit_only: bool = False) -> None:
587    def start_evaluation_progress(
588        self,
589        batched_intervals: t.Dict[Snapshot, Intervals],
590        environment_naming_info: EnvironmentNamingInfo,
591        default_catalog: t.Optional[str],
592        audit_only: bool = False,
593    ) -> None:
594        pass

Indicates that a new snapshot evaluation/auditing progress has begun.

def start_snapshot_evaluation_progress( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, audit_only: bool = False) -> None:
596    def start_snapshot_evaluation_progress(
597        self, snapshot: Snapshot, audit_only: bool = False
598    ) -> None:
599        pass

Starts the snapshot evaluation progress.

def update_snapshot_evaluation_progress( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, interval: Tuple[int, int], batch_idx: int, duration_ms: Optional[int], num_audits_passed: int, num_audits_failed: int, audit_only: bool = False, execution_stats: Optional[sqlmesh.core.snapshot.execution_tracker.QueryExecutionStats] = None, auto_restatement_triggers: Optional[List[sqlmesh.core.snapshot.definition.SnapshotId]] = None) -> None:
601    def update_snapshot_evaluation_progress(
602        self,
603        snapshot: Snapshot,
604        interval: Interval,
605        batch_idx: int,
606        duration_ms: t.Optional[int],
607        num_audits_passed: int,
608        num_audits_failed: int,
609        audit_only: bool = False,
610        execution_stats: t.Optional[QueryExecutionStats] = None,
611        auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None,
612    ) -> None:
613        pass

Updates the snapshot evaluation progress.

def stop_evaluation_progress(self, success: bool = True) -> None:
615    def stop_evaluation_progress(self, success: bool = True) -> None:
616        pass

Stops the snapshot evaluation progress.

def start_signal_progress( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, default_catalog: Optional[str], environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo) -> None:
618    def start_signal_progress(
619        self,
620        snapshot: Snapshot,
621        default_catalog: t.Optional[str],
622        environment_naming_info: EnvironmentNamingInfo,
623    ) -> None:
624        pass

Indicates that signal checking has begun for a snapshot.

def update_signal_progress( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, signal_name: str, signal_idx: int, total_signals: int, ready_intervals: List[Tuple[int, int]], check_intervals: List[Tuple[int, int]], duration: float) -> None:
626    def update_signal_progress(
627        self,
628        snapshot: Snapshot,
629        signal_name: str,
630        signal_idx: int,
631        total_signals: int,
632        ready_intervals: Intervals,
633        check_intervals: Intervals,
634        duration: float,
635    ) -> None:
636        pass

Updates the signal checking progress.

def stop_signal_progress(self) -> None:
638    def stop_signal_progress(self) -> None:
639        pass

Indicates that signal checking has completed for a snapshot.

def start_creation_progress( self, snapshots: List[sqlmesh.core.snapshot.definition.Snapshot], environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str]) -> None:
641    def start_creation_progress(
642        self,
643        snapshots: t.List[Snapshot],
644        environment_naming_info: EnvironmentNamingInfo,
645        default_catalog: t.Optional[str],
646    ) -> None:
647        pass

Indicates that a new snapshot creation progress has begun.

def update_creation_progress( self, snapshot: Union[sqlmesh.core.snapshot.definition.SnapshotTableInfo, sqlmesh.core.snapshot.definition.Snapshot]) -> None:
649    def update_creation_progress(self, snapshot: SnapshotInfoLike) -> None:
650        pass

Update the snapshot creation progress.

def stop_creation_progress(self, success: bool = True) -> None:
652    def stop_creation_progress(self, success: bool = True) -> None:
653        pass

Stop the snapshot creation progress.

def start_cleanup(self, ignore_ttl: bool) -> bool:
655    def start_cleanup(self, ignore_ttl: bool) -> bool:
656        return True

Start a janitor / snapshot cleanup run.

Arguments:
  • ignore_ttl: Indicates that the user wants to ignore the snapshot TTL and clean up everything not promoted to an environment
Returns:

Whether or not the cleanup run should proceed

def update_cleanup_progress(self, object_name: str) -> None:
658    def update_cleanup_progress(self, object_name: str) -> None:
659        pass

Update the snapshot cleanup progress.

def stop_cleanup(self, success: bool = True) -> None:
661    def stop_cleanup(self, success: bool = True) -> None:
662        pass

Indicates the janitor / snapshot cleanup run has ended

Arguments:
  • success: Whether or not the cleanup completed successfully
def start_promotion_progress( self, snapshots: List[sqlmesh.core.snapshot.definition.SnapshotTableInfo], environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str]) -> None:
664    def start_promotion_progress(
665        self,
666        snapshots: t.List[SnapshotTableInfo],
667        environment_naming_info: EnvironmentNamingInfo,
668        default_catalog: t.Optional[str],
669    ) -> None:
670        pass

Indicates that a new snapshot promotion progress has begun.

def update_promotion_progress( self, snapshot: Union[sqlmesh.core.snapshot.definition.SnapshotTableInfo, sqlmesh.core.snapshot.definition.Snapshot], promoted: bool) -> None:
672    def update_promotion_progress(self, snapshot: SnapshotInfoLike, promoted: bool) -> None:
673        pass

Update the snapshot promotion progress.

def stop_promotion_progress(self, success: bool = True) -> None:
675    def stop_promotion_progress(self, success: bool = True) -> None:
676        pass

Stop the snapshot promotion progress.

def start_snapshot_migration_progress(self, total_tasks: int) -> None:
678    def start_snapshot_migration_progress(self, total_tasks: int) -> None:
679        pass

Indicates that a new snapshot migration progress has begun.

def update_snapshot_migration_progress(self, num_tasks: int) -> None:
681    def update_snapshot_migration_progress(self, num_tasks: int) -> None:
682        pass

Update the snapshot migration progress.

def log_migration_status(self, success: bool = True) -> None:
684    def log_migration_status(self, success: bool = True) -> None:
685        pass

Log the finished migration status.

def stop_snapshot_migration_progress(self, success: bool = True) -> None:
687    def stop_snapshot_migration_progress(self, success: bool = True) -> None:
688        pass

Stop the snapshot migration progress.

def start_env_migration_progress(self, total_tasks: int) -> None:
690    def start_env_migration_progress(self, total_tasks: int) -> None:
691        pass

Indicates that a new environment migration progress has begun.

def update_env_migration_progress(self, num_tasks: int) -> None:
693    def update_env_migration_progress(self, num_tasks: int) -> None:
694        pass

Update the environment migration progress.

def stop_env_migration_progress(self, success: bool = True) -> None:
696    def stop_env_migration_progress(self, success: bool = True) -> None:
697        pass

Stop the environment migration progress.

def start_state_export( self, output_file: pathlib.Path, gateway: Optional[str] = None, state_connection_config: Optional[sqlmesh.core.config.connection.ConnectionConfig] = None, environment_names: Optional[List[str]] = None, local_only: bool = False, confirm: bool = True) -> bool:
699    def start_state_export(
700        self,
701        output_file: Path,
702        gateway: t.Optional[str] = None,
703        state_connection_config: t.Optional[ConnectionConfig] = None,
704        environment_names: t.Optional[t.List[str]] = None,
705        local_only: bool = False,
706        confirm: bool = True,
707    ) -> bool:
708        return confirm

State a state export

def update_state_export_progress( self, version_count: Optional[int] = None, versions_complete: bool = False, snapshot_count: Optional[int] = None, snapshots_complete: bool = False, environment_count: Optional[int] = None, environments_complete: bool = False) -> None:
710    def update_state_export_progress(
711        self,
712        version_count: t.Optional[int] = None,
713        versions_complete: bool = False,
714        snapshot_count: t.Optional[int] = None,
715        snapshots_complete: bool = False,
716        environment_count: t.Optional[int] = None,
717        environments_complete: bool = False,
718    ) -> None:
719        pass

Update the state export progress

def stop_state_export(self, success: bool, output_file: pathlib.Path) -> None:
721    def stop_state_export(self, success: bool, output_file: Path) -> None:
722        pass

Finish a state export

def start_state_import( self, input_file: pathlib.Path, gateway: str, state_connection_config: sqlmesh.core.config.connection.ConnectionConfig, clear: bool = False, confirm: bool = True) -> bool:
724    def start_state_import(
725        self,
726        input_file: Path,
727        gateway: str,
728        state_connection_config: ConnectionConfig,
729        clear: bool = False,
730        confirm: bool = True,
731    ) -> bool:
732        return confirm

Start a state import

def update_state_import_progress( self, timestamp: Optional[str] = None, state_file_version: Optional[int] = None, versions: Optional[sqlmesh.core.state_sync.base.Versions] = None, snapshot_count: Optional[int] = None, snapshots_complete: bool = False, environment_count: Optional[int] = None, environments_complete: bool = False) -> None:
734    def update_state_import_progress(
735        self,
736        timestamp: t.Optional[str] = None,
737        state_file_version: t.Optional[int] = None,
738        versions: t.Optional[Versions] = None,
739        snapshot_count: t.Optional[int] = None,
740        snapshots_complete: bool = False,
741        environment_count: t.Optional[int] = None,
742        environments_complete: bool = False,
743    ) -> None:
744        pass

Update the state import process

def stop_state_import(self, success: bool, input_file: pathlib.Path) -> None:
746    def stop_state_import(self, success: bool, input_file: Path) -> None:
747        pass

Finish a state import

def show_environment_difference_summary( self, context_diff: sqlmesh.core.context_diff.ContextDiff, no_diff: bool = True) -> None:
749    def show_environment_difference_summary(
750        self,
751        context_diff: ContextDiff,
752        no_diff: bool = True,
753    ) -> None:
754        pass

Displays a summary of differences for the environment.

def show_model_difference_summary( self, context_diff: sqlmesh.core.context_diff.ContextDiff, environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str], no_diff: bool = True) -> None:
756    def show_model_difference_summary(
757        self,
758        context_diff: ContextDiff,
759        environment_naming_info: EnvironmentNamingInfo,
760        default_catalog: t.Optional[str],
761        no_diff: bool = True,
762    ) -> None:
763        pass

Displays a summary of differences for the given models.

def plan( self, plan_builder: sqlmesh.core.plan.builder.PlanBuilder, auto_apply: bool, default_catalog: Optional[str], no_diff: bool = False, no_prompts: bool = False) -> None:
765    def plan(
766        self,
767        plan_builder: PlanBuilder,
768        auto_apply: bool,
769        default_catalog: t.Optional[str],
770        no_diff: bool = False,
771        no_prompts: bool = False,
772    ) -> None:
773        if auto_apply:
774            plan_builder.apply()

The main plan flow.

The console should present the user with choices on how to backfill and version the snapshots of a plan.

Arguments:
  • plan: The plan to make choices for.
  • auto_apply: Whether to automatically apply the plan after all choices have been made.
  • no_diff: Hide text differences for changed models.
  • no_prompts: Whether to disable interactive prompts for the backfill time range. Please note that if this flag is set to true and there are uncategorized changes the plan creation will fail. Default: False
def log_test_results( self, result: sqlmesh.core.test.result.ModelTextTestResult, target_dialect: str) -> None:
776    def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
777        pass

Display the test result and output.

Arguments:
  • result: The unittest test result that contains metrics like num success, fails, ect.
  • target_dialect: The dialect that tests were run against. Assumes all tests run against the same dialect.
def show_sql(self, sql: str) -> None:
779    def show_sql(self, sql: str) -> None:
780        pass

Display to the user SQL.

def log_status_update(self, message: str) -> None:
782    def log_status_update(self, message: str) -> None:
783        pass

Display general status update to the user.

def log_skipped_models(self, snapshot_names: Set[str]) -> None:
785    def log_skipped_models(self, snapshot_names: t.Set[str]) -> None:
786        pass

Display list of models skipped during evaluation to the user.

def log_failed_models( self, errors: List[sqlmesh.utils.concurrency.NodeExecutionFailedError]) -> None:
788    def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None:
789        pass

Display list of models that failed during evaluation to the user.

def log_models_updated_during_restatement( self, snapshots: List[Tuple[sqlmesh.core.snapshot.definition.SnapshotTableInfo, sqlmesh.core.snapshot.definition.SnapshotTableInfo]], environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str]) -> None:
791    def log_models_updated_during_restatement(
792        self,
793        snapshots: t.List[t.Tuple[SnapshotTableInfo, SnapshotTableInfo]],
794        environment_naming_info: EnvironmentNamingInfo,
795        default_catalog: t.Optional[str],
796    ) -> None:
797        pass

Display a list of models where new versions got deployed to the specified :environment while we were restating data the old versions

Arguments:
  • snapshots: a list of (snapshot_we_restated, snapshot_it_got_replaced_with_during_restatement) tuples
  • environment: which environment got updated while we were restating models
  • environment_naming_info: how snapshots are named in that :environment (for display name purposes)
  • default_catalog: the configured default catalog (for display name purposes)
def log_destructive_change( self, snapshot_name: str, alter_operations: List[sqlmesh.core.schema_diff.TableAlterOperation], dialect: str, error: bool = True) -> None:
799    def log_destructive_change(
800        self,
801        snapshot_name: str,
802        alter_operations: t.List[TableAlterOperation],
803        dialect: str,
804        error: bool = True,
805    ) -> None:
806        pass

Display a destructive change error or warning to the user.

def log_additive_change( self, snapshot_name: str, alter_operations: List[sqlmesh.core.schema_diff.TableAlterOperation], dialect: str, error: bool = True) -> None:
808    def log_additive_change(
809        self,
810        snapshot_name: str,
811        alter_operations: t.List[TableAlterOperation],
812        dialect: str,
813        error: bool = True,
814    ) -> None:
815        pass

Display an additive change error or warning to the user.

def log_error(self, message: str) -> None:
817    def log_error(self, message: str) -> None:
818        pass

Display error info to the user.

def log_warning(self, short_message: str, long_message: Optional[str] = None) -> None:
820    def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None:
821        logger.warning(long_message or short_message)

Display warning info to the user.

Arguments:
  • short_message: The warning message to print to console.
  • long_message: The warning message to log to file. If not provided, short_message is used.
def log_success(self, message: str) -> None:
823    def log_success(self, message: str) -> None:
824        pass

Display a general successful message to the user.

def loading_start(self, message: Optional[str] = None) -> uuid.UUID:
826    def loading_start(self, message: t.Optional[str] = None) -> uuid.UUID:
827        return uuid.uuid4()

Starts loading and returns a unique ID that can be used to stop the loading. Optionally can display a message.

def loading_stop(self, id: uuid.UUID) -> None:
829    def loading_stop(self, id: uuid.UUID) -> None:
830        pass

Stop loading for the given id.

def show_table_diff( self, table_diffs: List[sqlmesh.core.table_diff.TableDiff], show_sample: bool = True, skip_grain_check: bool = False, temp_schema: Optional[str] = None) -> None:
832    def show_table_diff(
833        self,
834        table_diffs: t.List[TableDiff],
835        show_sample: bool = True,
836        skip_grain_check: bool = False,
837        temp_schema: t.Optional[str] = None,
838    ) -> None:
839        for table_diff in table_diffs:
840            self.show_table_diff_summary(table_diff)
841            self.show_schema_diff(table_diff.schema_diff())
842            self.show_row_diff(
843                table_diff.row_diff(temp_schema=temp_schema, skip_grain_check=skip_grain_check),
844                show_sample=show_sample,
845                skip_grain_check=skip_grain_check,
846            )

Display the table diff between two or multiple tables.

def update_table_diff_progress(self, model: str) -> None:
848    def update_table_diff_progress(self, model: str) -> None:
849        pass

Update table diff progress bar

def start_table_diff_progress(self, models_to_diff: int) -> None:
851    def start_table_diff_progress(self, models_to_diff: int) -> None:
852        pass

Start table diff progress bar

def start_table_diff_model_progress(self, model: str) -> None:
854    def start_table_diff_model_progress(self, model: str) -> None:
855        pass

Start table diff model progress

def stop_table_diff_progress(self, success: bool) -> None:
857    def stop_table_diff_progress(self, success: bool) -> None:
858        pass

Stop table diff progress bar

def show_table_diff_details(self, models_to_diff: List[str]) -> None:
860    def show_table_diff_details(
861        self,
862        models_to_diff: t.List[str],
863    ) -> None:
864        pass

Display information about which tables are going to be diffed

def show_table_diff_summary(self, table_diff: sqlmesh.core.table_diff.TableDiff) -> None:
866    def show_table_diff_summary(self, table_diff: TableDiff) -> None:
867        pass

Display information about the tables being diffed and how they are being joined

def show_schema_diff(self, schema_diff: sqlmesh.core.table_diff.SchemaDiff) -> None:
869    def show_schema_diff(self, schema_diff: SchemaDiff) -> None:
870        pass

Show table schema diff.

def show_row_diff( self, row_diff: sqlmesh.core.table_diff.RowDiff, show_sample: bool = True, skip_grain_check: bool = False) -> None:
872    def show_row_diff(
873        self, row_diff: RowDiff, show_sample: bool = True, skip_grain_check: bool = False
874    ) -> None:
875        pass

Show table summary diff.

def print_environments( self, environments_summary: List[sqlmesh.core.environment.EnvironmentSummary]) -> None:
877    def print_environments(self, environments_summary: t.List[EnvironmentSummary]) -> None:
878        pass

Prints all environment names along with expiry datetime.

def show_intervals( self, snapshot_intervals: Dict[sqlmesh.core.snapshot.definition.Snapshot, sqlmesh.core.plan.definition.SnapshotIntervals]) -> None:
880    def show_intervals(self, snapshot_intervals: t.Dict[Snapshot, SnapshotIntervals]) -> None:
881        pass

Show ready intervals

883    def show_linter_violations(
884        self, violations: t.List[RuleViolation], model: Model, is_error: bool = False
885    ) -> None:
886        pass

Prints all linter violations depending on their severity

def print_connection_config( self, config: sqlmesh.core.config.connection.ConnectionConfig, title: Optional[str] = 'Connection') -> None:
888    def print_connection_config(
889        self, config: ConnectionConfig, title: t.Optional[str] = "Connection"
890    ) -> None:
891        pass
def start_destroy( self, schemas_to_delete: Optional[Set[str]] = None, views_to_delete: Optional[Set[str]] = None, tables_to_delete: Optional[Set[str]] = None) -> bool:
893    def start_destroy(
894        self,
895        schemas_to_delete: t.Optional[t.Set[str]] = None,
896        views_to_delete: t.Optional[t.Set[str]] = None,
897        tables_to_delete: t.Optional[t.Set[str]] = None,
898    ) -> bool:
899        return True

Start a destroy operation.

Arguments:
  • schemas_to_delete: Set of schemas that will be deleted
  • views_to_delete: Set of views that will be deleted
  • tables_to_delete: Set of tables that will be deleted
Returns:

Whether or not the destroy operation should proceed

def stop_destroy(self, success: bool = True) -> None:
901    def stop_destroy(self, success: bool = True) -> None:
902        pass

Indicates the destroy operation has ended

Arguments:
  • success: Whether or not the cleanup completed successfully
def make_progress_bar( message: str, console: Optional[rich.console.Console] = None, justify: Literal['default', 'left', 'center', 'right', 'full'] = 'right') -> rich.progress.Progress:
905def make_progress_bar(
906    message: str,
907    console: t.Optional[RichConsole] = None,
908    justify: t.Literal["default", "left", "center", "right", "full"] = "right",
909) -> Progress:
910    return Progress(
911        TextColumn(f"[bold blue]{message}", justify=justify),
912        BarColumn(bar_width=PROGRESS_BAR_WIDTH),
913        "[progress.percentage]{task.percentage:>3.1f}%",
914        "•",
915        srich.BatchColumn(),
916        "•",
917        TimeElapsedColumn(),
918        console=console,
919    )
class TerminalConsole(Console):
 922class TerminalConsole(Console):
 923    """A rich based implementation of the console."""
 924
 925    TABLE_DIFF_SOURCE_BLUE = "#0248ff"
 926    TABLE_DIFF_TARGET_GREEN = "green"
 927    AUDIT_PASS_MARK = "\u2714"
 928    GREEN_AUDIT_PASS_MARK = f"[green]{AUDIT_PASS_MARK}[/green]"
 929    AUDIT_FAIL_MARK = "\u274c"
 930    AUDIT_PADDING = 0
 931    CHECK_MARK = f"{AUDIT_PASS_MARK} "
 932
 933    def __init__(
 934        self,
 935        console: t.Optional[RichConsole] = None,
 936        verbosity: Verbosity = Verbosity.DEFAULT,
 937        dialect: DialectType = None,
 938        ignore_warnings: bool = False,
 939        **kwargs: t.Any,
 940    ) -> None:
 941        self.console: RichConsole = console or srich.console
 942
 943        self.evaluation_progress_live: t.Optional[Live] = None
 944        self.evaluation_total_progress: t.Optional[Progress] = None
 945        self.evaluation_total_task: t.Optional[TaskID] = None
 946        self.evaluation_model_progress: t.Optional[Progress] = None
 947        self.evaluation_model_tasks: t.Dict[str, TaskID] = {}
 948        self.evaluation_model_batch_sizes: t.Dict[Snapshot, int] = {}
 949        self.evaluation_column_widths: t.Dict[str, int] = {}
 950
 951        # Put in temporary values that are replaced when evaluating
 952        self.environment_naming_info = EnvironmentNamingInfo()
 953        self.default_catalog: t.Optional[str] = None
 954
 955        self.creation_progress: t.Optional[Progress] = None
 956        self.creation_column_widths: t.Dict[str, int] = {}
 957        self.creation_task: t.Optional[TaskID] = None
 958
 959        self.promotion_progress: t.Optional[Progress] = None
 960        self.promotion_column_widths: t.Dict[str, int] = {}
 961        self.promotion_task: t.Optional[TaskID] = None
 962
 963        self.migration_progress: t.Optional[Progress] = None
 964        self.migration_task: t.Optional[TaskID] = None
 965
 966        self.env_migration_progress: t.Optional[Progress] = None
 967        self.env_migration_task: t.Optional[TaskID] = None
 968
 969        self.loading_status: t.Dict[uuid.UUID, Status] = {}
 970
 971        self.state_export_progress: t.Optional[Progress] = None
 972        self.state_export_version_task: t.Optional[TaskID] = None
 973        self.state_export_snapshot_task: t.Optional[TaskID] = None
 974        self.state_export_environment_task: t.Optional[TaskID] = None
 975
 976        self.state_import_progress: t.Optional[Progress] = None
 977        self.state_import_version_task: t.Optional[TaskID] = None
 978        self.state_import_snapshot_task: t.Optional[TaskID] = None
 979        self.state_import_environment_task: t.Optional[TaskID] = None
 980
 981        self.table_diff_progress: t.Optional[Progress] = None
 982        self.table_diff_model_progress: t.Optional[Progress] = None
 983        self.table_diff_model_tasks: t.Dict[str, TaskID] = {}
 984        self.table_diff_progress_live: t.Optional[Live] = None
 985
 986        self.signal_progress_logged = False
 987        self.signal_status_tree: t.Optional[Tree] = None
 988
 989        self.verbosity = verbosity
 990        self.dialect = dialect
 991        self.ignore_warnings = ignore_warnings
 992
 993    def _limit_model_names(self, tree: Tree, verbosity: Verbosity = Verbosity.DEFAULT) -> Tree:
 994        """Trim long indirectly modified model lists below threshold."""
 995        modified_length = len(tree.children)
 996        if (
 997            verbosity < Verbosity.VERY_VERBOSE
 998            and modified_length > self.INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD
 999        ):
1000            tree.children = [
1001                tree.children[0],
1002                Tree(f".... {modified_length - 2} more ...."),
1003                tree.children[-1],
1004            ]
1005        return tree
1006
1007    def _print(self, value: t.Any, **kwargs: t.Any) -> None:
1008        self.console.print(value, **kwargs)
1009
1010    def _prompt(self, message: str, **kwargs: t.Any) -> t.Any:
1011        return Prompt.ask(message, console=self.console, **kwargs)
1012
1013    def _confirm(self, message: str, **kwargs: t.Any) -> bool:
1014        return Confirm.ask(message, console=self.console, **kwargs)
1015
1016    def start_plan_evaluation(self, plan: EvaluatablePlan) -> None:
1017        pass
1018
1019    def stop_plan_evaluation(self) -> None:
1020        pass
1021
1022    def start_evaluation_progress(
1023        self,
1024        batched_intervals: t.Dict[Snapshot, Intervals],
1025        environment_naming_info: EnvironmentNamingInfo,
1026        default_catalog: t.Optional[str],
1027        audit_only: bool = False,
1028    ) -> None:
1029        """Indicates that a new snapshot evaluation/auditing progress has begun."""
1030        # Add a newline to separate signal checking from evaluation
1031        if self.signal_progress_logged:
1032            self._print("")
1033
1034        if not self.evaluation_progress_live:
1035            self.evaluation_total_progress = make_progress_bar(
1036                "Executing model batches" if not audit_only else "Auditing models", self.console
1037            )
1038
1039            self.evaluation_model_progress = Progress(
1040                TextColumn("{task.fields[view_name]}", justify="right"),
1041                SpinnerColumn(spinner_name="simpleDots"),
1042                console=self.console,
1043            )
1044
1045            progress_table = Table.grid()
1046            progress_table.add_row(self.evaluation_total_progress)
1047            progress_table.add_row(self.evaluation_model_progress)
1048
1049            self.evaluation_progress_live = Live(
1050                progress_table, console=self.console, refresh_per_second=10
1051            )
1052            self.evaluation_progress_live.start()
1053
1054            batch_sizes = {
1055                snapshot: len(intervals) for snapshot, intervals in batched_intervals.items()
1056            }
1057            message = "Executing" if not audit_only else "Auditing"
1058            self.evaluation_total_task = self.evaluation_total_progress.add_task(
1059                f"{message} models...", total=sum(batch_sizes.values())
1060            )
1061
1062            # determine column widths
1063            self.evaluation_column_widths["annotation"] = (
1064                _calculate_annotation_str_len(
1065                    batched_intervals, self.AUDIT_PADDING, len(" (123.4m rows, 123.4 KiB)")
1066                )
1067                + 3  # brackets and opening escape backslash
1068            )
1069            self.evaluation_column_widths["name"] = max(
1070                len(
1071                    snapshot.display_name(
1072                        environment_naming_info,
1073                        default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1074                        dialect=self.dialect,
1075                    )
1076                )
1077                for snapshot in batched_intervals
1078            )
1079            largest_batch_size = max(batch_sizes.values())
1080            self.evaluation_column_widths["batch"] = len(str(largest_batch_size)) * 2 + 3  # [X/X]
1081            self.evaluation_column_widths["duration"] = 8
1082
1083            self.evaluation_model_batch_sizes = batch_sizes
1084            self.environment_naming_info = environment_naming_info
1085            self.default_catalog = default_catalog
1086
1087    def start_snapshot_evaluation_progress(
1088        self, snapshot: Snapshot, audit_only: bool = False
1089    ) -> None:
1090        if self.evaluation_model_progress and snapshot.name not in self.evaluation_model_tasks:
1091            display_name = snapshot.display_name(
1092                self.environment_naming_info,
1093                self.default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1094                dialect=self.dialect,
1095            )
1096            self.evaluation_model_tasks[snapshot.name] = self.evaluation_model_progress.add_task(
1097                f"{'Evaluating' if not audit_only else 'Auditing'} {display_name}...",
1098                view_name=display_name,
1099                total=self.evaluation_model_batch_sizes[snapshot],
1100            )
1101
1102    def update_snapshot_evaluation_progress(
1103        self,
1104        snapshot: Snapshot,
1105        interval: Interval,
1106        batch_idx: int,
1107        duration_ms: t.Optional[int],
1108        num_audits_passed: int,
1109        num_audits_failed: int,
1110        audit_only: bool = False,
1111        execution_stats: t.Optional[QueryExecutionStats] = None,
1112        auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None,
1113    ) -> None:
1114        """Update the snapshot evaluation progress."""
1115        if (
1116            self.evaluation_total_progress
1117            and self.evaluation_model_progress
1118            and self.evaluation_progress_live
1119        ):
1120            total_batches = self.evaluation_model_batch_sizes[snapshot]
1121            batch_num = str(batch_idx + 1).rjust(len(str(total_batches)))
1122            batch = f"[{batch_num}/{total_batches}]".ljust(self.evaluation_column_widths["batch"])
1123
1124            if duration_ms:
1125                display_name = snapshot.display_name(
1126                    self.environment_naming_info,
1127                    self.default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1128                    dialect=self.dialect,
1129                ).ljust(self.evaluation_column_widths["name"])
1130
1131                annotation = _create_evaluation_model_annotation(
1132                    snapshot, _format_evaluation_model_interval(snapshot, interval), execution_stats
1133                )
1134                audits_str = ""
1135                if num_audits_passed:
1136                    audits_str += f" {self.AUDIT_PASS_MARK}{num_audits_passed}"
1137                if num_audits_failed:
1138                    audits_str += f" {self.AUDIT_FAIL_MARK}{num_audits_failed}"
1139                audits_str = f", audits{audits_str}" if audits_str else ""
1140                annotation_len = self.evaluation_column_widths["annotation"]
1141                # don't adjust the annotation_len if we're using AUDIT_PADDING
1142                annotation = f"\\[{annotation + audits_str}]".ljust(
1143                    annotation_len - 1
1144                    if num_audits_failed and self.AUDIT_PADDING == 0
1145                    else annotation_len
1146                )
1147
1148                duration = f"{(duration_ms / 1000.0):.2f}s".ljust(
1149                    self.evaluation_column_widths["duration"]
1150                )
1151
1152                msg = f"{f'{batch} ' if not audit_only else ''}{display_name}   {annotation}   {duration}".replace(
1153                    self.AUDIT_PASS_MARK, self.GREEN_AUDIT_PASS_MARK
1154                )
1155
1156                self.evaluation_progress_live.console.print(msg)
1157
1158            self.evaluation_total_progress.update(
1159                self.evaluation_total_task or TaskID(0), refresh=True, advance=1
1160            )
1161
1162            model_task_id = self.evaluation_model_tasks[snapshot.name]
1163            self.evaluation_model_progress.update(model_task_id, refresh=True, advance=1)
1164            if (
1165                self.evaluation_model_progress._tasks[model_task_id].completed >= total_batches
1166                or audit_only
1167            ):
1168                self.evaluation_model_progress.remove_task(model_task_id)
1169
1170    def stop_evaluation_progress(self, success: bool = True) -> None:
1171        """Stop the snapshot evaluation progress."""
1172        if self.evaluation_progress_live:
1173            self.evaluation_progress_live.stop()
1174            if success:
1175                self.log_success(f"{self.CHECK_MARK}Model batches executed")
1176
1177        self.evaluation_progress_live = None
1178        self.evaluation_total_progress = None
1179        self.evaluation_total_task = None
1180        self.evaluation_model_progress = None
1181        self.evaluation_model_tasks = {}
1182        self.evaluation_model_batch_sizes = {}
1183        self.evaluation_column_widths = {}
1184        self.environment_naming_info = EnvironmentNamingInfo()
1185        self.default_catalog = None
1186
1187    def start_signal_progress(
1188        self,
1189        snapshot: Snapshot,
1190        default_catalog: t.Optional[str],
1191        environment_naming_info: EnvironmentNamingInfo,
1192    ) -> None:
1193        """Indicates that signal checking has begun for a snapshot."""
1194        display_name = snapshot.display_name(
1195            environment_naming_info,
1196            default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1197            dialect=self.dialect,
1198        )
1199        self.signal_status_tree = Tree(f"Checking signals for {display_name}")
1200
1201    def update_signal_progress(
1202        self,
1203        snapshot: Snapshot,
1204        signal_name: str,
1205        signal_idx: int,
1206        total_signals: int,
1207        ready_intervals: Intervals,
1208        check_intervals: Intervals,
1209        duration: float,
1210    ) -> None:
1211        """Updates the signal checking progress."""
1212        tree = Tree(f"[{signal_idx + 1}/{total_signals}] {signal_name} {duration:.2f}s")
1213
1214        formatted_check_intervals = [_format_signal_interval(snapshot, i) for i in check_intervals]
1215        formatted_ready_intervals = [_format_signal_interval(snapshot, i) for i in ready_intervals]
1216
1217        if not formatted_check_intervals:
1218            formatted_check_intervals = ["no intervals"]
1219        if not formatted_ready_intervals:
1220            formatted_ready_intervals = ["no intervals"]
1221
1222        # Color coding to help detect partial interval ranges quickly
1223        if ready_intervals == check_intervals:
1224            msg = "All ready"
1225            color = "green"
1226        elif ready_intervals:
1227            msg = "Some ready"
1228            color = "yellow"
1229        else:
1230            msg = "None ready"
1231            color = "red"
1232
1233        if self.verbosity < Verbosity.VERY_VERBOSE:
1234            num_check_intervals = len(formatted_check_intervals)
1235            if num_check_intervals > 3:
1236                formatted_check_intervals = formatted_check_intervals[:3]
1237                formatted_check_intervals.append(f"... and {num_check_intervals - 3} more")
1238
1239            num_ready_intervals = len(formatted_ready_intervals)
1240            if num_ready_intervals > 3:
1241                formatted_ready_intervals = formatted_ready_intervals[:3]
1242                formatted_ready_intervals.append(f"... and {num_ready_intervals - 3} more")
1243
1244            check = ", ".join(formatted_check_intervals)
1245            tree.add(f"Check: {check}")
1246
1247            ready = ", ".join(formatted_ready_intervals)
1248            tree.add(f"[{color}]{msg}: {ready}[/{color}]")
1249        else:
1250            check_tree = Tree("Check")
1251            tree.add(check_tree)
1252            for interval in formatted_check_intervals:
1253                check_tree.add(interval)
1254
1255            ready_tree = Tree(f"[{color}]{msg}[/{color}]")
1256            tree.add(ready_tree)
1257            for interval in formatted_ready_intervals:
1258                ready_tree.add(f"[{color}]{interval}[/{color}]")
1259
1260        if self.signal_status_tree is not None:
1261            self.signal_status_tree.add(tree)
1262
1263    def stop_signal_progress(self) -> None:
1264        """Indicates that signal checking has completed for a snapshot."""
1265        if self.signal_status_tree is not None:
1266            self._print(self.signal_status_tree)
1267            self.signal_status_tree = None
1268            self.signal_progress_logged = True
1269
1270    def start_creation_progress(
1271        self,
1272        snapshots: t.List[Snapshot],
1273        environment_naming_info: EnvironmentNamingInfo,
1274        default_catalog: t.Optional[str],
1275    ) -> None:
1276        """Indicates that a new creation progress has begun."""
1277        if self.creation_progress is None:
1278            self.creation_progress = make_progress_bar("Updating physical layer", self.console)
1279
1280            self._print("")
1281            self.creation_progress.start()
1282            self.creation_task = self.creation_progress.add_task(
1283                "Updating physical layer...",
1284                total=len(snapshots),
1285            )
1286
1287            # determine name column widths if we're printing name
1288            if self.verbosity >= Verbosity.VERBOSE:
1289                self.creation_column_widths["name"] = max(
1290                    len(
1291                        snapshot.display_name(
1292                            environment_naming_info,
1293                            default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1294                            dialect=self.dialect,
1295                        )
1296                    )
1297                    for snapshot in snapshots
1298                )
1299
1300            self.environment_naming_info = environment_naming_info
1301            self.default_catalog = default_catalog
1302
1303    def update_creation_progress(self, snapshot: SnapshotInfoLike) -> None:
1304        """Update the snapshot creation progress."""
1305        if self.creation_progress is not None and self.creation_task is not None:
1306            if self.verbosity >= Verbosity.VERBOSE:
1307                msg = snapshot.display_name(
1308                    self.environment_naming_info,
1309                    self.default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1310                    dialect=self.dialect,
1311                ).ljust(self.creation_column_widths["name"])
1312                self.creation_progress.live.console.print(msg + "  [green]created[/green]")
1313            self.creation_progress.update(self.creation_task, refresh=True, advance=1)
1314
1315    def stop_creation_progress(self, success: bool = True) -> None:
1316        """Stop the snapshot creation progress."""
1317        self.creation_task = None
1318        if self.creation_progress is not None:
1319            self.creation_progress.stop()
1320            self.creation_progress = None
1321            if success:
1322                self.log_success(f"\n{self.CHECK_MARK}Physical layer updated")
1323
1324        self.environment_naming_info = EnvironmentNamingInfo()
1325        self.default_catalog = None
1326        self.creation_column_widths = {}
1327
1328    def start_cleanup(self, ignore_ttl: bool) -> bool:
1329        if ignore_ttl:
1330            self._print(
1331                "Are you sure you want to delete all snapshots that are not referenced in any environment?"
1332            )
1333            self._print(
1334                "Note that this may cause a race condition if there are any concurrently running plans."
1335            )
1336            self._print(
1337                "It may also confuse users who were expecting to be able to rollback changes in their development environments."
1338            )
1339            if not self._confirm("Proceed?"):
1340                self.log_error("Cleanup aborted")
1341                return False
1342        return True
1343
1344    def update_cleanup_progress(self, object_name: str) -> None:
1345        """Update the snapshot cleanup progress."""
1346        self._print(f"Deleted object {object_name}")
1347
1348    def stop_cleanup(self, success: bool = False) -> None:
1349        if success:
1350            self.log_success("Cleanup complete.")
1351        else:
1352            self.log_error("Cleanup failed!")
1353
1354    def start_destroy(
1355        self,
1356        schemas_to_delete: t.Optional[t.Set[str]] = None,
1357        views_to_delete: t.Optional[t.Set[str]] = None,
1358        tables_to_delete: t.Optional[t.Set[str]] = None,
1359    ) -> bool:
1360        self.log_warning(
1361            "This will permanently delete all engine-managed objects, state tables and SQLMesh cache.\n"
1362            "The operation may disrupt any currently running or scheduled plans.\n"
1363        )
1364
1365        if schemas_to_delete or views_to_delete or tables_to_delete:
1366            if schemas_to_delete:
1367                self.log_error("Schemas to be deleted:")
1368                for schema in sorted(schemas_to_delete):
1369                    self.log_error(f"  • {schema}")
1370
1371            if views_to_delete:
1372                self.log_error("\nEnvironment views to be deleted:")
1373                for view in sorted(views_to_delete):
1374                    self.log_error(f"  • {view}")
1375
1376            if tables_to_delete:
1377                self.log_error("\nSnapshot tables to be deleted:")
1378                for table in sorted(tables_to_delete):
1379                    self.log_error(f"  • {table}")
1380
1381            self.log_error(
1382                "\nThis action will DELETE ALL the above resources managed by SQLMesh AND\n"
1383                "potentially external resources created by other tools in these schemas.\n"
1384            )
1385
1386        if not self._confirm("Are you ABSOLUTELY SURE you want to proceed with deletion?"):
1387            self.log_error("Destroy operation cancelled.")
1388            return False
1389        return True
1390
1391    def stop_destroy(self, success: bool = False) -> None:
1392        if success:
1393            self.log_success("Destroy completed successfully.")
1394        else:
1395            self.log_error("Destroy failed!")
1396
1397    def start_promotion_progress(
1398        self,
1399        snapshots: t.List[SnapshotTableInfo],
1400        environment_naming_info: EnvironmentNamingInfo,
1401        default_catalog: t.Optional[str],
1402    ) -> None:
1403        """Indicates that a new snapshot promotion progress has begun."""
1404        if snapshots and self.promotion_progress is None:
1405            self.promotion_progress = make_progress_bar(
1406                "Updating virtual layer ", self.console, justify="left"
1407            )
1408
1409            snapshots_with_virtual_views = [
1410                s for s in snapshots if s.is_model and not s.is_symbolic
1411            ]
1412            self.promotion_progress.start()
1413            self.promotion_task = self.promotion_progress.add_task(
1414                f"Virtually updating {environment_naming_info.name}...",
1415                total=len(snapshots_with_virtual_views),
1416            )
1417
1418            # determine name column widths if we're printing names
1419            if self.verbosity >= Verbosity.VERBOSE:
1420                self.promotion_column_widths["name"] = max(
1421                    len(
1422                        snapshot.display_name(
1423                            environment_naming_info,
1424                            default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1425                            dialect=self.dialect,
1426                        )
1427                    )
1428                    for snapshot in snapshots_with_virtual_views
1429                )
1430
1431            self.environment_naming_info = environment_naming_info
1432            self.default_catalog = default_catalog
1433
1434    def update_promotion_progress(self, snapshot: SnapshotInfoLike, promoted: bool) -> None:
1435        """Update the snapshot promotion progress."""
1436        if (
1437            self.promotion_progress is not None
1438            and self.promotion_task is not None
1439            and snapshot.is_model
1440            and not snapshot.is_symbolic
1441        ):
1442            if self.verbosity >= Verbosity.VERBOSE:
1443                display_name = snapshot.display_name(
1444                    self.environment_naming_info,
1445                    self.default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1446                    dialect=self.dialect,
1447                ).ljust(self.promotion_column_widths["name"])
1448                action_str = ""
1449                if promoted:
1450                    action_str = (
1451                        "[yellow]updated[/yellow]"
1452                        if snapshot.previous_version
1453                        else "[green]created[/green]"
1454                    )
1455                action_str = action_str or "[red]dropped[/red]"
1456                self.promotion_progress.live.console.print(f"{display_name}  {action_str}")
1457            self.promotion_progress.update(self.promotion_task, refresh=True, advance=1)
1458
1459    def stop_promotion_progress(self, success: bool = True) -> None:
1460        """Stop the snapshot promotion progress."""
1461        self.promotion_task = None
1462        if self.promotion_progress is not None:
1463            self.promotion_progress.stop()
1464            self.promotion_progress = None
1465            if success:
1466                self.log_success(f"\n{self.CHECK_MARK}Virtual layer updated")
1467
1468        self.environment_naming_info = EnvironmentNamingInfo()
1469        self.default_catalog = None
1470        self.promotion_column_widths = {}
1471
1472    def start_snapshot_migration_progress(self, total_tasks: int) -> None:
1473        """Indicates that a new snapshot migration progress has begun."""
1474        if self.migration_progress is None:
1475            self.migration_progress = make_progress_bar("Migrating snapshots", self.console)
1476
1477            self.migration_progress.start()
1478            self.migration_task = self.migration_progress.add_task(
1479                "Migrating snapshots...",
1480                total=total_tasks,
1481            )
1482
1483    def update_snapshot_migration_progress(self, num_tasks: int) -> None:
1484        """Update the migration progress."""
1485        if self.migration_progress is not None and self.migration_task is not None:
1486            self.migration_progress.update(self.migration_task, refresh=True, advance=num_tasks)
1487
1488    def log_migration_status(self, success: bool = True) -> None:
1489        """Log the migration status."""
1490        if self.migration_progress is not None:
1491            self.migration_progress = None
1492            if success:
1493                self.log_success("Migration completed successfully")
1494
1495    def stop_snapshot_migration_progress(self, success: bool = True) -> None:
1496        """Stop the migration progress."""
1497        self.migration_task = None
1498        if self.migration_progress is not None:
1499            self.migration_progress.stop()
1500            if success:
1501                self.log_success("Snapshots migrated successfully")
1502
1503    def start_env_migration_progress(self, total_tasks: int) -> None:
1504        """Indicates that a new environment migration has begun."""
1505        if self.env_migration_progress is None:
1506            self.env_migration_progress = make_progress_bar("Migrating environments", self.console)
1507            self.env_migration_progress.start()
1508            self.env_migration_task = self.env_migration_progress.add_task(
1509                "Migrating environments...",
1510                total=total_tasks,
1511            )
1512
1513    def update_env_migration_progress(self, num_tasks: int) -> None:
1514        """Update the environment migration progress."""
1515        if self.env_migration_progress is not None and self.env_migration_task is not None:
1516            self.env_migration_progress.update(
1517                self.env_migration_task, refresh=True, advance=num_tasks
1518            )
1519
1520    def stop_env_migration_progress(self, success: bool = True) -> None:
1521        """Stop the environment migration progress."""
1522        self.env_migration_task = None
1523        if self.env_migration_progress is not None:
1524            self.env_migration_progress.stop()
1525            self.env_migration_progress = None
1526            if success:
1527                self.log_success("Environments migrated successfully")
1528
1529    def start_state_export(
1530        self,
1531        output_file: Path,
1532        gateway: t.Optional[str] = None,
1533        state_connection_config: t.Optional[ConnectionConfig] = None,
1534        environment_names: t.Optional[t.List[str]] = None,
1535        local_only: bool = False,
1536        confirm: bool = True,
1537    ) -> bool:
1538        self.state_export_progress = None
1539
1540        if local_only:
1541            self.log_status_update(f"Exporting [b]local[/b] state to '{output_file.as_posix()}'\n")
1542            self.log_warning(
1543                "Local state exports just contain the model versions in your local context. Therefore, the resulting file cannot be imported."
1544            )
1545        else:
1546            self.log_status_update(
1547                f"Exporting state to '{output_file.as_posix()}' from the following connection:\n"
1548            )
1549            if gateway:
1550                self.log_status_update(f"[b]Gateway[/b]: [green]{gateway}[/green]")
1551            if state_connection_config:
1552                self.print_connection_config(state_connection_config, title="State Connection")
1553            if environment_names:
1554                heading = "Environments" if len(environment_names) > 1 else "Environment"
1555                self.log_status_update(
1556                    f"[b]{heading}[/b]: [yellow]{', '.join(environment_names)}[/yellow]"
1557                )
1558
1559        should_continue = True
1560        if confirm:
1561            should_continue = self._confirm("\nContinue?")
1562            self.log_status_update("")
1563
1564        if should_continue:
1565            self.state_export_progress = make_progress_bar("{task.description}", self.console)
1566            assert isinstance(self.state_export_progress, Progress)
1567
1568            self.state_export_version_task = self.state_export_progress.add_task(
1569                "Exporting versions", start=False
1570            )
1571            self.state_export_snapshot_task = self.state_export_progress.add_task(
1572                "Exporting snapshots", start=False
1573            )
1574            self.state_export_environment_task = self.state_export_progress.add_task(
1575                "Exporting environments", start=False
1576            )
1577
1578            self.state_export_progress.start()
1579
1580        return should_continue
1581
1582    def update_state_export_progress(
1583        self,
1584        version_count: t.Optional[int] = None,
1585        versions_complete: bool = False,
1586        snapshot_count: t.Optional[int] = None,
1587        snapshots_complete: bool = False,
1588        environment_count: t.Optional[int] = None,
1589        environments_complete: bool = False,
1590    ) -> None:
1591        if self.state_export_progress:
1592            if self.state_export_version_task is not None:
1593                if version_count is not None:
1594                    self.state_export_progress.start_task(self.state_export_version_task)
1595                    self.state_export_progress.update(
1596                        self.state_export_version_task,
1597                        total=version_count,
1598                        completed=version_count,
1599                        refresh=True,
1600                    )
1601                if versions_complete:
1602                    self.state_export_progress.stop_task(self.state_export_version_task)
1603
1604            if self.state_export_snapshot_task is not None:
1605                if snapshot_count is not None:
1606                    self.state_export_progress.start_task(self.state_export_snapshot_task)
1607                    self.state_export_progress.update(
1608                        self.state_export_snapshot_task,
1609                        total=snapshot_count,
1610                        completed=snapshot_count,
1611                        refresh=True,
1612                    )
1613                if snapshots_complete:
1614                    self.state_export_progress.stop_task(self.state_export_snapshot_task)
1615
1616            if self.state_export_environment_task is not None:
1617                if environment_count is not None:
1618                    self.state_export_progress.start_task(self.state_export_environment_task)
1619                    self.state_export_progress.update(
1620                        self.state_export_environment_task,
1621                        total=environment_count,
1622                        completed=environment_count,
1623                        refresh=True,
1624                    )
1625                if environments_complete:
1626                    self.state_export_progress.stop_task(self.state_export_environment_task)
1627
1628    def stop_state_export(self, success: bool, output_file: Path) -> None:
1629        if self.state_export_progress:
1630            self.state_export_progress.stop()
1631            self.state_export_progress = None
1632
1633            self.log_status_update("")
1634
1635            if success:
1636                self.log_success(f"State exported successfully to '{output_file.as_posix()}'")
1637            else:
1638                self.log_error("State export failed!")
1639
1640    def start_state_import(
1641        self,
1642        input_file: Path,
1643        gateway: str,
1644        state_connection_config: ConnectionConfig,
1645        clear: bool = False,
1646        confirm: bool = True,
1647    ) -> bool:
1648        self.log_status_update(
1649            f"Loading state from '{input_file.as_posix()}' into the following connection:\n"
1650        )
1651        self.log_status_update(f"[b]Gateway[/b]: [green]{gateway}[/green]")
1652        self.print_connection_config(state_connection_config, title="State Connection")
1653        self.log_status_update("")
1654
1655        if clear:
1656            self.log_warning(
1657                f"This [b]destructive[/b] operation will delete all existing state against the '{gateway}' gateway \n"
1658                f"and replace it with what's in the '{input_file.as_posix()}' file.\n"
1659            )
1660        else:
1661            self.log_warning(
1662                f"This operation will [b]merge[/b] the contents of the state file to the state located at the '{gateway}' gateway.\n"
1663                "Matching snapshots or environments will be replaced.\n"
1664                "Non-matching snapshots or environments will be ignored.\n"
1665            )
1666
1667        should_continue = True
1668        if confirm:
1669            should_continue = self._confirm("[red]Are you sure?[/red]")
1670            self.log_status_update("")
1671
1672        if should_continue:
1673            self.state_import_progress = make_progress_bar("{task.description}", self.console)
1674
1675            self.state_import_info = Tree("[bold]State File Information:")
1676
1677            self.state_import_version_task = self.state_import_progress.add_task(
1678                "Importing versions", start=False
1679            )
1680            self.state_import_snapshot_task = self.state_import_progress.add_task(
1681                "Importing snapshots", start=False
1682            )
1683            self.state_import_environment_task = self.state_import_progress.add_task(
1684                "Importing environments", start=False
1685            )
1686
1687            self.state_import_progress.start()
1688
1689        return should_continue
1690
1691    def update_state_import_progress(
1692        self,
1693        timestamp: t.Optional[str] = None,
1694        state_file_version: t.Optional[int] = None,
1695        versions: t.Optional[Versions] = None,
1696        snapshot_count: t.Optional[int] = None,
1697        snapshots_complete: bool = False,
1698        environment_count: t.Optional[int] = None,
1699        environments_complete: bool = False,
1700    ) -> None:
1701        if self.state_import_progress:
1702            if self.state_import_info:
1703                if timestamp:
1704                    self.state_import_info.add(f"Creation Timestamp: {timestamp}")
1705                if state_file_version:
1706                    self.state_import_info.add(f"File Version: {state_file_version}")
1707                if versions:
1708                    self.state_import_info.add(f"SQLMesh version: {versions.sqlmesh_version}")
1709                    self.state_import_info.add(
1710                        f"SQLMesh migration version: {versions.schema_version}"
1711                    )
1712                    self.state_import_info.add(f"SQLGlot version: {versions.sqlglot_version}\n")
1713
1714                    self._print(self.state_import_info)
1715
1716                    version_count = len(versions.model_dump())
1717
1718                    if self.state_import_version_task is not None:
1719                        self.state_import_progress.start_task(self.state_import_version_task)
1720                        self.state_import_progress.update(
1721                            self.state_import_version_task,
1722                            total=version_count,
1723                            completed=version_count,
1724                        )
1725                        self.state_import_progress.stop_task(self.state_import_version_task)
1726
1727            if self.state_import_snapshot_task is not None:
1728                if snapshot_count is not None:
1729                    self.state_import_progress.start_task(self.state_import_snapshot_task)
1730                    self.state_import_progress.update(
1731                        self.state_import_snapshot_task,
1732                        completed=snapshot_count,
1733                        total=snapshot_count,
1734                        refresh=True,
1735                    )
1736
1737                if snapshots_complete:
1738                    self.state_import_progress.stop_task(self.state_import_snapshot_task)
1739
1740            if self.state_import_environment_task is not None:
1741                if environment_count is not None:
1742                    self.state_import_progress.start_task(self.state_import_environment_task)
1743                    self.state_import_progress.update(
1744                        self.state_import_environment_task,
1745                        completed=environment_count,
1746                        total=environment_count,
1747                        refresh=True,
1748                    )
1749
1750                if environments_complete:
1751                    self.state_import_progress.stop_task(self.state_import_environment_task)
1752
1753    def stop_state_import(self, success: bool, input_file: Path) -> None:
1754        if self.state_import_progress:
1755            self.state_import_progress.stop()
1756            self.state_import_progress = None
1757
1758            self.log_status_update("")
1759
1760            if success:
1761                self.log_success(f"State imported successfully from '{input_file.as_posix()}'")
1762            else:
1763                self.log_error("State import failed!")
1764
1765    def show_environment_difference_summary(
1766        self,
1767        context_diff: ContextDiff,
1768        no_diff: bool = True,
1769    ) -> None:
1770        """Shows a summary of the environment differences.
1771
1772        Args:
1773            context_diff: The context diff to use to print the summary
1774            no_diff: Hide the actual environment statement differences.
1775        """
1776        if context_diff.is_new_environment:
1777            msg = (
1778                f"\n`{context_diff.environment}` environment will be initialized"
1779                if not context_diff.create_from_env_exists
1780                else f"\nNew environment `{context_diff.environment}` will be created from `{context_diff.create_from}`"
1781            )
1782            self._print(Tree(f"[bold]{msg}\n"))
1783            if not context_diff.has_snapshot_changes:
1784                return
1785
1786        if not context_diff.has_changes:
1787            # This is only reached when the plan is against an existing environment, so we use the environment
1788            #   name instead of the create_from name. The equivalent message for new environments happens in
1789            #   the PlanBuilder.
1790            self._print(
1791                Tree(
1792                    f"\n[bold]No changes to plan: project files match the `{context_diff.environment}` environment\n"
1793                )
1794            )
1795            return
1796
1797        if not context_diff.is_new_environment or (
1798            context_diff.is_new_environment and context_diff.create_from_env_exists
1799        ):
1800            self._print(
1801                Tree(
1802                    f"\n[bold]Differences from the `{context_diff.create_from if context_diff.is_new_environment else context_diff.environment}` environment:\n"
1803                )
1804            )
1805
1806        if context_diff.has_requirement_changes:
1807            self._print(f"[bold]Requirements:\n{context_diff.requirements_diff()}")
1808
1809        if context_diff.has_environment_statements_changes and not no_diff:
1810            self._print("[bold]Environment statements:\n")
1811            for type, diff in context_diff.environment_statements_diff(
1812                include_python_env=not context_diff.is_new_environment
1813            ):
1814                self._print(Syntax(diff, type, line_numbers=False))
1815
1816    def show_model_difference_summary(
1817        self,
1818        context_diff: ContextDiff,
1819        environment_naming_info: EnvironmentNamingInfo,
1820        default_catalog: t.Optional[str],
1821        no_diff: bool = True,
1822    ) -> None:
1823        """Shows a summary of the model differences.
1824
1825        Args:
1826            context_diff: The context diff to use to print the summary
1827            environment_naming_info: The environment naming info to reference when printing model names
1828            default_catalog: The default catalog to reference when deciding to remove catalog from display names
1829            no_diff: Hide the actual SQL differences.
1830        """
1831        self._show_summary_tree_for(
1832            context_diff,
1833            "Models",
1834            lambda x: x.is_model,
1835            environment_naming_info,
1836            default_catalog,
1837            no_diff=no_diff,
1838        )
1839        self._show_summary_tree_for(
1840            context_diff,
1841            "Standalone Audits",
1842            lambda x: x.is_audit,
1843            environment_naming_info,
1844            default_catalog,
1845            no_diff=no_diff,
1846        )
1847
1848    def plan(
1849        self,
1850        plan_builder: PlanBuilder,
1851        auto_apply: bool,
1852        default_catalog: t.Optional[str],
1853        no_diff: bool = False,
1854        no_prompts: bool = False,
1855    ) -> None:
1856        """The main plan flow.
1857
1858        The console should present the user with choices on how to backfill and version the snapshots
1859        of a plan.
1860
1861        Args:
1862            plan: The plan to make choices for.
1863            auto_apply: Whether to automatically apply the plan after all choices have been made.
1864            default_catalog: The default catalog to reference when deciding to remove catalog from display names
1865            no_diff: Hide text differences for changed models.
1866            no_prompts: Whether to disable interactive prompts for the backfill time range. Please note that
1867                if this flag is set to true and there are uncategorized changes the plan creation will
1868                fail. Default: False
1869        """
1870        self._prompt_categorize(
1871            plan_builder,
1872            auto_apply,
1873            no_diff=no_diff,
1874            no_prompts=no_prompts,
1875            default_catalog=default_catalog,
1876        )
1877
1878        self._show_options_after_categorization(
1879            plan_builder, auto_apply, default_catalog=default_catalog, no_prompts=no_prompts
1880        )
1881
1882        if auto_apply:
1883            plan_builder.apply()
1884
1885    def _show_summary_tree_for(
1886        self,
1887        context_diff: ContextDiff,
1888        header: str,
1889        snapshot_selector: t.Callable[[SnapshotInfoLike], bool],
1890        environment_naming_info: EnvironmentNamingInfo,
1891        default_catalog: t.Optional[str],
1892        no_diff: bool = True,
1893    ) -> None:
1894        added_snapshot_ids = {
1895            s_id for s_id in context_diff.added if snapshot_selector(context_diff.snapshots[s_id])
1896        }
1897        removed_snapshot_ids = {
1898            s_id
1899            for s_id, snapshot in context_diff.removed_snapshots.items()
1900            if snapshot_selector(snapshot)
1901        }
1902        modified_snapshot_ids = {
1903            current_snapshot.snapshot_id
1904            for _, (current_snapshot, _) in context_diff.modified_snapshots.items()
1905            if snapshot_selector(current_snapshot)
1906        }
1907
1908        tree_sets = (
1909            added_snapshot_ids,
1910            removed_snapshot_ids,
1911            modified_snapshot_ids,
1912        )
1913        if all(not s_ids for s_ids in tree_sets):
1914            return
1915
1916        tree = Tree(f"[bold]{header}:")
1917        if added_snapshot_ids:
1918            added_tree = Tree("[bold][added]Added:")
1919            for s_id in sorted(added_snapshot_ids):
1920                snapshot = context_diff.snapshots[s_id]
1921                added_tree.add(
1922                    f"[added]{snapshot.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}"
1923                )
1924            tree.add(self._limit_model_names(added_tree, self.verbosity))
1925        if removed_snapshot_ids:
1926            removed_tree = Tree("[bold][removed]Removed:")
1927            for s_id in sorted(removed_snapshot_ids):
1928                snapshot_table_info = context_diff.removed_snapshots[s_id]
1929                removed_tree.add(
1930                    f"[removed]{snapshot_table_info.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}"
1931                )
1932            tree.add(self._limit_model_names(removed_tree, self.verbosity))
1933        if modified_snapshot_ids:
1934            tree = self._add_modified_models(
1935                context_diff,
1936                modified_snapshot_ids,
1937                tree,
1938                environment_naming_info,
1939                default_catalog,
1940                no_diff,
1941            )
1942
1943        self._print(tree)
1944
1945    def _add_modified_models(
1946        self,
1947        context_diff: ContextDiff,
1948        modified_snapshot_ids: t.Set[SnapshotId],
1949        tree: Tree,
1950        environment_naming_info: EnvironmentNamingInfo,
1951        default_catalog: t.Optional[str] = None,
1952        no_diff: bool = True,
1953    ) -> Tree:
1954        direct = Tree("[bold][direct]Directly Modified:")
1955        indirect = Tree("[bold][indirect]Indirectly Modified:")
1956        metadata = Tree("[bold][metadata]Metadata Updated:")
1957        for s_id in modified_snapshot_ids:
1958            name = s_id.name
1959            display_name = context_diff.snapshots[s_id].display_name(
1960                environment_naming_info,
1961                default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1962                dialect=self.dialect,
1963            )
1964            if context_diff.directly_modified(name):
1965                direct.add(
1966                    f"[direct]{display_name}"
1967                    if no_diff
1968                    else Syntax(f"{display_name}\n{context_diff.text_diff(name)}", "sql")
1969                )
1970            elif context_diff.indirectly_modified(name):
1971                indirect.add(f"[indirect]{display_name}")
1972            elif context_diff.metadata_updated(name):
1973                metadata.add(
1974                    f"[metadata]{display_name}"
1975                    if no_diff
1976                    else Syntax(f"{display_name}\n{context_diff.text_diff(name)}", "sql")
1977                )
1978        if direct.children:
1979            tree.add(direct)
1980        if indirect.children:
1981            tree.add(self._limit_model_names(indirect, self.verbosity))
1982        if metadata.children:
1983            tree.add(metadata)
1984        return tree
1985
1986    def _show_options_after_categorization(
1987        self,
1988        plan_builder: PlanBuilder,
1989        auto_apply: bool,
1990        default_catalog: t.Optional[str],
1991        no_prompts: bool,
1992    ) -> None:
1993        plan = plan_builder.build()
1994        if not no_prompts and plan.forward_only and plan.new_snapshots:
1995            self._prompt_effective_from(plan_builder, auto_apply, default_catalog)
1996
1997        if plan.requires_backfill:
1998            self._show_missing_dates(plan_builder.build(), default_catalog)
1999
2000            if not no_prompts:
2001                self._prompt_backfill(plan_builder, auto_apply, default_catalog)
2002
2003            backfill_or_preview = "preview" if plan.is_dev and plan.forward_only else "backfill"
2004            if not auto_apply and self._confirm(
2005                f"Apply - {backfill_or_preview.capitalize()} Tables"
2006            ):
2007                plan_builder.apply()
2008        elif plan.has_changes and not auto_apply:
2009            self._prompt_promote(plan_builder)
2010        elif plan.has_unmodified_unpromoted and not auto_apply:
2011            self.log_status_update("\n[bold]Virtually updating unmodified models\n")
2012            self._prompt_promote(plan_builder)
2013
2014    def _prompt_categorize(
2015        self,
2016        plan_builder: PlanBuilder,
2017        auto_apply: bool,
2018        no_diff: bool,
2019        no_prompts: bool,
2020        default_catalog: t.Optional[str],
2021    ) -> None:
2022        """Get the user's change category for the directly modified models."""
2023        plan = plan_builder.build()
2024
2025        if plan.restatements:
2026            # A plan can have restatements for the following reasons:
2027            # - The user specifically called `sqlmesh plan` with --restate-model.
2028            #   This creates a "restatement plan" which disallows all other changes and simply force-backfills
2029            #   the selected models and their downstream dependencies using the versions of the models stored in state.
2030            # - There are no specific restatements (so changes are allowed) AND dev previews need to be computed.
2031            #   The "restatements" feature is currently reused for dev previews.
2032            if plan.selected_models_to_restate:
2033                # There were legitimate restatements, no dev previews
2034                tree = Tree(
2035                    "[bold]Models selected for restatement:[/bold]\n"
2036                    "This causes backfill of the model itself as well as affected downstream models"
2037                )
2038                model_fqn_to_snapshot = {s.name: s for s in plan.snapshots.values()}
2039                for model_fqn in plan.selected_models_to_restate:
2040                    snapshot = model_fqn_to_snapshot[model_fqn]
2041                    display_name = snapshot.display_name(
2042                        plan.environment_naming_info,
2043                        default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
2044                        dialect=self.dialect,
2045                    )
2046                    tree.add(
2047                        display_name
2048                    )  # note: we deliberately dont show any intervals here; they get shown in the backfill section
2049                self._print(tree)
2050            else:
2051                # We are computing dev previews, do not confuse the user by printing out something to do
2052                # with restatements. Dev previews are already highlighted in the backfill step
2053                pass
2054        else:
2055            self.show_environment_difference_summary(
2056                plan.context_diff,
2057                no_diff=no_diff,
2058            )
2059
2060        if plan.context_diff.has_changes:
2061            self.show_model_difference_summary(
2062                plan.context_diff,
2063                plan.environment_naming_info,
2064                default_catalog=default_catalog,
2065            )
2066
2067        if not no_diff:
2068            self._show_categorized_snapshots(plan, default_catalog)
2069
2070        for snapshot in plan.uncategorized:
2071            if snapshot.is_model and snapshot.model.forward_only:
2072                continue
2073            if not no_diff:
2074                self.show_sql(plan.context_diff.text_diff(snapshot.name))
2075            tree = Tree(
2076                f"[bold][direct]Directly Modified: {snapshot.display_name(plan.environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}"
2077            )
2078            indirect_tree = None
2079
2080            for child_sid in sorted(plan.indirectly_modified.get(snapshot.snapshot_id, set())):
2081                child_snapshot = plan.context_diff.snapshots[child_sid]
2082                if not indirect_tree:
2083                    indirect_tree = Tree("[indirect]Indirectly Modified Children:")
2084                    tree.add(indirect_tree)
2085                indirect_tree.add(
2086                    f"[indirect]{child_snapshot.display_name(plan.environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}"
2087                )
2088            if indirect_tree:
2089                indirect_tree = self._limit_model_names(indirect_tree, self.verbosity)
2090
2091            self._print(tree)
2092            if not no_prompts:
2093                self._get_snapshot_change_category(
2094                    snapshot, plan_builder, auto_apply, default_catalog
2095                )
2096
2097    def _show_categorized_snapshots(self, plan: Plan, default_catalog: t.Optional[str]) -> None:
2098        context_diff = plan.context_diff
2099
2100        for snapshot in plan.categorized:
2101            if context_diff.directly_modified(snapshot.name):
2102                category_str = SNAPSHOT_CHANGE_CATEGORY_STR[snapshot.change_category]
2103                tree = Tree(
2104                    f"\n[bold][direct]Directly Modified: {snapshot.display_name(plan.environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)} ({category_str})"
2105                )
2106                indirect_tree = None
2107                for child_sid in sorted(plan.indirectly_modified.get(snapshot.snapshot_id, set())):
2108                    child_snapshot = context_diff.snapshots[child_sid]
2109                    if not indirect_tree:
2110                        indirect_tree = Tree("[indirect]Indirectly Modified Children:")
2111                        tree.add(indirect_tree)
2112                    child_category_str = SNAPSHOT_CHANGE_CATEGORY_STR[
2113                        child_snapshot.change_category
2114                    ]
2115                    indirect_tree.add(
2116                        f"[indirect]{child_snapshot.display_name(plan.environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)} ({child_category_str})"
2117                    )
2118                if indirect_tree:
2119                    indirect_tree = self._limit_model_names(indirect_tree, self.verbosity)
2120            elif context_diff.metadata_updated(snapshot.name):
2121                tree = Tree(
2122                    f"\n[bold][metadata]Metadata Updated: {snapshot.display_name(plan.environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}"
2123                )
2124            else:
2125                continue
2126
2127            text_diff = context_diff.text_diff(snapshot.name)
2128            if text_diff:
2129                self._print("")
2130                self._print(Syntax(text_diff, "sql", word_wrap=True))
2131            self._print(tree)
2132
2133    def _show_missing_dates(self, plan: Plan, default_catalog: t.Optional[str]) -> None:
2134        """Displays the models with missing dates."""
2135        missing_intervals = plan.missing_intervals
2136        if not missing_intervals:
2137            return
2138        backfill = Tree("[bold]Models needing backfill:[/bold]")
2139        for missing in missing_intervals:
2140            snapshot = plan.context_diff.snapshots[missing.snapshot_id]
2141            if not snapshot.is_model:
2142                continue
2143
2144            preview_modifier = ""
2145            if not plan.deployability_index.is_deployable(snapshot):
2146                preview_modifier = " ([orange1]preview[/orange1])"
2147
2148            display_name = snapshot.display_name(
2149                plan.environment_naming_info,
2150                default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
2151                dialect=self.dialect,
2152            )
2153            backfill.add(
2154                f"{display_name}: \\[{_format_missing_intervals(snapshot, missing)}]{preview_modifier}"
2155            )
2156
2157        if backfill:
2158            backfill = self._limit_model_names(backfill, self.verbosity)
2159        self._print(backfill)
2160
2161    def _prompt_effective_from(
2162        self, plan_builder: PlanBuilder, auto_apply: bool, default_catalog: t.Optional[str]
2163    ) -> None:
2164        if not plan_builder.build().effective_from:
2165            effective_from = self._prompt(
2166                "Enter the effective date (eg. '1 year', '2020-01-01') to apply forward-only changes retroactively or blank to only apply them going forward once changes are deployed to prod"
2167            )
2168            if effective_from:
2169                plan_builder.set_effective_from(effective_from)
2170
2171    def _prompt_backfill(
2172        self, plan_builder: PlanBuilder, auto_apply: bool, default_catalog: t.Optional[str]
2173    ) -> None:
2174        plan = plan_builder.build()
2175        is_forward_only_dev = plan.is_dev and plan.forward_only
2176        backfill_or_preview = "preview" if is_forward_only_dev else "backfill"
2177
2178        if plan_builder.is_start_and_end_allowed:
2179            if not plan_builder.override_start:
2180                if is_forward_only_dev:
2181                    if plan.effective_from:
2182                        blank_meaning = f"to preview starting from the effective date ('{time_like_to_str(plan.effective_from)}')"
2183                        default_start = plan.effective_from
2184                    else:
2185                        blank_meaning = "to preview starting from yesterday"
2186                        default_start = yesterday_ds()
2187                else:
2188                    if plan.provided_start:
2189                        blank_meaning = f"starting from '{time_like_to_str(plan.provided_start)}'"
2190                    else:
2191                        blank_meaning = "from the beginning of history"
2192                    default_start = None
2193
2194                start = self._prompt(
2195                    f"Enter the {backfill_or_preview} start date (eg. '1 year', '2020-01-01') or blank to backfill {blank_meaning}",
2196                )
2197                if start:
2198                    plan_builder.set_start(start)
2199                elif default_start:
2200                    plan_builder.set_start(default_start)
2201
2202            if not plan_builder.override_end:
2203                if plan.provided_end:
2204                    blank_meaning = f"'{time_like_to_str(plan.provided_end)}'"
2205                elif plan.end_override_per_model:
2206                    max_end = max(plan.end_override_per_model.values())
2207                    blank_meaning = f"'{time_like_to_str(max_end)}'"
2208                else:
2209                    blank_meaning = "now"
2210                end = self._prompt(
2211                    f"Enter the {backfill_or_preview} end date (eg. '1 month ago', '2020-01-01') or blank to {backfill_or_preview} up until {blank_meaning}",
2212                )
2213                if end:
2214                    plan_builder.set_end(end)
2215
2216            plan = plan_builder.build()
2217
2218    def _prompt_promote(self, plan_builder: PlanBuilder) -> None:
2219        if self._confirm(
2220            "Apply - Virtual Update",
2221        ):
2222            plan_builder.apply()
2223
2224    def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
2225        # We don't log the test results if no tests were ran
2226        if not result.testsRun:
2227            return
2228
2229        divider_length = 70
2230
2231        self._log_test_details(result)
2232
2233        message = (
2234            f"Ran {result.testsRun} tests against {target_dialect} in {result.duration} seconds."
2235        )
2236        if result.wasSuccessful():
2237            self._print("=" * divider_length)
2238            self._print(
2239                f"Successfully {message}",
2240                style="green",
2241            )
2242            self._print("-" * divider_length)
2243        else:
2244            self._print("-" * divider_length)
2245            self._print("Test Failure Summary", style="red")
2246            self._print("=" * divider_length)
2247            fail_and_error_tests = result.get_fail_and_error_tests()
2248            self._print(f"{message} \n")
2249
2250            self._print(f"Failed tests ({len(fail_and_error_tests)}):")
2251            for test in fail_and_error_tests:
2252                self._print(f" • {test.path}::{test.test_name}")
2253            self._print("=" * divider_length, end="\n\n")
2254
2255    def _captured_unit_test_results(self, result: ModelTextTestResult) -> str:
2256        with self.console.capture() as capture:
2257            self._log_test_details(result)
2258        return strip_ansi_codes(capture.get())
2259
2260    def show_sql(self, sql: str) -> None:
2261        self._print(Syntax(sql, "sql", word_wrap=True), crop=False)
2262
2263    def log_status_update(self, message: str) -> None:
2264        self._print(message)
2265
2266    def log_skipped_models(self, snapshot_names: t.Set[str]) -> None:
2267        if snapshot_names:
2268            msg = "  " + "\n  ".join(snapshot_names)
2269            self._print(f"[dark_orange3]Skipped models[/dark_orange3]\n\n{msg}")
2270
2271    def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None:
2272        if errors:
2273            self._print("\n[red]Failed models[/red]\n")
2274
2275            error_messages = _format_node_errors(errors)
2276
2277            for node_name, msg in error_messages.items():
2278                self._print(f"  [red]{node_name}[/red]\n\n{msg}")
2279
2280    def log_models_updated_during_restatement(
2281        self,
2282        snapshots: t.List[t.Tuple[SnapshotTableInfo, SnapshotTableInfo]],
2283        environment_naming_info: EnvironmentNamingInfo,
2284        default_catalog: t.Optional[str] = None,
2285    ) -> None:
2286        if snapshots:
2287            tree = Tree(
2288                f"[yellow]The following models had new versions deployed while data was being restated:[/yellow]"
2289            )
2290
2291            for restated_snapshot, updated_snapshot in snapshots:
2292                display_name = restated_snapshot.display_name(
2293                    environment_naming_info,
2294                    default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
2295                    dialect=self.dialect,
2296                )
2297                current_branch = tree.add(display_name)
2298                current_branch.add(f"restated version: '{restated_snapshot.version}'")
2299                current_branch.add(f"currently active version: '{updated_snapshot.version}'")
2300
2301            self._print(tree)
2302            self._print("")  # newline spacer
2303
2304    def log_destructive_change(
2305        self,
2306        snapshot_name: str,
2307        alter_operations: t.List[TableAlterOperation],
2308        dialect: str,
2309        error: bool = True,
2310    ) -> None:
2311        if error:
2312            self._print(format_destructive_change_msg(snapshot_name, alter_operations, dialect))
2313        else:
2314            self.log_warning(
2315                format_destructive_change_msg(snapshot_name, alter_operations, dialect, error)
2316            )
2317
2318    def log_additive_change(
2319        self,
2320        snapshot_name: str,
2321        alter_operations: t.List[TableAlterOperation],
2322        dialect: str,
2323        error: bool = True,
2324    ) -> None:
2325        if error:
2326            self._print(format_additive_change_msg(snapshot_name, alter_operations, dialect))
2327        else:
2328            self.log_warning(
2329                format_additive_change_msg(snapshot_name, alter_operations, dialect, error)
2330            )
2331
2332    def log_error(self, message: str) -> None:
2333        self._print(f"[red]{message}[/red]")
2334
2335    def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None:
2336        logger.warning(long_message or short_message)
2337        if not self.ignore_warnings:
2338            if long_message:
2339                file_path = None
2340                for handler in logger.root.handlers:
2341                    if isinstance(handler, logging.FileHandler):
2342                        file_path = handler.baseFilename
2343                        break
2344                file_path_msg = f" Learn more in logs: {file_path}\n" if file_path else ""
2345                short_message = f"{short_message}{file_path_msg}"
2346            message_lstrip = short_message.lstrip()
2347            leading_ws = short_message[: -len(message_lstrip)]
2348            message_formatted = f"{leading_ws}[yellow]\\[WARNING] {message_lstrip}[/yellow]"
2349            self._print(message_formatted)
2350
2351    def log_success(self, message: str) -> None:
2352        self._print(f"[green]{message}[/green]\n")
2353
2354    def loading_start(self, message: t.Optional[str] = None) -> uuid.UUID:
2355        id = uuid.uuid4()
2356        self.loading_status[id] = Status(message or "", console=self.console, spinner="line")
2357        self.loading_status[id].start()
2358        return id
2359
2360    def loading_stop(self, id: uuid.UUID) -> None:
2361        self.loading_status[id].stop()
2362        del self.loading_status[id]
2363
2364    def show_table_diff_details(
2365        self,
2366        models_to_diff: t.List[str],
2367    ) -> None:
2368        """Display information about which tables are going to be diffed"""
2369
2370        if models_to_diff:
2371            m_tree = Tree("\n[b]Models to compare:")
2372            for m in models_to_diff:
2373                m_tree.add(f"[{self.TABLE_DIFF_SOURCE_BLUE}]{m}[/{self.TABLE_DIFF_SOURCE_BLUE}]")
2374            self._print(m_tree)
2375            self._print("")
2376
2377    def start_table_diff_progress(self, models_to_diff: int) -> None:
2378        if not self.table_diff_progress:
2379            self.table_diff_progress = make_progress_bar(
2380                "Calculating model differences", self.console
2381            )
2382            self.table_diff_model_progress = Progress(
2383                TextColumn("{task.fields[view_name]}", justify="right"),
2384                SpinnerColumn(spinner_name="simpleDots"),
2385                console=self.console,
2386            )
2387
2388            progress_table = Table.grid()
2389            progress_table.add_row(self.table_diff_progress)
2390            progress_table.add_row(self.table_diff_model_progress)
2391
2392            self.table_diff_progress_live = Live(progress_table, refresh_per_second=10)
2393            self.table_diff_progress_live.start()
2394
2395            self.table_diff_model_task = self.table_diff_progress.add_task(
2396                "Diffing", total=models_to_diff
2397            )
2398
2399    def start_table_diff_model_progress(self, model: str) -> None:
2400        if self.table_diff_model_progress and model not in self.table_diff_model_tasks:
2401            self.table_diff_model_tasks[model] = self.table_diff_model_progress.add_task(
2402                f"Diffing {model}...",
2403                view_name=model,
2404                total=1,
2405            )
2406
2407    def update_table_diff_progress(self, model: str) -> None:
2408        if self.table_diff_progress:
2409            self.table_diff_progress.update(self.table_diff_model_task, refresh=True, advance=1)
2410        if self.table_diff_model_progress and model in self.table_diff_model_tasks:
2411            model_task_id = self.table_diff_model_tasks[model]
2412            self.table_diff_model_progress.remove_task(model_task_id)
2413
2414    def stop_table_diff_progress(self, success: bool) -> None:
2415        if self.table_diff_progress_live:
2416            self.table_diff_progress_live.stop()
2417            self.table_diff_progress_live = None
2418            self.log_status_update("")
2419
2420            if success:
2421                self.log_success(f"Table diff completed successfully!")
2422            else:
2423                self.log_error("Table diff failed!")
2424
2425        self.table_diff_progress = None
2426        self.table_diff_model_progress = None
2427        self.table_diff_model_tasks = {}
2428
2429    def show_table_diff_summary(self, table_diff: TableDiff) -> None:
2430        tree = Tree("\n[b]Table Diff")
2431
2432        if table_diff.model_name:
2433            model = Tree("Model:")
2434            model.add(f"[blue]{table_diff.model_name}[/blue]")
2435
2436            tree.add(model)
2437
2438            envs = Tree("Environment:")
2439            source = Tree(
2440                f"Source: [{self.TABLE_DIFF_SOURCE_BLUE}]{table_diff.source_alias}[/{self.TABLE_DIFF_SOURCE_BLUE}]"
2441            )
2442            envs.add(source)
2443
2444            target = Tree(
2445                f"Target: [{self.TABLE_DIFF_TARGET_GREEN}]{table_diff.target_alias}[/{self.TABLE_DIFF_TARGET_GREEN}]"
2446            )
2447            envs.add(target)
2448
2449            tree.add(envs)
2450
2451        tables = Tree("Tables:")
2452
2453        tables.add(
2454            f"Source: [{self.TABLE_DIFF_SOURCE_BLUE}]{table_diff.source}[/{self.TABLE_DIFF_SOURCE_BLUE}]"
2455        )
2456        tables.add(
2457            f"Target: [{self.TABLE_DIFF_TARGET_GREEN}]{table_diff.target}[/{self.TABLE_DIFF_TARGET_GREEN}]"
2458        )
2459
2460        tree.add(tables)
2461
2462        join = Tree("Join On:")
2463        _, _, key_column_names = table_diff.key_columns
2464        for col_name in key_column_names:
2465            join.add(f"[yellow]{col_name}[/yellow]")
2466
2467        tree.add(join)
2468
2469        self._print(tree)
2470
2471    def show_schema_diff(self, schema_diff: SchemaDiff) -> None:
2472        source_name = schema_diff.source
2473        if schema_diff.source_alias:
2474            source_name = schema_diff.source_alias.upper()
2475        target_name = schema_diff.target
2476        if schema_diff.target_alias:
2477            target_name = schema_diff.target_alias.upper()
2478
2479        first_line = f"\n[b]Schema Diff Between '[{self.TABLE_DIFF_SOURCE_BLUE}]{source_name}[/{self.TABLE_DIFF_SOURCE_BLUE}]' and '[{self.TABLE_DIFF_TARGET_GREEN}]{target_name}[/{self.TABLE_DIFF_TARGET_GREEN}]'"
2480        if schema_diff.model_name:
2481            first_line = (
2482                first_line + f" environments for model '[blue]{schema_diff.model_name}[/blue]'"
2483            )
2484
2485        tree = Tree(first_line + ":")
2486
2487        if any([schema_diff.added, schema_diff.removed, schema_diff.modified]):
2488            if schema_diff.added:
2489                added = Tree("[green]Added Columns:")
2490                for c, t in schema_diff.added:
2491                    added.add(f"[green]{c} ({t})")
2492                tree.add(added)
2493
2494            if schema_diff.removed:
2495                removed = Tree("[red]Removed Columns:")
2496                for c, t in schema_diff.removed:
2497                    removed.add(f"[red]{c} ({t})")
2498                tree.add(removed)
2499
2500            if schema_diff.modified:
2501                modified = Tree("[magenta]Modified Columns:")
2502                for c, (ft, tt) in schema_diff.modified.items():
2503                    modified.add(f"[magenta]{c} ({ft} -> {tt})")
2504                tree.add(modified)
2505        else:
2506            tree.add("[b]Schemas match")
2507
2508        self.console.print(tree)
2509
2510    def show_row_diff(
2511        self, row_diff: RowDiff, show_sample: bool = True, skip_grain_check: bool = False
2512    ) -> None:
2513        if row_diff.empty:
2514            self.console.print(
2515                "\n[b][red]Neither the source nor the target table contained any records[/red][/b]"
2516            )
2517            return
2518
2519        source_name = row_diff.source
2520        if row_diff.source_alias:
2521            source_name = row_diff.source_alias.upper()
2522        target_name = row_diff.target
2523        if row_diff.target_alias:
2524            target_name = row_diff.target_alias.upper()
2525
2526        if row_diff.stats["null_grain_count"] > 0 or (
2527            not skip_grain_check
2528            and (
2529                row_diff.stats["distinct_count_s"] != row_diff.stats["s_count"]
2530                or row_diff.stats["distinct_count_t"] != row_diff.stats["t_count"]
2531            )
2532        ):
2533            self.console.print(
2534                "[b][red]\nGrain should have unique and not-null audits for accurate results.[/red][/b]"
2535            )
2536
2537        tree = Tree("[b]Row Counts:[/b]")
2538        if row_diff.full_match_count:
2539            tree.add(
2540                f" [b][cyan]FULL MATCH[/cyan]:[/b] {row_diff.full_match_count} rows ({row_diff.full_match_pct}%)"
2541            )
2542        if row_diff.partial_match_count:
2543            tree.add(
2544                f" [b][blue]PARTIAL MATCH[/blue]:[/b] {row_diff.partial_match_count} rows ({row_diff.partial_match_pct}%)"
2545            )
2546        if row_diff.s_only_count:
2547            tree.add(
2548                f" [b][yellow]{source_name} ONLY[/yellow]:[/b] {row_diff.s_only_count} rows ({row_diff.s_only_pct}%)"
2549            )
2550        if row_diff.t_only_count:
2551            tree.add(
2552                f" [b][green]{target_name} ONLY[/green]:[/b] {row_diff.t_only_count} rows ({row_diff.t_only_pct}%)"
2553            )
2554        self.console.print("\n", tree)
2555
2556        self.console.print("\n[b][blue]COMMON ROWS[/blue] column comparison stats:[/b]")
2557        if row_diff.column_stats.shape[0] > 0:
2558            self.console.print(row_diff.column_stats.to_string(index=True), end="\n\n")
2559        else:
2560            self.console.print("  No columns with same name and data type in both tables")
2561
2562        if show_sample:
2563            sample = row_diff.joined_sample
2564            self.console.print("\n[b][blue]COMMON ROWS[/blue] sample data differences:[/b]")
2565            if sample.shape[0] > 0:
2566                keys: list[str] = []
2567                columns: dict[str, list[str]] = {}
2568                source_prefix, source_name = (
2569                    (f"{source_name}__", source_name)
2570                    if source_name.lower() != row_diff.source.lower()
2571                    else ("s__", "SOURCE")
2572                )
2573                target_prefix, target_name = (
2574                    (f"{target_name}__", target_name)
2575                    if target_name.lower() != row_diff.target.lower()
2576                    else ("t__", "TARGET")
2577                )
2578
2579                # Extract key and column names from the joined sample
2580                for column in row_diff.joined_sample.columns:
2581                    if source_prefix in column:
2582                        column_name = "__".join(column.split(source_prefix)[1:])
2583                        columns[column_name] = [column, target_prefix + column_name]
2584                    elif target_prefix not in column:
2585                        keys.append(column)
2586
2587                column_styles = {
2588                    source_name: self.TABLE_DIFF_SOURCE_BLUE,
2589                    target_name: self.TABLE_DIFF_TARGET_GREEN,
2590                }
2591
2592                for column, [source_column, target_column] in columns.items():
2593                    # Create a table with the joined keys and comparison columns
2594                    column_table = row_diff.joined_sample[keys + [source_column, target_column]]
2595
2596                    # Filter to retain non identical-valued rows
2597                    column_table = column_table[
2598                        column_table.apply(
2599                            lambda row: not _cells_match(row[source_column], row[target_column]),
2600                            axis=1,
2601                        )
2602                    ]
2603
2604                    # Rename the column headers for readability
2605                    column_table = column_table.rename(
2606                        columns={
2607                            source_column: source_name,
2608                            target_column: target_name,
2609                        }
2610                    )
2611
2612                    table = Table(show_header=True)
2613                    for column_name in column_table.columns:
2614                        style = column_styles.get(column_name, "")
2615                        table.add_column(column_name, style=style, header_style=style)
2616
2617                    for _, row in column_table.iterrows():
2618                        table.add_row(
2619                            *[
2620                                str(
2621                                    round(cell, row_diff.decimals)
2622                                    if isinstance(cell, float)
2623                                    else cell
2624                                )
2625                                for cell in row
2626                            ]
2627                        )
2628
2629                    self.console.print(
2630                        f"Column: [underline][bold cyan]{column}[/bold cyan][/underline]",
2631                        table,
2632                        end="\n",
2633                    )
2634
2635            else:
2636                self.console.print("  All joined rows match")
2637
2638            if row_diff.s_sample.shape[0] > 0:
2639                self.console.print(f"\n[b][yellow]{source_name} ONLY[/yellow] sample rows:[/b]")
2640                self.console.print(row_diff.s_sample.to_string(index=False), end="\n\n")
2641
2642            if row_diff.t_sample.shape[0] > 0:
2643                self.console.print(f"\n[b][green]{target_name} ONLY[/green] sample rows:[/b]")
2644                self.console.print(row_diff.t_sample.to_string(index=False), end="\n\n")
2645
2646    def show_table_diff(
2647        self,
2648        table_diffs: t.List[TableDiff],
2649        show_sample: bool = True,
2650        skip_grain_check: bool = False,
2651        temp_schema: t.Optional[str] = None,
2652    ) -> None:
2653        """
2654        Display the table diff between all mismatched tables.
2655        """
2656        if len(table_diffs) > 1:
2657            mismatched_tables = []
2658            fully_matched = []
2659            for table_diff in table_diffs:
2660                if (
2661                    table_diff.schema_diff().source_schema == table_diff.schema_diff().target_schema
2662                ) and (
2663                    table_diff.row_diff(
2664                        temp_schema=temp_schema, skip_grain_check=skip_grain_check
2665                    ).full_match_pct
2666                    == 100
2667                ):
2668                    fully_matched.append(table_diff)
2669                else:
2670                    mismatched_tables.append(table_diff)
2671            table_diffs = mismatched_tables if mismatched_tables else []
2672            if fully_matched:
2673                m_tree = Tree("\n[b]Identical Tables")
2674                for m in fully_matched:
2675                    m_tree.add(
2676                        f"[{self.TABLE_DIFF_SOURCE_BLUE}]{m.source}[/{self.TABLE_DIFF_SOURCE_BLUE}] - [{self.TABLE_DIFF_TARGET_GREEN}]{m.target}[/{self.TABLE_DIFF_TARGET_GREEN}]"
2677                    )
2678                self._print(m_tree)
2679
2680            if mismatched_tables:
2681                m_tree = Tree("\n[b]Mismatched Tables")
2682                for m in mismatched_tables:
2683                    m_tree.add(
2684                        f"[{self.TABLE_DIFF_SOURCE_BLUE}]{m.source}[/{self.TABLE_DIFF_SOURCE_BLUE}] - [{self.TABLE_DIFF_TARGET_GREEN}]{m.target}[/{self.TABLE_DIFF_TARGET_GREEN}]"
2685                    )
2686                self._print(m_tree)
2687
2688        for table_diff in table_diffs:
2689            self.show_table_diff_summary(table_diff)
2690            self.show_schema_diff(table_diff.schema_diff())
2691            self.show_row_diff(
2692                table_diff.row_diff(temp_schema=temp_schema, skip_grain_check=skip_grain_check),
2693                show_sample=show_sample,
2694                skip_grain_check=skip_grain_check,
2695            )
2696
2697    def print_environments(self, environments_summary: t.List[EnvironmentSummary]) -> None:
2698        """Prints all environment names along with expiry datetime."""
2699        output = [
2700            f"{summary.name} - {time_like_to_str(summary.expiration_ts)}"
2701            if summary.expiration_ts
2702            else f"{summary.name} - No Expiry"
2703            for summary in environments_summary
2704        ]
2705        output_str = "\n".join([str(len(output)), *output])
2706        self.log_status_update(f"Number of SQLMesh environments are: {output_str}")
2707
2708    def show_intervals(self, snapshot_intervals: t.Dict[Snapshot, SnapshotIntervals]) -> None:
2709        complete = Tree(f"[b]Complete Intervals[/b]")
2710        incomplete = Tree(f"[b]Missing Intervals[/b]")
2711
2712        for snapshot, intervals in sorted(snapshot_intervals.items(), key=lambda s: s[0].node.name):
2713            if intervals.intervals:
2714                incomplete.add(
2715                    f"{snapshot.node.name}: [{intervals.format_intervals(snapshot.node.interval_unit)}]"
2716                )
2717            else:
2718                complete.add(snapshot.node.name)
2719
2720        if complete.children:
2721            self._print(complete)
2722
2723        if incomplete.children:
2724            self._print(incomplete)
2725
2726    def print_connection_config(self, config: ConnectionConfig, title: str = "Connection") -> None:
2727        tree = Tree(f"[b]{title}:[/b]")
2728        tree.add(f"Type: [bold cyan]{config.type_}[/bold cyan]")
2729        tree.add(f"Catalog: [bold cyan]{config.get_catalog()}[/bold cyan]")
2730
2731        try:
2732            engine_adapter_type = config._engine_adapter
2733            tree.add(f"Dialect: [bold cyan]{engine_adapter_type.DIALECT}[/bold cyan]")
2734        except NotImplementedError:
2735            # not all ConnectionConfig's have an engine adapter associated. The CloudConnectionConfig has a HTTP client instead
2736            pass
2737
2738        self._print(tree)
2739
2740    def _get_snapshot_change_category(
2741        self,
2742        snapshot: Snapshot,
2743        plan_builder: PlanBuilder,
2744        auto_apply: bool,
2745        default_catalog: t.Optional[str],
2746    ) -> None:
2747        choices = self._snapshot_change_choices(
2748            snapshot, plan_builder.environment_naming_info, default_catalog
2749        )
2750        response = self._prompt(
2751            "\n".join([f"[{i + 1}] {choice}" for i, choice in enumerate(choices.values())]),
2752            show_choices=False,
2753            choices=[f"{i + 1}" for i in range(len(choices))],
2754        )
2755        choice = list(choices)[int(response) - 1]
2756        plan_builder.set_choice(snapshot, choice)
2757
2758    def _snapshot_change_choices(
2759        self,
2760        snapshot: Snapshot,
2761        environment_naming_info: EnvironmentNamingInfo,
2762        default_catalog: t.Optional[str],
2763        use_rich_formatting: bool = True,
2764    ) -> t.Dict[SnapshotChangeCategory, str]:
2765        direct = snapshot.display_name(
2766            environment_naming_info,
2767            default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
2768            dialect=self.dialect,
2769        )
2770        if use_rich_formatting:
2771            direct = f"[direct]{direct}[/direct]"
2772        indirect = "indirectly modified children"
2773        if use_rich_formatting:
2774            indirect = f"[indirect]{indirect}[/indirect]"
2775        if snapshot.is_view:
2776            choices = {
2777                SnapshotChangeCategory.BREAKING: f"Update {direct} and backfill {indirect}",
2778                SnapshotChangeCategory.NON_BREAKING: f"Update {direct} but don't backfill {indirect}",
2779            }
2780        elif snapshot.is_symbolic:
2781            choices = {
2782                SnapshotChangeCategory.BREAKING: f"Backfill {indirect}",
2783                SnapshotChangeCategory.NON_BREAKING: f"Don't backfill {indirect}",
2784            }
2785        else:
2786            choices = {
2787                SnapshotChangeCategory.BREAKING: f"Backfill {direct} and {indirect}",
2788                SnapshotChangeCategory.NON_BREAKING: f"Backfill {direct} but not {indirect}",
2789            }
2790        labeled_choices = {
2791            k: f"[{SNAPSHOT_CHANGE_CATEGORY_STR[k]}] {v}" for k, v in choices.items()
2792        }
2793        return labeled_choices
2794
2795    def show_linter_violations(
2796        self, violations: t.List[RuleViolation], model: Model, is_error: bool = False
2797    ) -> None:
2798        severity = "errors" if is_error else "warnings"
2799
2800        # Sort violations by line, then alphabetically the name of the violation
2801        # Violations with no range go first
2802        sorted_violations = sorted(
2803            violations,
2804            key=lambda v: (
2805                v.violation_range.start.line if v.violation_range else -1,
2806                v.rule.name.lower(),
2807            ),
2808        )
2809        violations_text = [
2810            (
2811                f" - Line {v.violation_range.start.line + 1}: {v.rule.name} - {v.violation_msg}"
2812                if v.violation_range
2813                else f" - {v.rule.name}: {v.violation_msg}"
2814            )
2815            for v in sorted_violations
2816        ]
2817        violations_msg = "\n".join(violations_text)
2818        msg = f"Linter {severity} for {model._path}:\n{violations_msg}"
2819
2820        if is_error:
2821            self.log_error(msg)
2822        else:
2823            self.log_warning(msg)
2824
2825    def _log_test_details(
2826        self, result: ModelTextTestResult, unittest_char_separator: bool = True
2827    ) -> None:
2828        """
2829        This is a helper method that encapsulates the logic for logging the relevant unittest for the result.
2830        The top level method (`log_test_results`) reuses `_log_test_details` differently based on the console.
2831
2832        Args:
2833            result: The unittest test result that contains metrics like num success, fails, ect.
2834        """
2835        if result.wasSuccessful():
2836            self._print("\n", end="")
2837            return
2838
2839        if unittest_char_separator:
2840            self._print(f"\n{unittest.TextTestResult.separator1}\n\n", end="")
2841
2842        for (test_case, failure), test_failure_tables in zip_longest(  # type: ignore
2843            result.failures, result.failure_tables
2844        ):
2845            self._print(unittest.TextTestResult.separator2)
2846            self._print(f"FAIL: {test_case}")
2847
2848            if test_description := test_case.shortDescription():
2849                self._print(test_description)
2850            self._print(f"{unittest.TextTestResult.separator2}")
2851
2852            if not test_failure_tables:
2853                self._print(failure)
2854            else:
2855                for failure_table in test_failure_tables:
2856                    self._print(failure_table)
2857                    self._print("\n", end="")
2858
2859        for test_case, error in result.errors:
2860            self._print(unittest.TextTestResult.separator2)
2861            self._print(f"ERROR: {test_case}")
2862            self._print(f"{unittest.TextTestResult.separator2}")
2863            self._print(error)

A rich based implementation of the console.

TerminalConsole( console: Optional[rich.console.Console] = None, verbosity: sqlmesh.utils.Verbosity = <Verbosity.DEFAULT: 0>, dialect: Union[str, sqlglot.dialects.dialect.Dialect, Type[sqlglot.dialects.dialect.Dialect], NoneType] = None, ignore_warnings: bool = False, **kwargs: Any)
933    def __init__(
934        self,
935        console: t.Optional[RichConsole] = None,
936        verbosity: Verbosity = Verbosity.DEFAULT,
937        dialect: DialectType = None,
938        ignore_warnings: bool = False,
939        **kwargs: t.Any,
940    ) -> None:
941        self.console: RichConsole = console or srich.console
942
943        self.evaluation_progress_live: t.Optional[Live] = None
944        self.evaluation_total_progress: t.Optional[Progress] = None
945        self.evaluation_total_task: t.Optional[TaskID] = None
946        self.evaluation_model_progress: t.Optional[Progress] = None
947        self.evaluation_model_tasks: t.Dict[str, TaskID] = {}
948        self.evaluation_model_batch_sizes: t.Dict[Snapshot, int] = {}
949        self.evaluation_column_widths: t.Dict[str, int] = {}
950
951        # Put in temporary values that are replaced when evaluating
952        self.environment_naming_info = EnvironmentNamingInfo()
953        self.default_catalog: t.Optional[str] = None
954
955        self.creation_progress: t.Optional[Progress] = None
956        self.creation_column_widths: t.Dict[str, int] = {}
957        self.creation_task: t.Optional[TaskID] = None
958
959        self.promotion_progress: t.Optional[Progress] = None
960        self.promotion_column_widths: t.Dict[str, int] = {}
961        self.promotion_task: t.Optional[TaskID] = None
962
963        self.migration_progress: t.Optional[Progress] = None
964        self.migration_task: t.Optional[TaskID] = None
965
966        self.env_migration_progress: t.Optional[Progress] = None
967        self.env_migration_task: t.Optional[TaskID] = None
968
969        self.loading_status: t.Dict[uuid.UUID, Status] = {}
970
971        self.state_export_progress: t.Optional[Progress] = None
972        self.state_export_version_task: t.Optional[TaskID] = None
973        self.state_export_snapshot_task: t.Optional[TaskID] = None
974        self.state_export_environment_task: t.Optional[TaskID] = None
975
976        self.state_import_progress: t.Optional[Progress] = None
977        self.state_import_version_task: t.Optional[TaskID] = None
978        self.state_import_snapshot_task: t.Optional[TaskID] = None
979        self.state_import_environment_task: t.Optional[TaskID] = None
980
981        self.table_diff_progress: t.Optional[Progress] = None
982        self.table_diff_model_progress: t.Optional[Progress] = None
983        self.table_diff_model_tasks: t.Dict[str, TaskID] = {}
984        self.table_diff_progress_live: t.Optional[Live] = None
985
986        self.signal_progress_logged = False
987        self.signal_status_tree: t.Optional[Tree] = None
988
989        self.verbosity = verbosity
990        self.dialect = dialect
991        self.ignore_warnings = ignore_warnings
TABLE_DIFF_SOURCE_BLUE = '#0248ff'
TABLE_DIFF_TARGET_GREEN = 'green'
AUDIT_PASS_MARK = '✔'
GREEN_AUDIT_PASS_MARK = '[green]✔[/green]'
AUDIT_FAIL_MARK = '❌'
AUDIT_PADDING = 0
CHECK_MARK = '✔ '
console: rich.console.Console
evaluation_progress_live: Optional[rich.live.Live]
evaluation_total_progress: Optional[rich.progress.Progress]
evaluation_total_task: Optional[rich.progress.TaskID]
evaluation_model_progress: Optional[rich.progress.Progress]
evaluation_model_tasks: Dict[str, rich.progress.TaskID]
evaluation_model_batch_sizes: Dict[sqlmesh.core.snapshot.definition.Snapshot, int]
evaluation_column_widths: Dict[str, int]
environment_naming_info
default_catalog: Optional[str]
creation_progress: Optional[rich.progress.Progress]
creation_column_widths: Dict[str, int]
creation_task: Optional[rich.progress.TaskID]
promotion_progress: Optional[rich.progress.Progress]
promotion_column_widths: Dict[str, int]
promotion_task: Optional[rich.progress.TaskID]
migration_progress: Optional[rich.progress.Progress]
migration_task: Optional[rich.progress.TaskID]
env_migration_progress: Optional[rich.progress.Progress]
env_migration_task: Optional[rich.progress.TaskID]
loading_status: Dict[uuid.UUID, rich.status.Status]
state_export_progress: Optional[rich.progress.Progress]
state_export_version_task: Optional[rich.progress.TaskID]
state_export_snapshot_task: Optional[rich.progress.TaskID]
state_export_environment_task: Optional[rich.progress.TaskID]
state_import_progress: Optional[rich.progress.Progress]
state_import_version_task: Optional[rich.progress.TaskID]
state_import_snapshot_task: Optional[rich.progress.TaskID]
state_import_environment_task: Optional[rich.progress.TaskID]
table_diff_progress: Optional[rich.progress.Progress]
table_diff_model_progress: Optional[rich.progress.Progress]
table_diff_model_tasks: Dict[str, rich.progress.TaskID]
table_diff_progress_live: Optional[rich.live.Live]
signal_progress_logged
signal_status_tree: Optional[rich.tree.Tree]
verbosity
dialect
ignore_warnings
def start_plan_evaluation(self, plan: sqlmesh.core.plan.definition.EvaluatablePlan) -> None:
1016    def start_plan_evaluation(self, plan: EvaluatablePlan) -> None:
1017        pass

Indicates that a new evaluation has begun.

def stop_plan_evaluation(self) -> None:
1019    def stop_plan_evaluation(self) -> None:
1020        pass

Indicates that the evaluation has ended.

def start_evaluation_progress( self, batched_intervals: Dict[sqlmesh.core.snapshot.definition.Snapshot, List[Tuple[int, int]]], environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str], audit_only: bool = False) -> None:
1022    def start_evaluation_progress(
1023        self,
1024        batched_intervals: t.Dict[Snapshot, Intervals],
1025        environment_naming_info: EnvironmentNamingInfo,
1026        default_catalog: t.Optional[str],
1027        audit_only: bool = False,
1028    ) -> None:
1029        """Indicates that a new snapshot evaluation/auditing progress has begun."""
1030        # Add a newline to separate signal checking from evaluation
1031        if self.signal_progress_logged:
1032            self._print("")
1033
1034        if not self.evaluation_progress_live:
1035            self.evaluation_total_progress = make_progress_bar(
1036                "Executing model batches" if not audit_only else "Auditing models", self.console
1037            )
1038
1039            self.evaluation_model_progress = Progress(
1040                TextColumn("{task.fields[view_name]}", justify="right"),
1041                SpinnerColumn(spinner_name="simpleDots"),
1042                console=self.console,
1043            )
1044
1045            progress_table = Table.grid()
1046            progress_table.add_row(self.evaluation_total_progress)
1047            progress_table.add_row(self.evaluation_model_progress)
1048
1049            self.evaluation_progress_live = Live(
1050                progress_table, console=self.console, refresh_per_second=10
1051            )
1052            self.evaluation_progress_live.start()
1053
1054            batch_sizes = {
1055                snapshot: len(intervals) for snapshot, intervals in batched_intervals.items()
1056            }
1057            message = "Executing" if not audit_only else "Auditing"
1058            self.evaluation_total_task = self.evaluation_total_progress.add_task(
1059                f"{message} models...", total=sum(batch_sizes.values())
1060            )
1061
1062            # determine column widths
1063            self.evaluation_column_widths["annotation"] = (
1064                _calculate_annotation_str_len(
1065                    batched_intervals, self.AUDIT_PADDING, len(" (123.4m rows, 123.4 KiB)")
1066                )
1067                + 3  # brackets and opening escape backslash
1068            )
1069            self.evaluation_column_widths["name"] = max(
1070                len(
1071                    snapshot.display_name(
1072                        environment_naming_info,
1073                        default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1074                        dialect=self.dialect,
1075                    )
1076                )
1077                for snapshot in batched_intervals
1078            )
1079            largest_batch_size = max(batch_sizes.values())
1080            self.evaluation_column_widths["batch"] = len(str(largest_batch_size)) * 2 + 3  # [X/X]
1081            self.evaluation_column_widths["duration"] = 8
1082
1083            self.evaluation_model_batch_sizes = batch_sizes
1084            self.environment_naming_info = environment_naming_info
1085            self.default_catalog = default_catalog

Indicates that a new snapshot evaluation/auditing progress has begun.

def start_snapshot_evaluation_progress( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, audit_only: bool = False) -> None:
1087    def start_snapshot_evaluation_progress(
1088        self, snapshot: Snapshot, audit_only: bool = False
1089    ) -> None:
1090        if self.evaluation_model_progress and snapshot.name not in self.evaluation_model_tasks:
1091            display_name = snapshot.display_name(
1092                self.environment_naming_info,
1093                self.default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1094                dialect=self.dialect,
1095            )
1096            self.evaluation_model_tasks[snapshot.name] = self.evaluation_model_progress.add_task(
1097                f"{'Evaluating' if not audit_only else 'Auditing'} {display_name}...",
1098                view_name=display_name,
1099                total=self.evaluation_model_batch_sizes[snapshot],
1100            )

Starts the snapshot evaluation progress.

def update_snapshot_evaluation_progress( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, interval: Tuple[int, int], batch_idx: int, duration_ms: Optional[int], num_audits_passed: int, num_audits_failed: int, audit_only: bool = False, execution_stats: Optional[sqlmesh.core.snapshot.execution_tracker.QueryExecutionStats] = None, auto_restatement_triggers: Optional[List[sqlmesh.core.snapshot.definition.SnapshotId]] = None) -> None:
1102    def update_snapshot_evaluation_progress(
1103        self,
1104        snapshot: Snapshot,
1105        interval: Interval,
1106        batch_idx: int,
1107        duration_ms: t.Optional[int],
1108        num_audits_passed: int,
1109        num_audits_failed: int,
1110        audit_only: bool = False,
1111        execution_stats: t.Optional[QueryExecutionStats] = None,
1112        auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None,
1113    ) -> None:
1114        """Update the snapshot evaluation progress."""
1115        if (
1116            self.evaluation_total_progress
1117            and self.evaluation_model_progress
1118            and self.evaluation_progress_live
1119        ):
1120            total_batches = self.evaluation_model_batch_sizes[snapshot]
1121            batch_num = str(batch_idx + 1).rjust(len(str(total_batches)))
1122            batch = f"[{batch_num}/{total_batches}]".ljust(self.evaluation_column_widths["batch"])
1123
1124            if duration_ms:
1125                display_name = snapshot.display_name(
1126                    self.environment_naming_info,
1127                    self.default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1128                    dialect=self.dialect,
1129                ).ljust(self.evaluation_column_widths["name"])
1130
1131                annotation = _create_evaluation_model_annotation(
1132                    snapshot, _format_evaluation_model_interval(snapshot, interval), execution_stats
1133                )
1134                audits_str = ""
1135                if num_audits_passed:
1136                    audits_str += f" {self.AUDIT_PASS_MARK}{num_audits_passed}"
1137                if num_audits_failed:
1138                    audits_str += f" {self.AUDIT_FAIL_MARK}{num_audits_failed}"
1139                audits_str = f", audits{audits_str}" if audits_str else ""
1140                annotation_len = self.evaluation_column_widths["annotation"]
1141                # don't adjust the annotation_len if we're using AUDIT_PADDING
1142                annotation = f"\\[{annotation + audits_str}]".ljust(
1143                    annotation_len - 1
1144                    if num_audits_failed and self.AUDIT_PADDING == 0
1145                    else annotation_len
1146                )
1147
1148                duration = f"{(duration_ms / 1000.0):.2f}s".ljust(
1149                    self.evaluation_column_widths["duration"]
1150                )
1151
1152                msg = f"{f'{batch} ' if not audit_only else ''}{display_name}   {annotation}   {duration}".replace(
1153                    self.AUDIT_PASS_MARK, self.GREEN_AUDIT_PASS_MARK
1154                )
1155
1156                self.evaluation_progress_live.console.print(msg)
1157
1158            self.evaluation_total_progress.update(
1159                self.evaluation_total_task or TaskID(0), refresh=True, advance=1
1160            )
1161
1162            model_task_id = self.evaluation_model_tasks[snapshot.name]
1163            self.evaluation_model_progress.update(model_task_id, refresh=True, advance=1)
1164            if (
1165                self.evaluation_model_progress._tasks[model_task_id].completed >= total_batches
1166                or audit_only
1167            ):
1168                self.evaluation_model_progress.remove_task(model_task_id)

Update the snapshot evaluation progress.

def stop_evaluation_progress(self, success: bool = True) -> None:
1170    def stop_evaluation_progress(self, success: bool = True) -> None:
1171        """Stop the snapshot evaluation progress."""
1172        if self.evaluation_progress_live:
1173            self.evaluation_progress_live.stop()
1174            if success:
1175                self.log_success(f"{self.CHECK_MARK}Model batches executed")
1176
1177        self.evaluation_progress_live = None
1178        self.evaluation_total_progress = None
1179        self.evaluation_total_task = None
1180        self.evaluation_model_progress = None
1181        self.evaluation_model_tasks = {}
1182        self.evaluation_model_batch_sizes = {}
1183        self.evaluation_column_widths = {}
1184        self.environment_naming_info = EnvironmentNamingInfo()
1185        self.default_catalog = None

Stop the snapshot evaluation progress.

def start_signal_progress( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, default_catalog: Optional[str], environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo) -> None:
1187    def start_signal_progress(
1188        self,
1189        snapshot: Snapshot,
1190        default_catalog: t.Optional[str],
1191        environment_naming_info: EnvironmentNamingInfo,
1192    ) -> None:
1193        """Indicates that signal checking has begun for a snapshot."""
1194        display_name = snapshot.display_name(
1195            environment_naming_info,
1196            default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1197            dialect=self.dialect,
1198        )
1199        self.signal_status_tree = Tree(f"Checking signals for {display_name}")

Indicates that signal checking has begun for a snapshot.

def update_signal_progress( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, signal_name: str, signal_idx: int, total_signals: int, ready_intervals: List[Tuple[int, int]], check_intervals: List[Tuple[int, int]], duration: float) -> None:
1201    def update_signal_progress(
1202        self,
1203        snapshot: Snapshot,
1204        signal_name: str,
1205        signal_idx: int,
1206        total_signals: int,
1207        ready_intervals: Intervals,
1208        check_intervals: Intervals,
1209        duration: float,
1210    ) -> None:
1211        """Updates the signal checking progress."""
1212        tree = Tree(f"[{signal_idx + 1}/{total_signals}] {signal_name} {duration:.2f}s")
1213
1214        formatted_check_intervals = [_format_signal_interval(snapshot, i) for i in check_intervals]
1215        formatted_ready_intervals = [_format_signal_interval(snapshot, i) for i in ready_intervals]
1216
1217        if not formatted_check_intervals:
1218            formatted_check_intervals = ["no intervals"]
1219        if not formatted_ready_intervals:
1220            formatted_ready_intervals = ["no intervals"]
1221
1222        # Color coding to help detect partial interval ranges quickly
1223        if ready_intervals == check_intervals:
1224            msg = "All ready"
1225            color = "green"
1226        elif ready_intervals:
1227            msg = "Some ready"
1228            color = "yellow"
1229        else:
1230            msg = "None ready"
1231            color = "red"
1232
1233        if self.verbosity < Verbosity.VERY_VERBOSE:
1234            num_check_intervals = len(formatted_check_intervals)
1235            if num_check_intervals > 3:
1236                formatted_check_intervals = formatted_check_intervals[:3]
1237                formatted_check_intervals.append(f"... and {num_check_intervals - 3} more")
1238
1239            num_ready_intervals = len(formatted_ready_intervals)
1240            if num_ready_intervals > 3:
1241                formatted_ready_intervals = formatted_ready_intervals[:3]
1242                formatted_ready_intervals.append(f"... and {num_ready_intervals - 3} more")
1243
1244            check = ", ".join(formatted_check_intervals)
1245            tree.add(f"Check: {check}")
1246
1247            ready = ", ".join(formatted_ready_intervals)
1248            tree.add(f"[{color}]{msg}: {ready}[/{color}]")
1249        else:
1250            check_tree = Tree("Check")
1251            tree.add(check_tree)
1252            for interval in formatted_check_intervals:
1253                check_tree.add(interval)
1254
1255            ready_tree = Tree(f"[{color}]{msg}[/{color}]")
1256            tree.add(ready_tree)
1257            for interval in formatted_ready_intervals:
1258                ready_tree.add(f"[{color}]{interval}[/{color}]")
1259
1260        if self.signal_status_tree is not None:
1261            self.signal_status_tree.add(tree)

Updates the signal checking progress.

def stop_signal_progress(self) -> None:
1263    def stop_signal_progress(self) -> None:
1264        """Indicates that signal checking has completed for a snapshot."""
1265        if self.signal_status_tree is not None:
1266            self._print(self.signal_status_tree)
1267            self.signal_status_tree = None
1268            self.signal_progress_logged = True

Indicates that signal checking has completed for a snapshot.

def start_creation_progress( self, snapshots: List[sqlmesh.core.snapshot.definition.Snapshot], environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str]) -> None:
1270    def start_creation_progress(
1271        self,
1272        snapshots: t.List[Snapshot],
1273        environment_naming_info: EnvironmentNamingInfo,
1274        default_catalog: t.Optional[str],
1275    ) -> None:
1276        """Indicates that a new creation progress has begun."""
1277        if self.creation_progress is None:
1278            self.creation_progress = make_progress_bar("Updating physical layer", self.console)
1279
1280            self._print("")
1281            self.creation_progress.start()
1282            self.creation_task = self.creation_progress.add_task(
1283                "Updating physical layer...",
1284                total=len(snapshots),
1285            )
1286
1287            # determine name column widths if we're printing name
1288            if self.verbosity >= Verbosity.VERBOSE:
1289                self.creation_column_widths["name"] = max(
1290                    len(
1291                        snapshot.display_name(
1292                            environment_naming_info,
1293                            default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1294                            dialect=self.dialect,
1295                        )
1296                    )
1297                    for snapshot in snapshots
1298                )
1299
1300            self.environment_naming_info = environment_naming_info
1301            self.default_catalog = default_catalog

Indicates that a new creation progress has begun.

def update_creation_progress( self, snapshot: Union[sqlmesh.core.snapshot.definition.SnapshotTableInfo, sqlmesh.core.snapshot.definition.Snapshot]) -> None:
1303    def update_creation_progress(self, snapshot: SnapshotInfoLike) -> None:
1304        """Update the snapshot creation progress."""
1305        if self.creation_progress is not None and self.creation_task is not None:
1306            if self.verbosity >= Verbosity.VERBOSE:
1307                msg = snapshot.display_name(
1308                    self.environment_naming_info,
1309                    self.default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1310                    dialect=self.dialect,
1311                ).ljust(self.creation_column_widths["name"])
1312                self.creation_progress.live.console.print(msg + "  [green]created[/green]")
1313            self.creation_progress.update(self.creation_task, refresh=True, advance=1)

Update the snapshot creation progress.

def stop_creation_progress(self, success: bool = True) -> None:
1315    def stop_creation_progress(self, success: bool = True) -> None:
1316        """Stop the snapshot creation progress."""
1317        self.creation_task = None
1318        if self.creation_progress is not None:
1319            self.creation_progress.stop()
1320            self.creation_progress = None
1321            if success:
1322                self.log_success(f"\n{self.CHECK_MARK}Physical layer updated")
1323
1324        self.environment_naming_info = EnvironmentNamingInfo()
1325        self.default_catalog = None
1326        self.creation_column_widths = {}

Stop the snapshot creation progress.

def start_cleanup(self, ignore_ttl: bool) -> bool:
1328    def start_cleanup(self, ignore_ttl: bool) -> bool:
1329        if ignore_ttl:
1330            self._print(
1331                "Are you sure you want to delete all snapshots that are not referenced in any environment?"
1332            )
1333            self._print(
1334                "Note that this may cause a race condition if there are any concurrently running plans."
1335            )
1336            self._print(
1337                "It may also confuse users who were expecting to be able to rollback changes in their development environments."
1338            )
1339            if not self._confirm("Proceed?"):
1340                self.log_error("Cleanup aborted")
1341                return False
1342        return True

Start a janitor / snapshot cleanup run.

Arguments:
  • ignore_ttl: Indicates that the user wants to ignore the snapshot TTL and clean up everything not promoted to an environment
Returns:

Whether or not the cleanup run should proceed

def update_cleanup_progress(self, object_name: str) -> None:
1344    def update_cleanup_progress(self, object_name: str) -> None:
1345        """Update the snapshot cleanup progress."""
1346        self._print(f"Deleted object {object_name}")

Update the snapshot cleanup progress.

def stop_cleanup(self, success: bool = False) -> None:
1348    def stop_cleanup(self, success: bool = False) -> None:
1349        if success:
1350            self.log_success("Cleanup complete.")
1351        else:
1352            self.log_error("Cleanup failed!")

Indicates the janitor / snapshot cleanup run has ended

Arguments:
  • success: Whether or not the cleanup completed successfully
def start_destroy( self, schemas_to_delete: Optional[Set[str]] = None, views_to_delete: Optional[Set[str]] = None, tables_to_delete: Optional[Set[str]] = None) -> bool:
1354    def start_destroy(
1355        self,
1356        schemas_to_delete: t.Optional[t.Set[str]] = None,
1357        views_to_delete: t.Optional[t.Set[str]] = None,
1358        tables_to_delete: t.Optional[t.Set[str]] = None,
1359    ) -> bool:
1360        self.log_warning(
1361            "This will permanently delete all engine-managed objects, state tables and SQLMesh cache.\n"
1362            "The operation may disrupt any currently running or scheduled plans.\n"
1363        )
1364
1365        if schemas_to_delete or views_to_delete or tables_to_delete:
1366            if schemas_to_delete:
1367                self.log_error("Schemas to be deleted:")
1368                for schema in sorted(schemas_to_delete):
1369                    self.log_error(f"  • {schema}")
1370
1371            if views_to_delete:
1372                self.log_error("\nEnvironment views to be deleted:")
1373                for view in sorted(views_to_delete):
1374                    self.log_error(f"  • {view}")
1375
1376            if tables_to_delete:
1377                self.log_error("\nSnapshot tables to be deleted:")
1378                for table in sorted(tables_to_delete):
1379                    self.log_error(f"  • {table}")
1380
1381            self.log_error(
1382                "\nThis action will DELETE ALL the above resources managed by SQLMesh AND\n"
1383                "potentially external resources created by other tools in these schemas.\n"
1384            )
1385
1386        if not self._confirm("Are you ABSOLUTELY SURE you want to proceed with deletion?"):
1387            self.log_error("Destroy operation cancelled.")
1388            return False
1389        return True

Start a destroy operation.

Arguments:
  • schemas_to_delete: Set of schemas that will be deleted
  • views_to_delete: Set of views that will be deleted
  • tables_to_delete: Set of tables that will be deleted
Returns:

Whether or not the destroy operation should proceed

def stop_destroy(self, success: bool = False) -> None:
1391    def stop_destroy(self, success: bool = False) -> None:
1392        if success:
1393            self.log_success("Destroy completed successfully.")
1394        else:
1395            self.log_error("Destroy failed!")

Indicates the destroy operation has ended

Arguments:
  • success: Whether or not the cleanup completed successfully
def start_promotion_progress( self, snapshots: List[sqlmesh.core.snapshot.definition.SnapshotTableInfo], environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str]) -> None:
1397    def start_promotion_progress(
1398        self,
1399        snapshots: t.List[SnapshotTableInfo],
1400        environment_naming_info: EnvironmentNamingInfo,
1401        default_catalog: t.Optional[str],
1402    ) -> None:
1403        """Indicates that a new snapshot promotion progress has begun."""
1404        if snapshots and self.promotion_progress is None:
1405            self.promotion_progress = make_progress_bar(
1406                "Updating virtual layer ", self.console, justify="left"
1407            )
1408
1409            snapshots_with_virtual_views = [
1410                s for s in snapshots if s.is_model and not s.is_symbolic
1411            ]
1412            self.promotion_progress.start()
1413            self.promotion_task = self.promotion_progress.add_task(
1414                f"Virtually updating {environment_naming_info.name}...",
1415                total=len(snapshots_with_virtual_views),
1416            )
1417
1418            # determine name column widths if we're printing names
1419            if self.verbosity >= Verbosity.VERBOSE:
1420                self.promotion_column_widths["name"] = max(
1421                    len(
1422                        snapshot.display_name(
1423                            environment_naming_info,
1424                            default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1425                            dialect=self.dialect,
1426                        )
1427                    )
1428                    for snapshot in snapshots_with_virtual_views
1429                )
1430
1431            self.environment_naming_info = environment_naming_info
1432            self.default_catalog = default_catalog

Indicates that a new snapshot promotion progress has begun.

def update_promotion_progress( self, snapshot: Union[sqlmesh.core.snapshot.definition.SnapshotTableInfo, sqlmesh.core.snapshot.definition.Snapshot], promoted: bool) -> None:
1434    def update_promotion_progress(self, snapshot: SnapshotInfoLike, promoted: bool) -> None:
1435        """Update the snapshot promotion progress."""
1436        if (
1437            self.promotion_progress is not None
1438            and self.promotion_task is not None
1439            and snapshot.is_model
1440            and not snapshot.is_symbolic
1441        ):
1442            if self.verbosity >= Verbosity.VERBOSE:
1443                display_name = snapshot.display_name(
1444                    self.environment_naming_info,
1445                    self.default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
1446                    dialect=self.dialect,
1447                ).ljust(self.promotion_column_widths["name"])
1448                action_str = ""
1449                if promoted:
1450                    action_str = (
1451                        "[yellow]updated[/yellow]"
1452                        if snapshot.previous_version
1453                        else "[green]created[/green]"
1454                    )
1455                action_str = action_str or "[red]dropped[/red]"
1456                self.promotion_progress.live.console.print(f"{display_name}  {action_str}")
1457            self.promotion_progress.update(self.promotion_task, refresh=True, advance=1)

Update the snapshot promotion progress.

def stop_promotion_progress(self, success: bool = True) -> None:
1459    def stop_promotion_progress(self, success: bool = True) -> None:
1460        """Stop the snapshot promotion progress."""
1461        self.promotion_task = None
1462        if self.promotion_progress is not None:
1463            self.promotion_progress.stop()
1464            self.promotion_progress = None
1465            if success:
1466                self.log_success(f"\n{self.CHECK_MARK}Virtual layer updated")
1467
1468        self.environment_naming_info = EnvironmentNamingInfo()
1469        self.default_catalog = None
1470        self.promotion_column_widths = {}

Stop the snapshot promotion progress.

def start_snapshot_migration_progress(self, total_tasks: int) -> None:
1472    def start_snapshot_migration_progress(self, total_tasks: int) -> None:
1473        """Indicates that a new snapshot migration progress has begun."""
1474        if self.migration_progress is None:
1475            self.migration_progress = make_progress_bar("Migrating snapshots", self.console)
1476
1477            self.migration_progress.start()
1478            self.migration_task = self.migration_progress.add_task(
1479                "Migrating snapshots...",
1480                total=total_tasks,
1481            )

Indicates that a new snapshot migration progress has begun.

def update_snapshot_migration_progress(self, num_tasks: int) -> None:
1483    def update_snapshot_migration_progress(self, num_tasks: int) -> None:
1484        """Update the migration progress."""
1485        if self.migration_progress is not None and self.migration_task is not None:
1486            self.migration_progress.update(self.migration_task, refresh=True, advance=num_tasks)

Update the migration progress.

def log_migration_status(self, success: bool = True) -> None:
1488    def log_migration_status(self, success: bool = True) -> None:
1489        """Log the migration status."""
1490        if self.migration_progress is not None:
1491            self.migration_progress = None
1492            if success:
1493                self.log_success("Migration completed successfully")

Log the migration status.

def stop_snapshot_migration_progress(self, success: bool = True) -> None:
1495    def stop_snapshot_migration_progress(self, success: bool = True) -> None:
1496        """Stop the migration progress."""
1497        self.migration_task = None
1498        if self.migration_progress is not None:
1499            self.migration_progress.stop()
1500            if success:
1501                self.log_success("Snapshots migrated successfully")

Stop the migration progress.

def start_env_migration_progress(self, total_tasks: int) -> None:
1503    def start_env_migration_progress(self, total_tasks: int) -> None:
1504        """Indicates that a new environment migration has begun."""
1505        if self.env_migration_progress is None:
1506            self.env_migration_progress = make_progress_bar("Migrating environments", self.console)
1507            self.env_migration_progress.start()
1508            self.env_migration_task = self.env_migration_progress.add_task(
1509                "Migrating environments...",
1510                total=total_tasks,
1511            )

Indicates that a new environment migration has begun.

def update_env_migration_progress(self, num_tasks: int) -> None:
1513    def update_env_migration_progress(self, num_tasks: int) -> None:
1514        """Update the environment migration progress."""
1515        if self.env_migration_progress is not None and self.env_migration_task is not None:
1516            self.env_migration_progress.update(
1517                self.env_migration_task, refresh=True, advance=num_tasks
1518            )

Update the environment migration progress.

def stop_env_migration_progress(self, success: bool = True) -> None:
1520    def stop_env_migration_progress(self, success: bool = True) -> None:
1521        """Stop the environment migration progress."""
1522        self.env_migration_task = None
1523        if self.env_migration_progress is not None:
1524            self.env_migration_progress.stop()
1525            self.env_migration_progress = None
1526            if success:
1527                self.log_success("Environments migrated successfully")

Stop the environment migration progress.

def start_state_export( self, output_file: pathlib.Path, gateway: Optional[str] = None, state_connection_config: Optional[sqlmesh.core.config.connection.ConnectionConfig] = None, environment_names: Optional[List[str]] = None, local_only: bool = False, confirm: bool = True) -> bool:
1529    def start_state_export(
1530        self,
1531        output_file: Path,
1532        gateway: t.Optional[str] = None,
1533        state_connection_config: t.Optional[ConnectionConfig] = None,
1534        environment_names: t.Optional[t.List[str]] = None,
1535        local_only: bool = False,
1536        confirm: bool = True,
1537    ) -> bool:
1538        self.state_export_progress = None
1539
1540        if local_only:
1541            self.log_status_update(f"Exporting [b]local[/b] state to '{output_file.as_posix()}'\n")
1542            self.log_warning(
1543                "Local state exports just contain the model versions in your local context. Therefore, the resulting file cannot be imported."
1544            )
1545        else:
1546            self.log_status_update(
1547                f"Exporting state to '{output_file.as_posix()}' from the following connection:\n"
1548            )
1549            if gateway:
1550                self.log_status_update(f"[b]Gateway[/b]: [green]{gateway}[/green]")
1551            if state_connection_config:
1552                self.print_connection_config(state_connection_config, title="State Connection")
1553            if environment_names:
1554                heading = "Environments" if len(environment_names) > 1 else "Environment"
1555                self.log_status_update(
1556                    f"[b]{heading}[/b]: [yellow]{', '.join(environment_names)}[/yellow]"
1557                )
1558
1559        should_continue = True
1560        if confirm:
1561            should_continue = self._confirm("\nContinue?")
1562            self.log_status_update("")
1563
1564        if should_continue:
1565            self.state_export_progress = make_progress_bar("{task.description}", self.console)
1566            assert isinstance(self.state_export_progress, Progress)
1567
1568            self.state_export_version_task = self.state_export_progress.add_task(
1569                "Exporting versions", start=False
1570            )
1571            self.state_export_snapshot_task = self.state_export_progress.add_task(
1572                "Exporting snapshots", start=False
1573            )
1574            self.state_export_environment_task = self.state_export_progress.add_task(
1575                "Exporting environments", start=False
1576            )
1577
1578            self.state_export_progress.start()
1579
1580        return should_continue

State a state export

def update_state_export_progress( self, version_count: Optional[int] = None, versions_complete: bool = False, snapshot_count: Optional[int] = None, snapshots_complete: bool = False, environment_count: Optional[int] = None, environments_complete: bool = False) -> None:
1582    def update_state_export_progress(
1583        self,
1584        version_count: t.Optional[int] = None,
1585        versions_complete: bool = False,
1586        snapshot_count: t.Optional[int] = None,
1587        snapshots_complete: bool = False,
1588        environment_count: t.Optional[int] = None,
1589        environments_complete: bool = False,
1590    ) -> None:
1591        if self.state_export_progress:
1592            if self.state_export_version_task is not None:
1593                if version_count is not None:
1594                    self.state_export_progress.start_task(self.state_export_version_task)
1595                    self.state_export_progress.update(
1596                        self.state_export_version_task,
1597                        total=version_count,
1598                        completed=version_count,
1599                        refresh=True,
1600                    )
1601                if versions_complete:
1602                    self.state_export_progress.stop_task(self.state_export_version_task)
1603
1604            if self.state_export_snapshot_task is not None:
1605                if snapshot_count is not None:
1606                    self.state_export_progress.start_task(self.state_export_snapshot_task)
1607                    self.state_export_progress.update(
1608                        self.state_export_snapshot_task,
1609                        total=snapshot_count,
1610                        completed=snapshot_count,
1611                        refresh=True,
1612                    )
1613                if snapshots_complete:
1614                    self.state_export_progress.stop_task(self.state_export_snapshot_task)
1615
1616            if self.state_export_environment_task is not None:
1617                if environment_count is not None:
1618                    self.state_export_progress.start_task(self.state_export_environment_task)
1619                    self.state_export_progress.update(
1620                        self.state_export_environment_task,
1621                        total=environment_count,
1622                        completed=environment_count,
1623                        refresh=True,
1624                    )
1625                if environments_complete:
1626                    self.state_export_progress.stop_task(self.state_export_environment_task)

Update the state export progress

def stop_state_export(self, success: bool, output_file: pathlib.Path) -> None:
1628    def stop_state_export(self, success: bool, output_file: Path) -> None:
1629        if self.state_export_progress:
1630            self.state_export_progress.stop()
1631            self.state_export_progress = None
1632
1633            self.log_status_update("")
1634
1635            if success:
1636                self.log_success(f"State exported successfully to '{output_file.as_posix()}'")
1637            else:
1638                self.log_error("State export failed!")

Finish a state export

def start_state_import( self, input_file: pathlib.Path, gateway: str, state_connection_config: sqlmesh.core.config.connection.ConnectionConfig, clear: bool = False, confirm: bool = True) -> bool:
1640    def start_state_import(
1641        self,
1642        input_file: Path,
1643        gateway: str,
1644        state_connection_config: ConnectionConfig,
1645        clear: bool = False,
1646        confirm: bool = True,
1647    ) -> bool:
1648        self.log_status_update(
1649            f"Loading state from '{input_file.as_posix()}' into the following connection:\n"
1650        )
1651        self.log_status_update(f"[b]Gateway[/b]: [green]{gateway}[/green]")
1652        self.print_connection_config(state_connection_config, title="State Connection")
1653        self.log_status_update("")
1654
1655        if clear:
1656            self.log_warning(
1657                f"This [b]destructive[/b] operation will delete all existing state against the '{gateway}' gateway \n"
1658                f"and replace it with what's in the '{input_file.as_posix()}' file.\n"
1659            )
1660        else:
1661            self.log_warning(
1662                f"This operation will [b]merge[/b] the contents of the state file to the state located at the '{gateway}' gateway.\n"
1663                "Matching snapshots or environments will be replaced.\n"
1664                "Non-matching snapshots or environments will be ignored.\n"
1665            )
1666
1667        should_continue = True
1668        if confirm:
1669            should_continue = self._confirm("[red]Are you sure?[/red]")
1670            self.log_status_update("")
1671
1672        if should_continue:
1673            self.state_import_progress = make_progress_bar("{task.description}", self.console)
1674
1675            self.state_import_info = Tree("[bold]State File Information:")
1676
1677            self.state_import_version_task = self.state_import_progress.add_task(
1678                "Importing versions", start=False
1679            )
1680            self.state_import_snapshot_task = self.state_import_progress.add_task(
1681                "Importing snapshots", start=False
1682            )
1683            self.state_import_environment_task = self.state_import_progress.add_task(
1684                "Importing environments", start=False
1685            )
1686
1687            self.state_import_progress.start()
1688
1689        return should_continue

Start a state import

def update_state_import_progress( self, timestamp: Optional[str] = None, state_file_version: Optional[int] = None, versions: Optional[sqlmesh.core.state_sync.base.Versions] = None, snapshot_count: Optional[int] = None, snapshots_complete: bool = False, environment_count: Optional[int] = None, environments_complete: bool = False) -> None:
1691    def update_state_import_progress(
1692        self,
1693        timestamp: t.Optional[str] = None,
1694        state_file_version: t.Optional[int] = None,
1695        versions: t.Optional[Versions] = None,
1696        snapshot_count: t.Optional[int] = None,
1697        snapshots_complete: bool = False,
1698        environment_count: t.Optional[int] = None,
1699        environments_complete: bool = False,
1700    ) -> None:
1701        if self.state_import_progress:
1702            if self.state_import_info:
1703                if timestamp:
1704                    self.state_import_info.add(f"Creation Timestamp: {timestamp}")
1705                if state_file_version:
1706                    self.state_import_info.add(f"File Version: {state_file_version}")
1707                if versions:
1708                    self.state_import_info.add(f"SQLMesh version: {versions.sqlmesh_version}")
1709                    self.state_import_info.add(
1710                        f"SQLMesh migration version: {versions.schema_version}"
1711                    )
1712                    self.state_import_info.add(f"SQLGlot version: {versions.sqlglot_version}\n")
1713
1714                    self._print(self.state_import_info)
1715
1716                    version_count = len(versions.model_dump())
1717
1718                    if self.state_import_version_task is not None:
1719                        self.state_import_progress.start_task(self.state_import_version_task)
1720                        self.state_import_progress.update(
1721                            self.state_import_version_task,
1722                            total=version_count,
1723                            completed=version_count,
1724                        )
1725                        self.state_import_progress.stop_task(self.state_import_version_task)
1726
1727            if self.state_import_snapshot_task is not None:
1728                if snapshot_count is not None:
1729                    self.state_import_progress.start_task(self.state_import_snapshot_task)
1730                    self.state_import_progress.update(
1731                        self.state_import_snapshot_task,
1732                        completed=snapshot_count,
1733                        total=snapshot_count,
1734                        refresh=True,
1735                    )
1736
1737                if snapshots_complete:
1738                    self.state_import_progress.stop_task(self.state_import_snapshot_task)
1739
1740            if self.state_import_environment_task is not None:
1741                if environment_count is not None:
1742                    self.state_import_progress.start_task(self.state_import_environment_task)
1743                    self.state_import_progress.update(
1744                        self.state_import_environment_task,
1745                        completed=environment_count,
1746                        total=environment_count,
1747                        refresh=True,
1748                    )
1749
1750                if environments_complete:
1751                    self.state_import_progress.stop_task(self.state_import_environment_task)

Update the state import process

def stop_state_import(self, success: bool, input_file: pathlib.Path) -> None:
1753    def stop_state_import(self, success: bool, input_file: Path) -> None:
1754        if self.state_import_progress:
1755            self.state_import_progress.stop()
1756            self.state_import_progress = None
1757
1758            self.log_status_update("")
1759
1760            if success:
1761                self.log_success(f"State imported successfully from '{input_file.as_posix()}'")
1762            else:
1763                self.log_error("State import failed!")

Finish a state import

def show_environment_difference_summary( self, context_diff: sqlmesh.core.context_diff.ContextDiff, no_diff: bool = True) -> None:
1765    def show_environment_difference_summary(
1766        self,
1767        context_diff: ContextDiff,
1768        no_diff: bool = True,
1769    ) -> None:
1770        """Shows a summary of the environment differences.
1771
1772        Args:
1773            context_diff: The context diff to use to print the summary
1774            no_diff: Hide the actual environment statement differences.
1775        """
1776        if context_diff.is_new_environment:
1777            msg = (
1778                f"\n`{context_diff.environment}` environment will be initialized"
1779                if not context_diff.create_from_env_exists
1780                else f"\nNew environment `{context_diff.environment}` will be created from `{context_diff.create_from}`"
1781            )
1782            self._print(Tree(f"[bold]{msg}\n"))
1783            if not context_diff.has_snapshot_changes:
1784                return
1785
1786        if not context_diff.has_changes:
1787            # This is only reached when the plan is against an existing environment, so we use the environment
1788            #   name instead of the create_from name. The equivalent message for new environments happens in
1789            #   the PlanBuilder.
1790            self._print(
1791                Tree(
1792                    f"\n[bold]No changes to plan: project files match the `{context_diff.environment}` environment\n"
1793                )
1794            )
1795            return
1796
1797        if not context_diff.is_new_environment or (
1798            context_diff.is_new_environment and context_diff.create_from_env_exists
1799        ):
1800            self._print(
1801                Tree(
1802                    f"\n[bold]Differences from the `{context_diff.create_from if context_diff.is_new_environment else context_diff.environment}` environment:\n"
1803                )
1804            )
1805
1806        if context_diff.has_requirement_changes:
1807            self._print(f"[bold]Requirements:\n{context_diff.requirements_diff()}")
1808
1809        if context_diff.has_environment_statements_changes and not no_diff:
1810            self._print("[bold]Environment statements:\n")
1811            for type, diff in context_diff.environment_statements_diff(
1812                include_python_env=not context_diff.is_new_environment
1813            ):
1814                self._print(Syntax(diff, type, line_numbers=False))

Shows a summary of the environment differences.

Arguments:
  • context_diff: The context diff to use to print the summary
  • no_diff: Hide the actual environment statement differences.
def show_model_difference_summary( self, context_diff: sqlmesh.core.context_diff.ContextDiff, environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str], no_diff: bool = True) -> None:
1816    def show_model_difference_summary(
1817        self,
1818        context_diff: ContextDiff,
1819        environment_naming_info: EnvironmentNamingInfo,
1820        default_catalog: t.Optional[str],
1821        no_diff: bool = True,
1822    ) -> None:
1823        """Shows a summary of the model differences.
1824
1825        Args:
1826            context_diff: The context diff to use to print the summary
1827            environment_naming_info: The environment naming info to reference when printing model names
1828            default_catalog: The default catalog to reference when deciding to remove catalog from display names
1829            no_diff: Hide the actual SQL differences.
1830        """
1831        self._show_summary_tree_for(
1832            context_diff,
1833            "Models",
1834            lambda x: x.is_model,
1835            environment_naming_info,
1836            default_catalog,
1837            no_diff=no_diff,
1838        )
1839        self._show_summary_tree_for(
1840            context_diff,
1841            "Standalone Audits",
1842            lambda x: x.is_audit,
1843            environment_naming_info,
1844            default_catalog,
1845            no_diff=no_diff,
1846        )

Shows a summary of the model differences.

Arguments:
  • context_diff: The context diff to use to print the summary
  • environment_naming_info: The environment naming info to reference when printing model names
  • default_catalog: The default catalog to reference when deciding to remove catalog from display names
  • no_diff: Hide the actual SQL differences.
def plan( self, plan_builder: sqlmesh.core.plan.builder.PlanBuilder, auto_apply: bool, default_catalog: Optional[str], no_diff: bool = False, no_prompts: bool = False) -> None:
1848    def plan(
1849        self,
1850        plan_builder: PlanBuilder,
1851        auto_apply: bool,
1852        default_catalog: t.Optional[str],
1853        no_diff: bool = False,
1854        no_prompts: bool = False,
1855    ) -> None:
1856        """The main plan flow.
1857
1858        The console should present the user with choices on how to backfill and version the snapshots
1859        of a plan.
1860
1861        Args:
1862            plan: The plan to make choices for.
1863            auto_apply: Whether to automatically apply the plan after all choices have been made.
1864            default_catalog: The default catalog to reference when deciding to remove catalog from display names
1865            no_diff: Hide text differences for changed models.
1866            no_prompts: Whether to disable interactive prompts for the backfill time range. Please note that
1867                if this flag is set to true and there are uncategorized changes the plan creation will
1868                fail. Default: False
1869        """
1870        self._prompt_categorize(
1871            plan_builder,
1872            auto_apply,
1873            no_diff=no_diff,
1874            no_prompts=no_prompts,
1875            default_catalog=default_catalog,
1876        )
1877
1878        self._show_options_after_categorization(
1879            plan_builder, auto_apply, default_catalog=default_catalog, no_prompts=no_prompts
1880        )
1881
1882        if auto_apply:
1883            plan_builder.apply()

The main plan flow.

The console should present the user with choices on how to backfill and version the snapshots of a plan.

Arguments:
  • plan: The plan to make choices for.
  • auto_apply: Whether to automatically apply the plan after all choices have been made.
  • default_catalog: The default catalog to reference when deciding to remove catalog from display names
  • no_diff: Hide text differences for changed models.
  • no_prompts: Whether to disable interactive prompts for the backfill time range. Please note that if this flag is set to true and there are uncategorized changes the plan creation will fail. Default: False
def log_test_results( self, result: sqlmesh.core.test.result.ModelTextTestResult, target_dialect: str) -> None:
2224    def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
2225        # We don't log the test results if no tests were ran
2226        if not result.testsRun:
2227            return
2228
2229        divider_length = 70
2230
2231        self._log_test_details(result)
2232
2233        message = (
2234            f"Ran {result.testsRun} tests against {target_dialect} in {result.duration} seconds."
2235        )
2236        if result.wasSuccessful():
2237            self._print("=" * divider_length)
2238            self._print(
2239                f"Successfully {message}",
2240                style="green",
2241            )
2242            self._print("-" * divider_length)
2243        else:
2244            self._print("-" * divider_length)
2245            self._print("Test Failure Summary", style="red")
2246            self._print("=" * divider_length)
2247            fail_and_error_tests = result.get_fail_and_error_tests()
2248            self._print(f"{message} \n")
2249
2250            self._print(f"Failed tests ({len(fail_and_error_tests)}):")
2251            for test in fail_and_error_tests:
2252                self._print(f" • {test.path}::{test.test_name}")
2253            self._print("=" * divider_length, end="\n\n")

Display the test result and output.

Arguments:
  • result: The unittest test result that contains metrics like num success, fails, ect.
  • target_dialect: The dialect that tests were run against. Assumes all tests run against the same dialect.
def show_sql(self, sql: str) -> None:
2260    def show_sql(self, sql: str) -> None:
2261        self._print(Syntax(sql, "sql", word_wrap=True), crop=False)

Display to the user SQL.

def log_status_update(self, message: str) -> None:
2263    def log_status_update(self, message: str) -> None:
2264        self._print(message)

Display general status update to the user.

def log_skipped_models(self, snapshot_names: Set[str]) -> None:
2266    def log_skipped_models(self, snapshot_names: t.Set[str]) -> None:
2267        if snapshot_names:
2268            msg = "  " + "\n  ".join(snapshot_names)
2269            self._print(f"[dark_orange3]Skipped models[/dark_orange3]\n\n{msg}")

Display list of models skipped during evaluation to the user.

def log_failed_models( self, errors: List[sqlmesh.utils.concurrency.NodeExecutionFailedError]) -> None:
2271    def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None:
2272        if errors:
2273            self._print("\n[red]Failed models[/red]\n")
2274
2275            error_messages = _format_node_errors(errors)
2276
2277            for node_name, msg in error_messages.items():
2278                self._print(f"  [red]{node_name}[/red]\n\n{msg}")

Display list of models that failed during evaluation to the user.

def log_models_updated_during_restatement( self, snapshots: List[Tuple[sqlmesh.core.snapshot.definition.SnapshotTableInfo, sqlmesh.core.snapshot.definition.SnapshotTableInfo]], environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str] = None) -> None:
2280    def log_models_updated_during_restatement(
2281        self,
2282        snapshots: t.List[t.Tuple[SnapshotTableInfo, SnapshotTableInfo]],
2283        environment_naming_info: EnvironmentNamingInfo,
2284        default_catalog: t.Optional[str] = None,
2285    ) -> None:
2286        if snapshots:
2287            tree = Tree(
2288                f"[yellow]The following models had new versions deployed while data was being restated:[/yellow]"
2289            )
2290
2291            for restated_snapshot, updated_snapshot in snapshots:
2292                display_name = restated_snapshot.display_name(
2293                    environment_naming_info,
2294                    default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
2295                    dialect=self.dialect,
2296                )
2297                current_branch = tree.add(display_name)
2298                current_branch.add(f"restated version: '{restated_snapshot.version}'")
2299                current_branch.add(f"currently active version: '{updated_snapshot.version}'")
2300
2301            self._print(tree)
2302            self._print("")  # newline spacer

Display a list of models where new versions got deployed to the specified :environment while we were restating data the old versions

Arguments:
  • snapshots: a list of (snapshot_we_restated, snapshot_it_got_replaced_with_during_restatement) tuples
  • environment: which environment got updated while we were restating models
  • environment_naming_info: how snapshots are named in that :environment (for display name purposes)
  • default_catalog: the configured default catalog (for display name purposes)
def log_destructive_change( self, snapshot_name: str, alter_operations: List[sqlmesh.core.schema_diff.TableAlterOperation], dialect: str, error: bool = True) -> None:
2304    def log_destructive_change(
2305        self,
2306        snapshot_name: str,
2307        alter_operations: t.List[TableAlterOperation],
2308        dialect: str,
2309        error: bool = True,
2310    ) -> None:
2311        if error:
2312            self._print(format_destructive_change_msg(snapshot_name, alter_operations, dialect))
2313        else:
2314            self.log_warning(
2315                format_destructive_change_msg(snapshot_name, alter_operations, dialect, error)
2316            )

Display a destructive change error or warning to the user.

def log_additive_change( self, snapshot_name: str, alter_operations: List[sqlmesh.core.schema_diff.TableAlterOperation], dialect: str, error: bool = True) -> None:
2318    def log_additive_change(
2319        self,
2320        snapshot_name: str,
2321        alter_operations: t.List[TableAlterOperation],
2322        dialect: str,
2323        error: bool = True,
2324    ) -> None:
2325        if error:
2326            self._print(format_additive_change_msg(snapshot_name, alter_operations, dialect))
2327        else:
2328            self.log_warning(
2329                format_additive_change_msg(snapshot_name, alter_operations, dialect, error)
2330            )

Display an additive change error or warning to the user.

def log_error(self, message: str) -> None:
2332    def log_error(self, message: str) -> None:
2333        self._print(f"[red]{message}[/red]")

Display error info to the user.

def log_warning(self, short_message: str, long_message: Optional[str] = None) -> None:
2335    def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None:
2336        logger.warning(long_message or short_message)
2337        if not self.ignore_warnings:
2338            if long_message:
2339                file_path = None
2340                for handler in logger.root.handlers:
2341                    if isinstance(handler, logging.FileHandler):
2342                        file_path = handler.baseFilename
2343                        break
2344                file_path_msg = f" Learn more in logs: {file_path}\n" if file_path else ""
2345                short_message = f"{short_message}{file_path_msg}"
2346            message_lstrip = short_message.lstrip()
2347            leading_ws = short_message[: -len(message_lstrip)]
2348            message_formatted = f"{leading_ws}[yellow]\\[WARNING] {message_lstrip}[/yellow]"
2349            self._print(message_formatted)

Display warning info to the user.

Arguments:
  • short_message: The warning message to print to console.
  • long_message: The warning message to log to file. If not provided, short_message is used.
def log_success(self, message: str) -> None:
2351    def log_success(self, message: str) -> None:
2352        self._print(f"[green]{message}[/green]\n")

Display a general successful message to the user.

def loading_start(self, message: Optional[str] = None) -> uuid.UUID:
2354    def loading_start(self, message: t.Optional[str] = None) -> uuid.UUID:
2355        id = uuid.uuid4()
2356        self.loading_status[id] = Status(message or "", console=self.console, spinner="line")
2357        self.loading_status[id].start()
2358        return id

Starts loading and returns a unique ID that can be used to stop the loading. Optionally can display a message.

def loading_stop(self, id: uuid.UUID) -> None:
2360    def loading_stop(self, id: uuid.UUID) -> None:
2361        self.loading_status[id].stop()
2362        del self.loading_status[id]

Stop loading for the given id.

def show_table_diff_details(self, models_to_diff: List[str]) -> None:
2364    def show_table_diff_details(
2365        self,
2366        models_to_diff: t.List[str],
2367    ) -> None:
2368        """Display information about which tables are going to be diffed"""
2369
2370        if models_to_diff:
2371            m_tree = Tree("\n[b]Models to compare:")
2372            for m in models_to_diff:
2373                m_tree.add(f"[{self.TABLE_DIFF_SOURCE_BLUE}]{m}[/{self.TABLE_DIFF_SOURCE_BLUE}]")
2374            self._print(m_tree)
2375            self._print("")

Display information about which tables are going to be diffed

def start_table_diff_progress(self, models_to_diff: int) -> None:
2377    def start_table_diff_progress(self, models_to_diff: int) -> None:
2378        if not self.table_diff_progress:
2379            self.table_diff_progress = make_progress_bar(
2380                "Calculating model differences", self.console
2381            )
2382            self.table_diff_model_progress = Progress(
2383                TextColumn("{task.fields[view_name]}", justify="right"),
2384                SpinnerColumn(spinner_name="simpleDots"),
2385                console=self.console,
2386            )
2387
2388            progress_table = Table.grid()
2389            progress_table.add_row(self.table_diff_progress)
2390            progress_table.add_row(self.table_diff_model_progress)
2391
2392            self.table_diff_progress_live = Live(progress_table, refresh_per_second=10)
2393            self.table_diff_progress_live.start()
2394
2395            self.table_diff_model_task = self.table_diff_progress.add_task(
2396                "Diffing", total=models_to_diff
2397            )

Start table diff progress bar

def start_table_diff_model_progress(self, model: str) -> None:
2399    def start_table_diff_model_progress(self, model: str) -> None:
2400        if self.table_diff_model_progress and model not in self.table_diff_model_tasks:
2401            self.table_diff_model_tasks[model] = self.table_diff_model_progress.add_task(
2402                f"Diffing {model}...",
2403                view_name=model,
2404                total=1,
2405            )

Start table diff model progress

def update_table_diff_progress(self, model: str) -> None:
2407    def update_table_diff_progress(self, model: str) -> None:
2408        if self.table_diff_progress:
2409            self.table_diff_progress.update(self.table_diff_model_task, refresh=True, advance=1)
2410        if self.table_diff_model_progress and model in self.table_diff_model_tasks:
2411            model_task_id = self.table_diff_model_tasks[model]
2412            self.table_diff_model_progress.remove_task(model_task_id)

Update table diff progress bar

def stop_table_diff_progress(self, success: bool) -> None:
2414    def stop_table_diff_progress(self, success: bool) -> None:
2415        if self.table_diff_progress_live:
2416            self.table_diff_progress_live.stop()
2417            self.table_diff_progress_live = None
2418            self.log_status_update("")
2419
2420            if success:
2421                self.log_success(f"Table diff completed successfully!")
2422            else:
2423                self.log_error("Table diff failed!")
2424
2425        self.table_diff_progress = None
2426        self.table_diff_model_progress = None
2427        self.table_diff_model_tasks = {}

Stop table diff progress bar

def show_table_diff_summary(self, table_diff: sqlmesh.core.table_diff.TableDiff) -> None:
2429    def show_table_diff_summary(self, table_diff: TableDiff) -> None:
2430        tree = Tree("\n[b]Table Diff")
2431
2432        if table_diff.model_name:
2433            model = Tree("Model:")
2434            model.add(f"[blue]{table_diff.model_name}[/blue]")
2435
2436            tree.add(model)
2437
2438            envs = Tree("Environment:")
2439            source = Tree(
2440                f"Source: [{self.TABLE_DIFF_SOURCE_BLUE}]{table_diff.source_alias}[/{self.TABLE_DIFF_SOURCE_BLUE}]"
2441            )
2442            envs.add(source)
2443
2444            target = Tree(
2445                f"Target: [{self.TABLE_DIFF_TARGET_GREEN}]{table_diff.target_alias}[/{self.TABLE_DIFF_TARGET_GREEN}]"
2446            )
2447            envs.add(target)
2448
2449            tree.add(envs)
2450
2451        tables = Tree("Tables:")
2452
2453        tables.add(
2454            f"Source: [{self.TABLE_DIFF_SOURCE_BLUE}]{table_diff.source}[/{self.TABLE_DIFF_SOURCE_BLUE}]"
2455        )
2456        tables.add(
2457            f"Target: [{self.TABLE_DIFF_TARGET_GREEN}]{table_diff.target}[/{self.TABLE_DIFF_TARGET_GREEN}]"
2458        )
2459
2460        tree.add(tables)
2461
2462        join = Tree("Join On:")
2463        _, _, key_column_names = table_diff.key_columns
2464        for col_name in key_column_names:
2465            join.add(f"[yellow]{col_name}[/yellow]")
2466
2467        tree.add(join)
2468
2469        self._print(tree)

Display information about the tables being diffed and how they are being joined

def show_schema_diff(self, schema_diff: sqlmesh.core.table_diff.SchemaDiff) -> None:
2471    def show_schema_diff(self, schema_diff: SchemaDiff) -> None:
2472        source_name = schema_diff.source
2473        if schema_diff.source_alias:
2474            source_name = schema_diff.source_alias.upper()
2475        target_name = schema_diff.target
2476        if schema_diff.target_alias:
2477            target_name = schema_diff.target_alias.upper()
2478
2479        first_line = f"\n[b]Schema Diff Between '[{self.TABLE_DIFF_SOURCE_BLUE}]{source_name}[/{self.TABLE_DIFF_SOURCE_BLUE}]' and '[{self.TABLE_DIFF_TARGET_GREEN}]{target_name}[/{self.TABLE_DIFF_TARGET_GREEN}]'"
2480        if schema_diff.model_name:
2481            first_line = (
2482                first_line + f" environments for model '[blue]{schema_diff.model_name}[/blue]'"
2483            )
2484
2485        tree = Tree(first_line + ":")
2486
2487        if any([schema_diff.added, schema_diff.removed, schema_diff.modified]):
2488            if schema_diff.added:
2489                added = Tree("[green]Added Columns:")
2490                for c, t in schema_diff.added:
2491                    added.add(f"[green]{c} ({t})")
2492                tree.add(added)
2493
2494            if schema_diff.removed:
2495                removed = Tree("[red]Removed Columns:")
2496                for c, t in schema_diff.removed:
2497                    removed.add(f"[red]{c} ({t})")
2498                tree.add(removed)
2499
2500            if schema_diff.modified:
2501                modified = Tree("[magenta]Modified Columns:")
2502                for c, (ft, tt) in schema_diff.modified.items():
2503                    modified.add(f"[magenta]{c} ({ft} -> {tt})")
2504                tree.add(modified)
2505        else:
2506            tree.add("[b]Schemas match")
2507
2508        self.console.print(tree)

Show table schema diff.

def show_row_diff( self, row_diff: sqlmesh.core.table_diff.RowDiff, show_sample: bool = True, skip_grain_check: bool = False) -> None:
2510    def show_row_diff(
2511        self, row_diff: RowDiff, show_sample: bool = True, skip_grain_check: bool = False
2512    ) -> None:
2513        if row_diff.empty:
2514            self.console.print(
2515                "\n[b][red]Neither the source nor the target table contained any records[/red][/b]"
2516            )
2517            return
2518
2519        source_name = row_diff.source
2520        if row_diff.source_alias:
2521            source_name = row_diff.source_alias.upper()
2522        target_name = row_diff.target
2523        if row_diff.target_alias:
2524            target_name = row_diff.target_alias.upper()
2525
2526        if row_diff.stats["null_grain_count"] > 0 or (
2527            not skip_grain_check
2528            and (
2529                row_diff.stats["distinct_count_s"] != row_diff.stats["s_count"]
2530                or row_diff.stats["distinct_count_t"] != row_diff.stats["t_count"]
2531            )
2532        ):
2533            self.console.print(
2534                "[b][red]\nGrain should have unique and not-null audits for accurate results.[/red][/b]"
2535            )
2536
2537        tree = Tree("[b]Row Counts:[/b]")
2538        if row_diff.full_match_count:
2539            tree.add(
2540                f" [b][cyan]FULL MATCH[/cyan]:[/b] {row_diff.full_match_count} rows ({row_diff.full_match_pct}%)"
2541            )
2542        if row_diff.partial_match_count:
2543            tree.add(
2544                f" [b][blue]PARTIAL MATCH[/blue]:[/b] {row_diff.partial_match_count} rows ({row_diff.partial_match_pct}%)"
2545            )
2546        if row_diff.s_only_count:
2547            tree.add(
2548                f" [b][yellow]{source_name} ONLY[/yellow]:[/b] {row_diff.s_only_count} rows ({row_diff.s_only_pct}%)"
2549            )
2550        if row_diff.t_only_count:
2551            tree.add(
2552                f" [b][green]{target_name} ONLY[/green]:[/b] {row_diff.t_only_count} rows ({row_diff.t_only_pct}%)"
2553            )
2554        self.console.print("\n", tree)
2555
2556        self.console.print("\n[b][blue]COMMON ROWS[/blue] column comparison stats:[/b]")
2557        if row_diff.column_stats.shape[0] > 0:
2558            self.console.print(row_diff.column_stats.to_string(index=True), end="\n\n")
2559        else:
2560            self.console.print("  No columns with same name and data type in both tables")
2561
2562        if show_sample:
2563            sample = row_diff.joined_sample
2564            self.console.print("\n[b][blue]COMMON ROWS[/blue] sample data differences:[/b]")
2565            if sample.shape[0] > 0:
2566                keys: list[str] = []
2567                columns: dict[str, list[str]] = {}
2568                source_prefix, source_name = (
2569                    (f"{source_name}__", source_name)
2570                    if source_name.lower() != row_diff.source.lower()
2571                    else ("s__", "SOURCE")
2572                )
2573                target_prefix, target_name = (
2574                    (f"{target_name}__", target_name)
2575                    if target_name.lower() != row_diff.target.lower()
2576                    else ("t__", "TARGET")
2577                )
2578
2579                # Extract key and column names from the joined sample
2580                for column in row_diff.joined_sample.columns:
2581                    if source_prefix in column:
2582                        column_name = "__".join(column.split(source_prefix)[1:])
2583                        columns[column_name] = [column, target_prefix + column_name]
2584                    elif target_prefix not in column:
2585                        keys.append(column)
2586
2587                column_styles = {
2588                    source_name: self.TABLE_DIFF_SOURCE_BLUE,
2589                    target_name: self.TABLE_DIFF_TARGET_GREEN,
2590                }
2591
2592                for column, [source_column, target_column] in columns.items():
2593                    # Create a table with the joined keys and comparison columns
2594                    column_table = row_diff.joined_sample[keys + [source_column, target_column]]
2595
2596                    # Filter to retain non identical-valued rows
2597                    column_table = column_table[
2598                        column_table.apply(
2599                            lambda row: not _cells_match(row[source_column], row[target_column]),
2600                            axis=1,
2601                        )
2602                    ]
2603
2604                    # Rename the column headers for readability
2605                    column_table = column_table.rename(
2606                        columns={
2607                            source_column: source_name,
2608                            target_column: target_name,
2609                        }
2610                    )
2611
2612                    table = Table(show_header=True)
2613                    for column_name in column_table.columns:
2614                        style = column_styles.get(column_name, "")
2615                        table.add_column(column_name, style=style, header_style=style)
2616
2617                    for _, row in column_table.iterrows():
2618                        table.add_row(
2619                            *[
2620                                str(
2621                                    round(cell, row_diff.decimals)
2622                                    if isinstance(cell, float)
2623                                    else cell
2624                                )
2625                                for cell in row
2626                            ]
2627                        )
2628
2629                    self.console.print(
2630                        f"Column: [underline][bold cyan]{column}[/bold cyan][/underline]",
2631                        table,
2632                        end="\n",
2633                    )
2634
2635            else:
2636                self.console.print("  All joined rows match")
2637
2638            if row_diff.s_sample.shape[0] > 0:
2639                self.console.print(f"\n[b][yellow]{source_name} ONLY[/yellow] sample rows:[/b]")
2640                self.console.print(row_diff.s_sample.to_string(index=False), end="\n\n")
2641
2642            if row_diff.t_sample.shape[0] > 0:
2643                self.console.print(f"\n[b][green]{target_name} ONLY[/green] sample rows:[/b]")
2644                self.console.print(row_diff.t_sample.to_string(index=False), end="\n\n")

Show table summary diff.

def show_table_diff( self, table_diffs: List[sqlmesh.core.table_diff.TableDiff], show_sample: bool = True, skip_grain_check: bool = False, temp_schema: Optional[str] = None) -> None:
2646    def show_table_diff(
2647        self,
2648        table_diffs: t.List[TableDiff],
2649        show_sample: bool = True,
2650        skip_grain_check: bool = False,
2651        temp_schema: t.Optional[str] = None,
2652    ) -> None:
2653        """
2654        Display the table diff between all mismatched tables.
2655        """
2656        if len(table_diffs) > 1:
2657            mismatched_tables = []
2658            fully_matched = []
2659            for table_diff in table_diffs:
2660                if (
2661                    table_diff.schema_diff().source_schema == table_diff.schema_diff().target_schema
2662                ) and (
2663                    table_diff.row_diff(
2664                        temp_schema=temp_schema, skip_grain_check=skip_grain_check
2665                    ).full_match_pct
2666                    == 100
2667                ):
2668                    fully_matched.append(table_diff)
2669                else:
2670                    mismatched_tables.append(table_diff)
2671            table_diffs = mismatched_tables if mismatched_tables else []
2672            if fully_matched:
2673                m_tree = Tree("\n[b]Identical Tables")
2674                for m in fully_matched:
2675                    m_tree.add(
2676                        f"[{self.TABLE_DIFF_SOURCE_BLUE}]{m.source}[/{self.TABLE_DIFF_SOURCE_BLUE}] - [{self.TABLE_DIFF_TARGET_GREEN}]{m.target}[/{self.TABLE_DIFF_TARGET_GREEN}]"
2677                    )
2678                self._print(m_tree)
2679
2680            if mismatched_tables:
2681                m_tree = Tree("\n[b]Mismatched Tables")
2682                for m in mismatched_tables:
2683                    m_tree.add(
2684                        f"[{self.TABLE_DIFF_SOURCE_BLUE}]{m.source}[/{self.TABLE_DIFF_SOURCE_BLUE}] - [{self.TABLE_DIFF_TARGET_GREEN}]{m.target}[/{self.TABLE_DIFF_TARGET_GREEN}]"
2685                    )
2686                self._print(m_tree)
2687
2688        for table_diff in table_diffs:
2689            self.show_table_diff_summary(table_diff)
2690            self.show_schema_diff(table_diff.schema_diff())
2691            self.show_row_diff(
2692                table_diff.row_diff(temp_schema=temp_schema, skip_grain_check=skip_grain_check),
2693                show_sample=show_sample,
2694                skip_grain_check=skip_grain_check,
2695            )

Display the table diff between all mismatched tables.

def print_environments( self, environments_summary: List[sqlmesh.core.environment.EnvironmentSummary]) -> None:
2697    def print_environments(self, environments_summary: t.List[EnvironmentSummary]) -> None:
2698        """Prints all environment names along with expiry datetime."""
2699        output = [
2700            f"{summary.name} - {time_like_to_str(summary.expiration_ts)}"
2701            if summary.expiration_ts
2702            else f"{summary.name} - No Expiry"
2703            for summary in environments_summary
2704        ]
2705        output_str = "\n".join([str(len(output)), *output])
2706        self.log_status_update(f"Number of SQLMesh environments are: {output_str}")

Prints all environment names along with expiry datetime.

def show_intervals( self, snapshot_intervals: Dict[sqlmesh.core.snapshot.definition.Snapshot, sqlmesh.core.plan.definition.SnapshotIntervals]) -> None:
2708    def show_intervals(self, snapshot_intervals: t.Dict[Snapshot, SnapshotIntervals]) -> None:
2709        complete = Tree(f"[b]Complete Intervals[/b]")
2710        incomplete = Tree(f"[b]Missing Intervals[/b]")
2711
2712        for snapshot, intervals in sorted(snapshot_intervals.items(), key=lambda s: s[0].node.name):
2713            if intervals.intervals:
2714                incomplete.add(
2715                    f"{snapshot.node.name}: [{intervals.format_intervals(snapshot.node.interval_unit)}]"
2716                )
2717            else:
2718                complete.add(snapshot.node.name)
2719
2720        if complete.children:
2721            self._print(complete)
2722
2723        if incomplete.children:
2724            self._print(incomplete)

Show ready intervals

def print_connection_config( self, config: sqlmesh.core.config.connection.ConnectionConfig, title: str = 'Connection') -> None:
2726    def print_connection_config(self, config: ConnectionConfig, title: str = "Connection") -> None:
2727        tree = Tree(f"[b]{title}:[/b]")
2728        tree.add(f"Type: [bold cyan]{config.type_}[/bold cyan]")
2729        tree.add(f"Catalog: [bold cyan]{config.get_catalog()}[/bold cyan]")
2730
2731        try:
2732            engine_adapter_type = config._engine_adapter
2733            tree.add(f"Dialect: [bold cyan]{engine_adapter_type.DIALECT}[/bold cyan]")
2734        except NotImplementedError:
2735            # not all ConnectionConfig's have an engine adapter associated. The CloudConnectionConfig has a HTTP client instead
2736            pass
2737
2738        self._print(tree)
2795    def show_linter_violations(
2796        self, violations: t.List[RuleViolation], model: Model, is_error: bool = False
2797    ) -> None:
2798        severity = "errors" if is_error else "warnings"
2799
2800        # Sort violations by line, then alphabetically the name of the violation
2801        # Violations with no range go first
2802        sorted_violations = sorted(
2803            violations,
2804            key=lambda v: (
2805                v.violation_range.start.line if v.violation_range else -1,
2806                v.rule.name.lower(),
2807            ),
2808        )
2809        violations_text = [
2810            (
2811                f" - Line {v.violation_range.start.line + 1}: {v.rule.name} - {v.violation_msg}"
2812                if v.violation_range
2813                else f" - {v.rule.name}: {v.violation_msg}"
2814            )
2815            for v in sorted_violations
2816        ]
2817        violations_msg = "\n".join(violations_text)
2818        msg = f"Linter {severity} for {model._path}:\n{violations_msg}"
2819
2820        if is_error:
2821            self.log_error(msg)
2822        else:
2823            self.log_warning(msg)

Prints all linter violations depending on their severity

def add_to_layout_widget( target_widget: ~LayoutWidget, *widgets: ipywidgets.widgets.widget.Widget) -> ~LayoutWidget:
2890def add_to_layout_widget(target_widget: LayoutWidget, *widgets: widgets.Widget) -> LayoutWidget:
2891    """Helper function to add a widget to a layout widget.
2892
2893    Args:
2894        target_widget: The layout widget to add the other widget(s) to.
2895        *widgets: The widgets to add to the layout widget.
2896
2897    Returns:
2898        The layout widget with the children added.
2899    """
2900    target_widget.children += tuple(widgets)
2901    return target_widget

Helper function to add a widget to a layout widget.

Arguments:
  • target_widget: The layout widget to add the other widget(s) to.
  • *widgets: The widgets to add to the layout widget.
Returns:

The layout widget with the children added.

class NotebookMagicConsole(TerminalConsole):
2904class NotebookMagicConsole(TerminalConsole):
2905    """
2906    Console to be used when using the magic notebook interface (`%<command>`).
2907    Generally reuses the Terminal console when possible by either directly outputing what it provides
2908    or capturing it and converting it into a widget.
2909    """
2910
2911    def __init__(
2912        self,
2913        display: t.Optional[t.Callable] = None,
2914        console: t.Optional[RichConsole] = None,
2915        dialect: DialectType = None,
2916        **kwargs: t.Any,
2917    ) -> None:
2918        import ipywidgets as widgets
2919        from IPython import get_ipython
2920        from IPython.display import display as ipython_display
2921
2922        super().__init__(console, **kwargs)
2923
2924        self.display = display or get_ipython().user_ns.get("display", ipython_display)
2925        self.missing_dates_output = widgets.Output()
2926        self.dynamic_options_after_categorization_output = widgets.VBox()
2927
2928        self.dialect = dialect
2929
2930    def _show_missing_dates(self, plan: Plan, default_catalog: t.Optional[str]) -> None:
2931        self._add_to_dynamic_options(self.missing_dates_output)
2932        self.missing_dates_output.outputs = ()
2933        with self.missing_dates_output:
2934            super()._show_missing_dates(plan, default_catalog)
2935
2936    def _apply(self, button: widgets.Button) -> None:
2937        button.disabled = True
2938        with button.output:
2939            button.plan_builder.apply()
2940
2941    def _prompt_promote(self, plan_builder: PlanBuilder) -> None:
2942        import ipywidgets as widgets
2943
2944        button = widgets.Button(
2945            description="Apply - Virtual Update",
2946            disabled=False,
2947            button_style="success",
2948            # Auto will make the button really large.
2949            # Likely changing this soon anyways to be just `Apply` with description above
2950            layout={"width": "10rem"},
2951        )
2952        self._add_to_dynamic_options(button)
2953        output = widgets.Output()
2954        self._add_to_dynamic_options(output)
2955
2956        button.plan_builder = plan_builder
2957        button.on_click(self._apply)
2958        button.output = output
2959
2960    def _prompt_effective_from(
2961        self, plan_builder: PlanBuilder, auto_apply: bool, default_catalog: t.Optional[str]
2962    ) -> None:
2963        import ipywidgets as widgets
2964
2965        prompt = widgets.VBox()
2966
2967        def effective_from_change_callback(change: t.Dict[str, datetime.datetime]) -> None:
2968            plan_builder.set_effective_from(change["new"])
2969            self._show_options_after_categorization(
2970                plan_builder, auto_apply, default_catalog, no_prompts=False
2971            )
2972
2973        def going_forward_change_callback(change: t.Dict[str, bool]) -> None:
2974            checked = change["new"]
2975            plan_builder.set_effective_from(None if checked else yesterday_ds())
2976            self._show_options_after_categorization(
2977                plan_builder,
2978                auto_apply=auto_apply,
2979                default_catalog=default_catalog,
2980                no_prompts=False,
2981            )
2982
2983        date_picker = widgets.DatePicker(
2984            disabled=plan_builder.build().effective_from is None,
2985            value=to_date(plan_builder.build().effective_from or yesterday_ds()),
2986            layout={"width": "auto"},
2987        )
2988        date_picker.observe(effective_from_change_callback, "value")
2989
2990        going_forward_checkbox = widgets.Checkbox(
2991            value=plan_builder.build().effective_from is None,
2992            description="Apply Going Forward Once Deployed To Prod",
2993            disabled=False,
2994            indent=False,
2995        )
2996        going_forward_checkbox.observe(going_forward_change_callback, "value")
2997
2998        add_to_layout_widget(
2999            prompt,
3000            widgets.HBox(
3001                [
3002                    widgets.Label("Effective From Date:", layout={"width": "8rem"}),
3003                    date_picker,
3004                    going_forward_checkbox,
3005                ]
3006            ),
3007        )
3008
3009        self._add_to_dynamic_options(prompt)
3010
3011    def _prompt_backfill(
3012        self, plan_builder: PlanBuilder, auto_apply: bool, default_catalog: t.Optional[str]
3013    ) -> None:
3014        import ipywidgets as widgets
3015
3016        prompt = widgets.VBox()
3017
3018        backfill_or_preview = (
3019            "Preview"
3020            if plan_builder.build().is_dev and plan_builder.build().forward_only
3021            else "Backfill"
3022        )
3023
3024        def _date_picker(
3025            plan_builder: PlanBuilder, value: t.Any, on_change: t.Callable, disabled: bool = False
3026        ) -> widgets.DatePicker:
3027            picker = widgets.DatePicker(
3028                disabled=disabled,
3029                value=value,
3030                layout={"width": "auto"},
3031            )
3032
3033            picker.observe(on_change, "value")
3034            return picker
3035
3036        def start_change_callback(change: t.Dict[str, datetime.datetime]) -> None:
3037            plan_builder.set_start(change["new"])
3038            self._show_options_after_categorization(
3039                plan_builder, auto_apply, default_catalog, no_prompts=False
3040            )
3041
3042        def end_change_callback(change: t.Dict[str, datetime.datetime]) -> None:
3043            plan_builder.set_end(change["new"])
3044            self._show_options_after_categorization(
3045                plan_builder, auto_apply, default_catalog, no_prompts=False
3046            )
3047
3048        if plan_builder.is_start_and_end_allowed:
3049            add_to_layout_widget(
3050                prompt,
3051                widgets.HBox(
3052                    [
3053                        widgets.Label(
3054                            f"Start {backfill_or_preview} Date:", layout={"width": "8rem"}
3055                        ),
3056                        _date_picker(
3057                            plan_builder, to_date(plan_builder.build().start), start_change_callback
3058                        ),
3059                    ]
3060                ),
3061            )
3062
3063            add_to_layout_widget(
3064                prompt,
3065                widgets.HBox(
3066                    [
3067                        widgets.Label(f"End {backfill_or_preview} Date:", layout={"width": "8rem"}),
3068                        _date_picker(
3069                            plan_builder,
3070                            to_date(plan_builder.build().end),
3071                            end_change_callback,
3072                        ),
3073                    ]
3074                ),
3075            )
3076
3077        self._add_to_dynamic_options(prompt)
3078
3079        if not auto_apply:
3080            button = widgets.Button(
3081                description=f"Apply - {backfill_or_preview} Tables",
3082                disabled=False,
3083                button_style="success",
3084            )
3085            self._add_to_dynamic_options(button)
3086            output = widgets.Output()
3087            self._add_to_dynamic_options(output)
3088
3089            button.plan_builder = plan_builder
3090            button.on_click(self._apply)
3091            button.output = output
3092
3093    def _show_options_after_categorization(
3094        self,
3095        plan_builder: PlanBuilder,
3096        auto_apply: bool,
3097        default_catalog: t.Optional[str],
3098        no_prompts: bool,
3099    ) -> None:
3100        self.dynamic_options_after_categorization_output.children = ()
3101        self.display(self.dynamic_options_after_categorization_output)
3102        super()._show_options_after_categorization(
3103            plan_builder, auto_apply, default_catalog, no_prompts
3104        )
3105
3106    def _add_to_dynamic_options(self, *widgets: widgets.Widget) -> None:
3107        add_to_layout_widget(self.dynamic_options_after_categorization_output, *widgets)
3108
3109    def _get_snapshot_change_category(
3110        self,
3111        snapshot: Snapshot,
3112        plan_builder: PlanBuilder,
3113        auto_apply: bool,
3114        default_catalog: t.Optional[str],
3115    ) -> None:
3116        import ipywidgets as widgets
3117
3118        choice_mapping = self._snapshot_change_choices(
3119            snapshot,
3120            plan_builder.environment_naming_info,
3121            default_catalog,
3122            use_rich_formatting=False,
3123        )
3124        choices = list(choice_mapping)
3125        plan_builder.set_choice(snapshot, choices[0])
3126
3127        def radio_button_selected(change: t.Dict[str, t.Any]) -> None:
3128            plan_builder.set_choice(snapshot, choices[change["owner"].index])
3129            self._show_options_after_categorization(
3130                plan_builder, auto_apply, default_catalog, no_prompts=False
3131            )
3132
3133        radio = widgets.RadioButtons(
3134            options=choice_mapping.values(),
3135            layout={"width": "max-content"},
3136            disabled=False,
3137        )
3138        radio.observe(
3139            radio_button_selected,
3140            "value",
3141        )
3142        self.display(radio)
3143
3144    def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
3145        # We don't log the test results if no tests were ran
3146        if not result.testsRun:
3147            return
3148
3149        import ipywidgets as widgets
3150
3151        divider_length = 70
3152        shared_style = {
3153            "font-size": "11px",
3154            "font-weight": "bold",
3155            "font-family": "Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace",
3156        }
3157
3158        message = (
3159            f"Ran {result.testsRun} tests against {target_dialect} in {result.duration} seconds."
3160        )
3161
3162        if result.wasSuccessful():
3163            success_color = {"color": "#008000"}
3164            header = str(h("span", {"style": shared_style}, "-" * divider_length))
3165            message = str(
3166                h(
3167                    "span",
3168                    {"style": {**shared_style, **success_color}},
3169                    f"Successfully {message}",
3170                )
3171            )
3172            footer = str(h("span", {"style": shared_style}, "=" * divider_length))
3173            self.display(widgets.HTML("<br>".join([header, message, footer])))
3174        else:
3175            output = self._captured_unit_test_results(result)
3176
3177            fail_color = {"color": "#db3737"}
3178            fail_shared_style = {**shared_style, **fail_color}
3179            header = str(h("span", {"style": fail_shared_style}, "-" * divider_length))
3180            message = str(h("span", {"style": fail_shared_style}, "Test Failure Summary"))
3181            fail_and_error_tests = result.get_fail_and_error_tests()
3182            failed_tests = [
3183                str(
3184                    h(
3185                        "span",
3186                        {"style": fail_shared_style},
3187                        f"Failed tests ({len(fail_and_error_tests)}):",
3188                    )
3189                )
3190            ]
3191
3192            for test in fail_and_error_tests:
3193                failed_tests.append(
3194                    str(
3195                        h(
3196                            "span",
3197                            {"style": fail_shared_style},
3198                            f" • {test.model.name}::{test.test_name}",
3199                        )
3200                    )
3201                )
3202            failures = "<br>".join(failed_tests)
3203            footer = str(h("span", {"style": fail_shared_style}, "=" * divider_length))
3204            error_output = widgets.Textarea(output, layout={"height": "300px", "width": "100%"})
3205            test_info = widgets.HTML("<br>".join([header, message, footer, failures, footer]))
3206            self.display(widgets.VBox(children=[test_info, error_output], layout={"width": "100%"}))

Console to be used when using the magic notebook interface (%<command>). Generally reuses the Terminal console when possible by either directly outputing what it provides or capturing it and converting it into a widget.

NotebookMagicConsole( display: Optional[Callable] = None, console: Optional[rich.console.Console] = None, dialect: Union[str, sqlglot.dialects.dialect.Dialect, Type[sqlglot.dialects.dialect.Dialect], NoneType] = None, **kwargs: Any)
2911    def __init__(
2912        self,
2913        display: t.Optional[t.Callable] = None,
2914        console: t.Optional[RichConsole] = None,
2915        dialect: DialectType = None,
2916        **kwargs: t.Any,
2917    ) -> None:
2918        import ipywidgets as widgets
2919        from IPython import get_ipython
2920        from IPython.display import display as ipython_display
2921
2922        super().__init__(console, **kwargs)
2923
2924        self.display = display or get_ipython().user_ns.get("display", ipython_display)
2925        self.missing_dates_output = widgets.Output()
2926        self.dynamic_options_after_categorization_output = widgets.VBox()
2927
2928        self.dialect = dialect
display
missing_dates_output
dynamic_options_after_categorization_output
dialect
def log_test_results( self, result: sqlmesh.core.test.result.ModelTextTestResult, target_dialect: str) -> None:
3144    def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
3145        # We don't log the test results if no tests were ran
3146        if not result.testsRun:
3147            return
3148
3149        import ipywidgets as widgets
3150
3151        divider_length = 70
3152        shared_style = {
3153            "font-size": "11px",
3154            "font-weight": "bold",
3155            "font-family": "Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace",
3156        }
3157
3158        message = (
3159            f"Ran {result.testsRun} tests against {target_dialect} in {result.duration} seconds."
3160        )
3161
3162        if result.wasSuccessful():
3163            success_color = {"color": "#008000"}
3164            header = str(h("span", {"style": shared_style}, "-" * divider_length))
3165            message = str(
3166                h(
3167                    "span",
3168                    {"style": {**shared_style, **success_color}},
3169                    f"Successfully {message}",
3170                )
3171            )
3172            footer = str(h("span", {"style": shared_style}, "=" * divider_length))
3173            self.display(widgets.HTML("<br>".join([header, message, footer])))
3174        else:
3175            output = self._captured_unit_test_results(result)
3176
3177            fail_color = {"color": "#db3737"}
3178            fail_shared_style = {**shared_style, **fail_color}
3179            header = str(h("span", {"style": fail_shared_style}, "-" * divider_length))
3180            message = str(h("span", {"style": fail_shared_style}, "Test Failure Summary"))
3181            fail_and_error_tests = result.get_fail_and_error_tests()
3182            failed_tests = [
3183                str(
3184                    h(
3185                        "span",
3186                        {"style": fail_shared_style},
3187                        f"Failed tests ({len(fail_and_error_tests)}):",
3188                    )
3189                )
3190            ]
3191
3192            for test in fail_and_error_tests:
3193                failed_tests.append(
3194                    str(
3195                        h(
3196                            "span",
3197                            {"style": fail_shared_style},
3198                            f" • {test.model.name}::{test.test_name}",
3199                        )
3200                    )
3201                )
3202            failures = "<br>".join(failed_tests)
3203            footer = str(h("span", {"style": fail_shared_style}, "=" * divider_length))
3204            error_output = widgets.Textarea(output, layout={"height": "300px", "width": "100%"})
3205            test_info = widgets.HTML("<br>".join([header, message, footer, failures, footer]))
3206            self.display(widgets.VBox(children=[test_info, error_output], layout={"width": "100%"}))

Display the test result and output.

Arguments:
  • result: The unittest test result that contains metrics like num success, fails, ect.
  • target_dialect: The dialect that tests were run against. Assumes all tests run against the same dialect.
Inherited Members
TerminalConsole
TABLE_DIFF_SOURCE_BLUE
TABLE_DIFF_TARGET_GREEN
AUDIT_PASS_MARK
GREEN_AUDIT_PASS_MARK
AUDIT_FAIL_MARK
AUDIT_PADDING
CHECK_MARK
console
evaluation_progress_live
evaluation_total_progress
evaluation_total_task
evaluation_model_progress
evaluation_model_tasks
evaluation_model_batch_sizes
evaluation_column_widths
environment_naming_info
default_catalog
creation_progress
creation_column_widths
creation_task
promotion_progress
promotion_column_widths
promotion_task
migration_progress
migration_task
env_migration_progress
env_migration_task
loading_status
state_export_progress
state_export_version_task
state_export_snapshot_task
state_export_environment_task
state_import_progress
state_import_version_task
state_import_snapshot_task
state_import_environment_task
table_diff_progress
table_diff_model_progress
table_diff_model_tasks
table_diff_progress_live
signal_progress_logged
signal_status_tree
verbosity
ignore_warnings
start_plan_evaluation
stop_plan_evaluation
start_evaluation_progress
start_snapshot_evaluation_progress
update_snapshot_evaluation_progress
stop_evaluation_progress
start_signal_progress
update_signal_progress
stop_signal_progress
start_creation_progress
update_creation_progress
stop_creation_progress
start_cleanup
update_cleanup_progress
stop_cleanup
start_destroy
stop_destroy
start_promotion_progress
update_promotion_progress
stop_promotion_progress
start_snapshot_migration_progress
update_snapshot_migration_progress
log_migration_status
stop_snapshot_migration_progress
start_env_migration_progress
update_env_migration_progress
stop_env_migration_progress
start_state_export
update_state_export_progress
stop_state_export
start_state_import
update_state_import_progress
stop_state_import
show_environment_difference_summary
show_model_difference_summary
plan
show_sql
log_status_update
log_skipped_models
log_failed_models
log_models_updated_during_restatement
log_destructive_change
log_additive_change
log_error
log_warning
log_success
loading_start
loading_stop
show_table_diff_details
start_table_diff_progress
start_table_diff_model_progress
update_table_diff_progress
stop_table_diff_progress
show_table_diff_summary
show_schema_diff
show_row_diff
show_table_diff
print_environments
show_intervals
print_connection_config
show_linter_violations
Console
INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD
class CaptureTerminalConsole(TerminalConsole):
3209class CaptureTerminalConsole(TerminalConsole):
3210    """
3211    Captures the output of the terminal console so that it can be extracted out and displayed within other interfaces.
3212    The captured output is cleared out after it is retrieved.
3213
3214    Note: `_prompt` and `_confirm` need to also be overriden to work with the custom interface if you want to use
3215    this console interactively.
3216    """
3217
3218    def __init__(self, console: t.Optional[RichConsole] = None, **kwargs: t.Any) -> None:
3219        super().__init__(console=console, **kwargs)
3220        self._captured_outputs: t.List[str] = []
3221        self._warnings: t.List[str] = []
3222        self._errors: t.List[str] = []
3223
3224    @property
3225    def captured_output(self) -> str:
3226        return "".join(self._captured_outputs)
3227
3228    @property
3229    def captured_warnings(self) -> str:
3230        return "".join(self._warnings)
3231
3232    @property
3233    def captured_errors(self) -> str:
3234        return "".join(self._errors)
3235
3236    def consume_captured_output(self) -> str:
3237        try:
3238            return self.captured_output
3239        finally:
3240            self._captured_outputs = []
3241
3242    def consume_captured_warnings(self) -> str:
3243        try:
3244            return self.captured_warnings
3245        finally:
3246            self._warnings = []
3247
3248    def consume_captured_errors(self) -> str:
3249        try:
3250            return self.captured_errors
3251        finally:
3252            self._errors = []
3253
3254    def log_warning(
3255        self,
3256        short_message: str,
3257        long_message: t.Optional[str] = None,
3258        *args: t.Any,
3259        **kwargs: t.Any,
3260    ) -> None:
3261        if short_message not in self._warnings:
3262            self._warnings.append(short_message)
3263        if kwargs.pop("print", True):
3264            super().log_warning(short_message, long_message)
3265
3266    def log_error(self, message: str, *args: t.Any, **kwargs: t.Any) -> None:
3267        if message not in self._errors:
3268            self._errors.append(message)
3269        if kwargs.pop("print", True):
3270            super().log_error(message)
3271
3272    def log_skipped_models(self, snapshot_names: t.Set[str]) -> None:
3273        if snapshot_names:
3274            self._captured_outputs.append(
3275                "\n".join([f"SKIPPED snapshot {skipped}\n" for skipped in snapshot_names])
3276            )
3277            super().log_skipped_models(snapshot_names)
3278
3279    def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None:
3280        self._errors.extend([str(ex) for ex in errors if str(ex) not in self._errors])
3281        super().log_failed_models(errors)
3282
3283    def _print(self, value: t.Any, **kwargs: t.Any) -> None:
3284        with self.console.capture() as capture:
3285            self.console.print(value, **kwargs)
3286        self._captured_outputs.append(capture.get())

Captures the output of the terminal console so that it can be extracted out and displayed within other interfaces. The captured output is cleared out after it is retrieved.

Note: _prompt and _confirm need to also be overriden to work with the custom interface if you want to use this console interactively.

CaptureTerminalConsole(console: Optional[rich.console.Console] = None, **kwargs: Any)
3218    def __init__(self, console: t.Optional[RichConsole] = None, **kwargs: t.Any) -> None:
3219        super().__init__(console=console, **kwargs)
3220        self._captured_outputs: t.List[str] = []
3221        self._warnings: t.List[str] = []
3222        self._errors: t.List[str] = []
captured_output: str
3224    @property
3225    def captured_output(self) -> str:
3226        return "".join(self._captured_outputs)
captured_warnings: str
3228    @property
3229    def captured_warnings(self) -> str:
3230        return "".join(self._warnings)
captured_errors: str
3232    @property
3233    def captured_errors(self) -> str:
3234        return "".join(self._errors)
def consume_captured_output(self) -> str:
3236    def consume_captured_output(self) -> str:
3237        try:
3238            return self.captured_output
3239        finally:
3240            self._captured_outputs = []
def consume_captured_warnings(self) -> str:
3242    def consume_captured_warnings(self) -> str:
3243        try:
3244            return self.captured_warnings
3245        finally:
3246            self._warnings = []
def consume_captured_errors(self) -> str:
3248    def consume_captured_errors(self) -> str:
3249        try:
3250            return self.captured_errors
3251        finally:
3252            self._errors = []
def log_warning( self, short_message: str, long_message: Optional[str] = None, *args: Any, **kwargs: Any) -> None:
3254    def log_warning(
3255        self,
3256        short_message: str,
3257        long_message: t.Optional[str] = None,
3258        *args: t.Any,
3259        **kwargs: t.Any,
3260    ) -> None:
3261        if short_message not in self._warnings:
3262            self._warnings.append(short_message)
3263        if kwargs.pop("print", True):
3264            super().log_warning(short_message, long_message)

Display warning info to the user.

Arguments:
  • short_message: The warning message to print to console.
  • long_message: The warning message to log to file. If not provided, short_message is used.
def log_error(self, message: str, *args: Any, **kwargs: Any) -> None:
3266    def log_error(self, message: str, *args: t.Any, **kwargs: t.Any) -> None:
3267        if message not in self._errors:
3268            self._errors.append(message)
3269        if kwargs.pop("print", True):
3270            super().log_error(message)

Display error info to the user.

def log_skipped_models(self, snapshot_names: Set[str]) -> None:
3272    def log_skipped_models(self, snapshot_names: t.Set[str]) -> None:
3273        if snapshot_names:
3274            self._captured_outputs.append(
3275                "\n".join([f"SKIPPED snapshot {skipped}\n" for skipped in snapshot_names])
3276            )
3277            super().log_skipped_models(snapshot_names)

Display list of models skipped during evaluation to the user.

def log_failed_models( self, errors: List[sqlmesh.utils.concurrency.NodeExecutionFailedError]) -> None:
3279    def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None:
3280        self._errors.extend([str(ex) for ex in errors if str(ex) not in self._errors])
3281        super().log_failed_models(errors)

Display list of models that failed during evaluation to the user.

Inherited Members
TerminalConsole
TABLE_DIFF_SOURCE_BLUE
TABLE_DIFF_TARGET_GREEN
AUDIT_PASS_MARK
GREEN_AUDIT_PASS_MARK
AUDIT_FAIL_MARK
AUDIT_PADDING
CHECK_MARK
console
evaluation_progress_live
evaluation_total_progress
evaluation_total_task
evaluation_model_progress
evaluation_model_tasks
evaluation_model_batch_sizes
evaluation_column_widths
environment_naming_info
default_catalog
creation_progress
creation_column_widths
creation_task
promotion_progress
promotion_column_widths
promotion_task
migration_progress
migration_task
env_migration_progress
env_migration_task
loading_status
state_export_progress
state_export_version_task
state_export_snapshot_task
state_export_environment_task
state_import_progress
state_import_version_task
state_import_snapshot_task
state_import_environment_task
table_diff_progress
table_diff_model_progress
table_diff_model_tasks
table_diff_progress_live
signal_progress_logged
signal_status_tree
verbosity
dialect
ignore_warnings
start_plan_evaluation
stop_plan_evaluation
start_evaluation_progress
start_snapshot_evaluation_progress
update_snapshot_evaluation_progress
stop_evaluation_progress
start_signal_progress
update_signal_progress
stop_signal_progress
start_creation_progress
update_creation_progress
stop_creation_progress
start_cleanup
update_cleanup_progress
stop_cleanup
start_destroy
stop_destroy
start_promotion_progress
update_promotion_progress
stop_promotion_progress
start_snapshot_migration_progress
update_snapshot_migration_progress
log_migration_status
stop_snapshot_migration_progress
start_env_migration_progress
update_env_migration_progress
stop_env_migration_progress
start_state_export
update_state_export_progress
stop_state_export
start_state_import
update_state_import_progress
stop_state_import
show_environment_difference_summary
show_model_difference_summary
plan
log_test_results
show_sql
log_status_update
log_models_updated_during_restatement
log_destructive_change
log_additive_change
log_success
loading_start
loading_stop
show_table_diff_details
start_table_diff_progress
start_table_diff_model_progress
update_table_diff_progress
stop_table_diff_progress
show_table_diff_summary
show_schema_diff
show_row_diff
show_table_diff
print_environments
show_intervals
print_connection_config
show_linter_violations
Console
INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD
class MarkdownConsole(CaptureTerminalConsole):
3289class MarkdownConsole(CaptureTerminalConsole):
3290    """
3291    A console that outputs markdown. Currently this is only configured for non-interactive use so for use cases
3292    where you want to display a plan or test results in markdown.
3293    """
3294
3295    CHECK_MARK = ""
3296    AUDIT_PASS_MARK = "passed "
3297    GREEN_AUDIT_PASS_MARK = AUDIT_PASS_MARK
3298    AUDIT_FAIL_MARK = "failed "
3299    AUDIT_PADDING = 7
3300
3301    def __init__(self, **kwargs: t.Any) -> None:
3302        self.alert_block_max_content_length = int(kwargs.pop("alert_block_max_content_length", 500))
3303        self.alert_block_collapsible_threshold = int(
3304            kwargs.pop("alert_block_collapsible_threshold", 200)
3305        )
3306
3307        # capture_only = True: capture but dont print to console
3308        # capture_only = False: capture and also print to console
3309        self.warning_capture_only = kwargs.pop("warning_capture_only", False)
3310        self.error_capture_only = kwargs.pop("error_capture_only", False)
3311
3312        super().__init__(
3313            **{**kwargs, "console": RichConsole(no_color=True, width=kwargs.pop("width", None))}
3314        )
3315
3316    def show_environment_difference_summary(
3317        self,
3318        context_diff: ContextDiff,
3319        no_diff: bool = True,
3320    ) -> None:
3321        """Shows a summary of the environment differences.
3322
3323        Args:
3324            context_diff: The context diff to use to print the summary.
3325            no_diff: Hide the actual environment statements differences.
3326        """
3327        if context_diff.is_new_environment:
3328            msg = (
3329                f"\n**`{context_diff.environment}` environment will be initialized**"
3330                if not context_diff.create_from_env_exists
3331                else f"\n**New environment `{context_diff.environment}` will be created from `{context_diff.create_from}`**"
3332            )
3333            self._print(msg)
3334            if not context_diff.has_snapshot_changes:
3335                return
3336
3337        if not context_diff.has_changes:
3338            self._print(
3339                f"\n**No changes to plan: project files match the `{context_diff.environment}` environment**\n"
3340            )
3341            return
3342
3343        self._print(f"\n**Summary of differences from `{context_diff.environment}`:**")
3344
3345        if context_diff.has_requirement_changes:
3346            self._print(f"\nRequirements:\n{context_diff.requirements_diff()}")
3347
3348        if context_diff.has_environment_statements_changes and not no_diff:
3349            self._print("\nEnvironment statements:\n")
3350            for _, diff in context_diff.environment_statements_diff(
3351                include_python_env=not context_diff.is_new_environment
3352            ):
3353                self._print(diff)
3354
3355    def show_model_difference_summary(
3356        self,
3357        context_diff: ContextDiff,
3358        environment_naming_info: EnvironmentNamingInfo,
3359        default_catalog: t.Optional[str],
3360        no_diff: bool = True,
3361    ) -> None:
3362        """Shows a summary of the model differences.
3363
3364        Args:
3365            context_diff: The context diff to use to print the summary.
3366            environment_naming_info: The environment naming info to reference when printing model names
3367            default_catalog: The default catalog to reference when deciding to remove catalog from display names
3368            no_diff: Hide the actual SQL differences.
3369        """
3370        added_snapshots = {context_diff.snapshots[s_id] for s_id in context_diff.added}
3371        if added_snapshots:
3372            self._print("\n**Added Models:**")
3373            self._print_models_with_threshold(
3374                environment_naming_info, {s for s in added_snapshots if s.is_model}, default_catalog
3375            )
3376
3377        added_snapshot_audits = {s for s in added_snapshots if s.is_audit}
3378        if added_snapshot_audits:
3379            self._print("\n**Added Standalone Audits:**")
3380            for snapshot in sorted(added_snapshot_audits):
3381                self._print(
3382                    f"- `{snapshot.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`"
3383                )
3384
3385        removed_snapshot_table_infos = set(context_diff.removed_snapshots.values())
3386        if removed_snapshot_table_infos:
3387            self._print("\n**Removed Models:**")
3388            self._print_models_with_threshold(
3389                environment_naming_info,
3390                {s for s in removed_snapshot_table_infos if s.is_model},
3391                default_catalog,
3392            )
3393
3394        removed_audit_snapshot_table_infos = {s for s in removed_snapshot_table_infos if s.is_audit}
3395        if removed_audit_snapshot_table_infos:
3396            self._print("\n**Removed Standalone Audits:**")
3397            for snapshot_table_info in sorted(removed_audit_snapshot_table_infos):
3398                self._print(
3399                    f"- `{snapshot_table_info.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`"
3400                )
3401
3402        modified_snapshots = {
3403            current_snapshot for current_snapshot, _ in context_diff.modified_snapshots.values()
3404        }
3405        if modified_snapshots:
3406            self._print_modified_models(
3407                context_diff, modified_snapshots, environment_naming_info, default_catalog, no_diff
3408            )
3409
3410    def _print_models_with_threshold(
3411        self,
3412        environment_naming_info: EnvironmentNamingInfo,
3413        snapshot_table_infos: t.Set[SnapshotInfoLike],
3414        default_catalog: t.Optional[str] = None,
3415    ) -> None:
3416        models = sorted(snapshot_table_infos)
3417        list_length = len(models)
3418        if (
3419            self.verbosity < Verbosity.VERY_VERBOSE
3420            and list_length > self.INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD
3421        ):
3422            self._print(
3423                f"- `{models[0].display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`"
3424            )
3425            self._print(f"- `.... {list_length - 2} more ....`\n")
3426            self._print(
3427                f"- `{models[-1].display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`"
3428            )
3429        else:
3430            for snapshot_table_info in models:
3431                category_str = SNAPSHOT_CHANGE_CATEGORY_STR[snapshot_table_info.change_category]
3432                self._print(
3433                    f"- `{snapshot_table_info.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}` ({category_str})"
3434                )
3435
3436    def _print_modified_models(
3437        self,
3438        context_diff: ContextDiff,
3439        modified_snapshots: t.Set[Snapshot],
3440        environment_naming_info: EnvironmentNamingInfo,
3441        default_catalog: t.Optional[str] = None,
3442        no_diff: bool = True,
3443    ) -> None:
3444        directly_modified = []
3445        indirectly_modified: t.List[Snapshot] = []
3446        metadata_modified = []
3447        for snapshot in modified_snapshots:
3448            if context_diff.directly_modified(snapshot.name):
3449                directly_modified.append(snapshot)
3450            elif context_diff.indirectly_modified(snapshot.name):
3451                indirectly_modified.append(snapshot)
3452            elif context_diff.metadata_updated(snapshot.name):
3453                metadata_modified.append(snapshot)
3454        if directly_modified:
3455            self._print("\n**Directly Modified:**")
3456            for snapshot in sorted(directly_modified):
3457                category_str = SNAPSHOT_CHANGE_CATEGORY_STR[snapshot.change_category]
3458                self._print(
3459                    f"* `{snapshot.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}` ({category_str})"
3460                )
3461
3462                indirectly_modified_children = sorted(
3463                    [s for s in indirectly_modified if snapshot.snapshot_id in s.parents]
3464                )
3465
3466                if not no_diff:
3467                    diff_text = context_diff.text_diff(snapshot.name)
3468                    # sometimes there is no text_diff, like on a seed model where the data has been updated
3469                    if diff_text:
3470                        diff_text = f"\n```diff\n{diff_text}\n```"
3471                        # these are part of a Markdown list, so indent them by 2 spaces to relate them to the current list item
3472                        diff_text_indented = "\n".join(
3473                            [f"  {line}" for line in diff_text.splitlines()]
3474                        )
3475                        self._print(diff_text_indented)
3476                    else:
3477                        if indirectly_modified_children:
3478                            self._print("\n")
3479
3480                if indirectly_modified_children:
3481                    self._print("  Indirectly Modified Children:")
3482                    for child_snapshot in indirectly_modified_children:
3483                        child_category_str = SNAPSHOT_CHANGE_CATEGORY_STR[
3484                            child_snapshot.change_category
3485                        ]
3486                        self._print(
3487                            f"    - `{child_snapshot.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}` ({child_category_str})"
3488                        )
3489                    self._print("\n")
3490
3491        if indirectly_modified:
3492            self._print("\n**Indirectly Modified:**")
3493            self._print_models_with_threshold(
3494                environment_naming_info, set(indirectly_modified), default_catalog
3495            )
3496        if metadata_modified:
3497            self._print("\n**Metadata Updated:**")
3498            for snapshot in sorted(metadata_modified):
3499                self._print(
3500                    f"- `{snapshot.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`"
3501                )
3502
3503    def _show_missing_dates(self, plan: Plan, default_catalog: t.Optional[str]) -> None:
3504        """Displays the models with missing dates."""
3505        missing_intervals = plan.missing_intervals
3506        if not missing_intervals:
3507            return
3508        self._print("\n**Models needing backfill:**")
3509        snapshots = []
3510        for missing in missing_intervals:
3511            snapshot = plan.context_diff.snapshots[missing.snapshot_id]
3512            if not snapshot.is_model:
3513                continue
3514
3515            preview_modifier = ""
3516            if not plan.deployability_index.is_deployable(snapshot):
3517                preview_modifier = " (**preview**)"
3518
3519            display_name = snapshot.display_name(
3520                plan.environment_naming_info,
3521                default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
3522                dialect=self.dialect,
3523            )
3524            snapshots.append(
3525                f"* `{display_name}`: \\[{_format_missing_intervals(snapshot, missing)}]{preview_modifier}"
3526            )
3527
3528        length = len(snapshots)
3529        if (
3530            self.verbosity < Verbosity.VERY_VERBOSE
3531            and length > self.INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD
3532        ):
3533            self._print(snapshots[0])
3534            self._print(f"- `.... {length - 2} more ....`\n")
3535            self._print(snapshots[-1])
3536        else:
3537            for snap in snapshots:
3538                self._print(snap)
3539
3540    def _show_categorized_snapshots(self, plan: Plan, default_catalog: t.Optional[str]) -> None:
3541        context_diff = plan.context_diff
3542        for snapshot in plan.categorized:
3543            if context_diff.directly_modified(snapshot.name):
3544                category_str = SNAPSHOT_CHANGE_CATEGORY_STR[snapshot.change_category]
3545                tree = Tree(
3546                    f"[bold][direct]Directly Modified: {snapshot.display_name(plan.environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)} ({category_str})"
3547                )
3548                indirect_tree = None
3549                for child_sid in sorted(plan.indirectly_modified.get(snapshot.snapshot_id, set())):
3550                    child_snapshot = context_diff.snapshots[child_sid]
3551                    if not indirect_tree:
3552                        indirect_tree = Tree("[indirect]Indirectly Modified Children:")
3553                        tree.add(indirect_tree)
3554                    child_category_str = SNAPSHOT_CHANGE_CATEGORY_STR[
3555                        child_snapshot.change_category
3556                    ]
3557                    indirect_tree.add(
3558                        f"[indirect]{child_snapshot.display_name(plan.environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)} ({child_category_str})"
3559                    )
3560                if indirect_tree:
3561                    indirect_tree = self._limit_model_names(indirect_tree, self.verbosity)
3562            elif context_diff.metadata_updated(snapshot.name):
3563                tree = Tree(
3564                    f"[bold][metadata]Metadata Updated: {snapshot.display_name(plan.environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}"
3565                )
3566            else:
3567                continue
3568
3569            self._print(f"```diff\n{context_diff.text_diff(snapshot.name)}\n```\n")
3570            self._print("```\n")
3571            self._print(tree)
3572            self._print("\n```")
3573
3574    def stop_evaluation_progress(self, success: bool = True) -> None:
3575        super().stop_evaluation_progress(success)
3576        self._print("\n")
3577
3578    def stop_creation_progress(self, success: bool = True) -> None:
3579        super().stop_creation_progress(success)
3580        self._print("\n")
3581
3582    def stop_promotion_progress(self, success: bool = True) -> None:
3583        super().stop_promotion_progress(success)
3584        self._print("\n")
3585
3586    def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None:
3587        super().log_warning(short_message, long_message, print=not self.warning_capture_only)
3588
3589    def log_error(self, message: str) -> None:
3590        super().log_error(message, print=not self.error_capture_only)
3591
3592    def log_success(self, message: str) -> None:
3593        self._print(message)
3594
3595    def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
3596        # We don't log the test results if no tests were ran
3597        if not result.testsRun:
3598            return
3599
3600        message = f"Ran `{result.testsRun}` Tests Against `{target_dialect}`"
3601
3602        if result.wasSuccessful():
3603            self._print(f"**Successfully {message}**\n\n")
3604        else:
3605            self._print("```")
3606            self._log_test_details(result, unittest_char_separator=False)
3607            self._print("```\n\n")
3608
3609            fail_and_error_tests = result.get_fail_and_error_tests()
3610            self._print(f"**{message}**\n")
3611            self._print(f"**Failed tests ({len(fail_and_error_tests)}):**")
3612            for test in fail_and_error_tests:
3613                if isinstance(test, ModelTest):
3614                    self._print(f" • `{test.model.name}`::`{test.test_name}`\n\n")
3615
3616    def log_skipped_models(self, snapshot_names: t.Set[str]) -> None:
3617        if snapshot_names:
3618            self._print(f"**Skipped models**")
3619            for snapshot_name in snapshot_names:
3620                self._print(f"* `{snapshot_name}`")
3621            self._print("")
3622
3623    def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None:
3624        if errors:
3625            self._print("**Failed models**")
3626
3627            error_messages = _format_node_errors(errors)
3628
3629            for node_name, msg in error_messages.items():
3630                self._print(f"* `{node_name}`\n")
3631                self._print("  ```")
3632                self._print(msg)
3633                self._print("  ```")
3634
3635            self._print("")
3636
3637    def show_linter_violations(
3638        self, violations: t.List[RuleViolation], model: Model, is_error: bool = False
3639    ) -> None:
3640        severity = "**errors**" if is_error else "warnings"
3641        violations_msg = "\n".join(f" - {violation}" for violation in violations)
3642        msg = f"\nLinter {severity} for `{model._path}`:\n{violations_msg}\n"
3643
3644        self._print(msg)
3645        if is_error:
3646            self._errors.append(msg)
3647        else:
3648            self._warnings.append(msg)
3649
3650    @property
3651    def captured_warnings(self) -> str:
3652        return self._render_alert_block("WARNING", self._warnings)
3653
3654    @property
3655    def captured_errors(self) -> str:
3656        return self._render_alert_block("CAUTION", self._errors)
3657
3658    def _render_alert_block(self, block_type: str, items: t.List[str]) -> str:
3659        # GitHub Markdown alert syntax, https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts
3660        if items:
3661            item_contents = ""
3662            list_indicator = "- " if len(items) > 1 else ""
3663
3664            for item in items:
3665                item = item.replace("\n", "\n> ")
3666                item_contents += f">\n> {list_indicator}{item}\n"
3667
3668                if len(item_contents) > self.alert_block_max_content_length:
3669                    truncation_msg = (
3670                        "...\n>\n> Truncated. Please check the console for full information.\n"
3671                    )
3672                    item_contents = item_contents[
3673                        0 : self.alert_block_max_content_length - len(truncation_msg)
3674                    ]
3675                    item_contents += truncation_msg
3676                    break
3677
3678            if len(item_contents) > self.alert_block_collapsible_threshold:
3679                item_contents = f"> <details>\n{item_contents}> </details>"
3680
3681            return f"> [!{block_type}]\n{item_contents}\n"
3682
3683        return ""
3684
3685    def _print(self, value: t.Any, **kwargs: t.Any) -> None:
3686        self.console.print(value, **kwargs)
3687        with self.console.capture() as capture:
3688            self.console.print(value, **kwargs)
3689        self._captured_outputs.append(capture.get())

A console that outputs markdown. Currently this is only configured for non-interactive use so for use cases where you want to display a plan or test results in markdown.

MarkdownConsole(**kwargs: Any)
3301    def __init__(self, **kwargs: t.Any) -> None:
3302        self.alert_block_max_content_length = int(kwargs.pop("alert_block_max_content_length", 500))
3303        self.alert_block_collapsible_threshold = int(
3304            kwargs.pop("alert_block_collapsible_threshold", 200)
3305        )
3306
3307        # capture_only = True: capture but dont print to console
3308        # capture_only = False: capture and also print to console
3309        self.warning_capture_only = kwargs.pop("warning_capture_only", False)
3310        self.error_capture_only = kwargs.pop("error_capture_only", False)
3311
3312        super().__init__(
3313            **{**kwargs, "console": RichConsole(no_color=True, width=kwargs.pop("width", None))}
3314        )
CHECK_MARK = ''
AUDIT_PASS_MARK = 'passed '
GREEN_AUDIT_PASS_MARK = 'passed '
AUDIT_FAIL_MARK = 'failed '
AUDIT_PADDING = 7
alert_block_max_content_length
alert_block_collapsible_threshold
warning_capture_only
error_capture_only
def show_environment_difference_summary( self, context_diff: sqlmesh.core.context_diff.ContextDiff, no_diff: bool = True) -> None:
3316    def show_environment_difference_summary(
3317        self,
3318        context_diff: ContextDiff,
3319        no_diff: bool = True,
3320    ) -> None:
3321        """Shows a summary of the environment differences.
3322
3323        Args:
3324            context_diff: The context diff to use to print the summary.
3325            no_diff: Hide the actual environment statements differences.
3326        """
3327        if context_diff.is_new_environment:
3328            msg = (
3329                f"\n**`{context_diff.environment}` environment will be initialized**"
3330                if not context_diff.create_from_env_exists
3331                else f"\n**New environment `{context_diff.environment}` will be created from `{context_diff.create_from}`**"
3332            )
3333            self._print(msg)
3334            if not context_diff.has_snapshot_changes:
3335                return
3336
3337        if not context_diff.has_changes:
3338            self._print(
3339                f"\n**No changes to plan: project files match the `{context_diff.environment}` environment**\n"
3340            )
3341            return
3342
3343        self._print(f"\n**Summary of differences from `{context_diff.environment}`:**")
3344
3345        if context_diff.has_requirement_changes:
3346            self._print(f"\nRequirements:\n{context_diff.requirements_diff()}")
3347
3348        if context_diff.has_environment_statements_changes and not no_diff:
3349            self._print("\nEnvironment statements:\n")
3350            for _, diff in context_diff.environment_statements_diff(
3351                include_python_env=not context_diff.is_new_environment
3352            ):
3353                self._print(diff)

Shows a summary of the environment differences.

Arguments:
  • context_diff: The context diff to use to print the summary.
  • no_diff: Hide the actual environment statements differences.
def show_model_difference_summary( self, context_diff: sqlmesh.core.context_diff.ContextDiff, environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str], no_diff: bool = True) -> None:
3355    def show_model_difference_summary(
3356        self,
3357        context_diff: ContextDiff,
3358        environment_naming_info: EnvironmentNamingInfo,
3359        default_catalog: t.Optional[str],
3360        no_diff: bool = True,
3361    ) -> None:
3362        """Shows a summary of the model differences.
3363
3364        Args:
3365            context_diff: The context diff to use to print the summary.
3366            environment_naming_info: The environment naming info to reference when printing model names
3367            default_catalog: The default catalog to reference when deciding to remove catalog from display names
3368            no_diff: Hide the actual SQL differences.
3369        """
3370        added_snapshots = {context_diff.snapshots[s_id] for s_id in context_diff.added}
3371        if added_snapshots:
3372            self._print("\n**Added Models:**")
3373            self._print_models_with_threshold(
3374                environment_naming_info, {s for s in added_snapshots if s.is_model}, default_catalog
3375            )
3376
3377        added_snapshot_audits = {s for s in added_snapshots if s.is_audit}
3378        if added_snapshot_audits:
3379            self._print("\n**Added Standalone Audits:**")
3380            for snapshot in sorted(added_snapshot_audits):
3381                self._print(
3382                    f"- `{snapshot.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`"
3383                )
3384
3385        removed_snapshot_table_infos = set(context_diff.removed_snapshots.values())
3386        if removed_snapshot_table_infos:
3387            self._print("\n**Removed Models:**")
3388            self._print_models_with_threshold(
3389                environment_naming_info,
3390                {s for s in removed_snapshot_table_infos if s.is_model},
3391                default_catalog,
3392            )
3393
3394        removed_audit_snapshot_table_infos = {s for s in removed_snapshot_table_infos if s.is_audit}
3395        if removed_audit_snapshot_table_infos:
3396            self._print("\n**Removed Standalone Audits:**")
3397            for snapshot_table_info in sorted(removed_audit_snapshot_table_infos):
3398                self._print(
3399                    f"- `{snapshot_table_info.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`"
3400                )
3401
3402        modified_snapshots = {
3403            current_snapshot for current_snapshot, _ in context_diff.modified_snapshots.values()
3404        }
3405        if modified_snapshots:
3406            self._print_modified_models(
3407                context_diff, modified_snapshots, environment_naming_info, default_catalog, no_diff
3408            )

Shows a summary of the model differences.

Arguments:
  • context_diff: The context diff to use to print the summary.
  • environment_naming_info: The environment naming info to reference when printing model names
  • default_catalog: The default catalog to reference when deciding to remove catalog from display names
  • no_diff: Hide the actual SQL differences.
def stop_evaluation_progress(self, success: bool = True) -> None:
3574    def stop_evaluation_progress(self, success: bool = True) -> None:
3575        super().stop_evaluation_progress(success)
3576        self._print("\n")

Stop the snapshot evaluation progress.

def stop_creation_progress(self, success: bool = True) -> None:
3578    def stop_creation_progress(self, success: bool = True) -> None:
3579        super().stop_creation_progress(success)
3580        self._print("\n")

Stop the snapshot creation progress.

def stop_promotion_progress(self, success: bool = True) -> None:
3582    def stop_promotion_progress(self, success: bool = True) -> None:
3583        super().stop_promotion_progress(success)
3584        self._print("\n")

Stop the snapshot promotion progress.

def log_warning(self, short_message: str, long_message: Optional[str] = None) -> None:
3586    def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None:
3587        super().log_warning(short_message, long_message, print=not self.warning_capture_only)

Display warning info to the user.

Arguments:
  • short_message: The warning message to print to console.
  • long_message: The warning message to log to file. If not provided, short_message is used.
def log_error(self, message: str) -> None:
3589    def log_error(self, message: str) -> None:
3590        super().log_error(message, print=not self.error_capture_only)

Display error info to the user.

def log_success(self, message: str) -> None:
3592    def log_success(self, message: str) -> None:
3593        self._print(message)

Display a general successful message to the user.

def log_test_results( self, result: sqlmesh.core.test.result.ModelTextTestResult, target_dialect: str) -> None:
3595    def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
3596        # We don't log the test results if no tests were ran
3597        if not result.testsRun:
3598            return
3599
3600        message = f"Ran `{result.testsRun}` Tests Against `{target_dialect}`"
3601
3602        if result.wasSuccessful():
3603            self._print(f"**Successfully {message}**\n\n")
3604        else:
3605            self._print("```")
3606            self._log_test_details(result, unittest_char_separator=False)
3607            self._print("```\n\n")
3608
3609            fail_and_error_tests = result.get_fail_and_error_tests()
3610            self._print(f"**{message}**\n")
3611            self._print(f"**Failed tests ({len(fail_and_error_tests)}):**")
3612            for test in fail_and_error_tests:
3613                if isinstance(test, ModelTest):
3614                    self._print(f" • `{test.model.name}`::`{test.test_name}`\n\n")

Display the test result and output.

Arguments:
  • result: The unittest test result that contains metrics like num success, fails, ect.
  • target_dialect: The dialect that tests were run against. Assumes all tests run against the same dialect.
def log_skipped_models(self, snapshot_names: Set[str]) -> None:
3616    def log_skipped_models(self, snapshot_names: t.Set[str]) -> None:
3617        if snapshot_names:
3618            self._print(f"**Skipped models**")
3619            for snapshot_name in snapshot_names:
3620                self._print(f"* `{snapshot_name}`")
3621            self._print("")

Display list of models skipped during evaluation to the user.

def log_failed_models( self, errors: List[sqlmesh.utils.concurrency.NodeExecutionFailedError]) -> None:
3623    def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None:
3624        if errors:
3625            self._print("**Failed models**")
3626
3627            error_messages = _format_node_errors(errors)
3628
3629            for node_name, msg in error_messages.items():
3630                self._print(f"* `{node_name}`\n")
3631                self._print("  ```")
3632                self._print(msg)
3633                self._print("  ```")
3634
3635            self._print("")

Display list of models that failed during evaluation to the user.

3637    def show_linter_violations(
3638        self, violations: t.List[RuleViolation], model: Model, is_error: bool = False
3639    ) -> None:
3640        severity = "**errors**" if is_error else "warnings"
3641        violations_msg = "\n".join(f" - {violation}" for violation in violations)
3642        msg = f"\nLinter {severity} for `{model._path}`:\n{violations_msg}\n"
3643
3644        self._print(msg)
3645        if is_error:
3646            self._errors.append(msg)
3647        else:
3648            self._warnings.append(msg)

Prints all linter violations depending on their severity

captured_warnings: str
3650    @property
3651    def captured_warnings(self) -> str:
3652        return self._render_alert_block("WARNING", self._warnings)
captured_errors: str
3654    @property
3655    def captured_errors(self) -> str:
3656        return self._render_alert_block("CAUTION", self._errors)
Inherited Members
CaptureTerminalConsole
captured_output
consume_captured_output
consume_captured_warnings
consume_captured_errors
TerminalConsole
TABLE_DIFF_SOURCE_BLUE
TABLE_DIFF_TARGET_GREEN
console
evaluation_progress_live
evaluation_total_progress
evaluation_total_task
evaluation_model_progress
evaluation_model_tasks
evaluation_model_batch_sizes
evaluation_column_widths
environment_naming_info
default_catalog
creation_progress
creation_column_widths
creation_task
promotion_progress
promotion_column_widths
promotion_task
migration_progress
migration_task
env_migration_progress
env_migration_task
loading_status
state_export_progress
state_export_version_task
state_export_snapshot_task
state_export_environment_task
state_import_progress
state_import_version_task
state_import_snapshot_task
state_import_environment_task
table_diff_progress
table_diff_model_progress
table_diff_model_tasks
table_diff_progress_live
signal_progress_logged
signal_status_tree
verbosity
dialect
ignore_warnings
start_plan_evaluation
stop_plan_evaluation
start_evaluation_progress
start_snapshot_evaluation_progress
update_snapshot_evaluation_progress
start_signal_progress
update_signal_progress
stop_signal_progress
start_creation_progress
update_creation_progress
start_cleanup
update_cleanup_progress
stop_cleanup
start_destroy
stop_destroy
start_promotion_progress
update_promotion_progress
start_snapshot_migration_progress
update_snapshot_migration_progress
log_migration_status
stop_snapshot_migration_progress
start_env_migration_progress
update_env_migration_progress
stop_env_migration_progress
start_state_export
update_state_export_progress
stop_state_export
start_state_import
update_state_import_progress
stop_state_import
plan
show_sql
log_status_update
log_models_updated_during_restatement
log_destructive_change
log_additive_change
loading_start
loading_stop
show_table_diff_details
start_table_diff_progress
start_table_diff_model_progress
update_table_diff_progress
stop_table_diff_progress
show_table_diff_summary
show_schema_diff
show_row_diff
show_table_diff
print_environments
show_intervals
print_connection_config
Console
INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD
class DatabricksMagicConsole(CaptureTerminalConsole):
3692class DatabricksMagicConsole(CaptureTerminalConsole):
3693    """
3694    Note: Databricks Magic Console currently does not support progress bars while a plan is being applied. The
3695    NotebookMagicConsole does support progress bars, but they will time out after 5 minutes of execution
3696    and it makes it difficult to see the progress of the plan.
3697    """
3698
3699    def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
3700        super().__init__(*args, **kwargs)
3701        self.evaluation_batch_progress: t.Dict[SnapshotId, t.Tuple[str, int]] = {}
3702        self.promotion_status: t.Tuple[int, int] = (0, 0)
3703        self.model_creation_status: t.Tuple[int, int] = (0, 0)
3704        self.migration_status: t.Tuple[int, int] = (0, 0)
3705
3706    def _print(self, value: t.Any, **kwargs: t.Any) -> None:
3707        super()._print(value, **kwargs)
3708        for captured_output in self._captured_outputs:
3709            print(captured_output)
3710        self.consume_captured_output()
3711
3712    def _prompt(self, message: str, **kwargs: t.Any) -> t.Any:
3713        self._print(message)
3714        return super()._prompt("", **kwargs)
3715
3716    def _confirm(self, message: str, **kwargs: t.Any) -> bool:
3717        message = f"{message} [y/n]"
3718        self._print(message)
3719        return super()._confirm("", **kwargs)
3720
3721    def start_evaluation_progress(
3722        self,
3723        batched_intervals: t.Dict[Snapshot, Intervals],
3724        environment_naming_info: EnvironmentNamingInfo,
3725        default_catalog: t.Optional[str],
3726        audit_only: bool = False,
3727    ) -> None:
3728        self.evaluation_model_batch_sizes = {
3729            snapshot: len(intervals) for snapshot, intervals in batched_intervals.items()
3730        }
3731        self.evaluation_environment_naming_info = environment_naming_info
3732        self.default_catalog = default_catalog
3733
3734    def start_snapshot_evaluation_progress(
3735        self, snapshot: Snapshot, audit_only: bool = False
3736    ) -> None:
3737        if not self.evaluation_batch_progress.get(snapshot.snapshot_id):
3738            display_name = snapshot.display_name(
3739                self.evaluation_environment_naming_info,
3740                self.default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
3741                dialect=self.dialect,
3742            )
3743            self.evaluation_batch_progress[snapshot.snapshot_id] = (display_name, 0)
3744            print(
3745                f"Starting '{display_name}', Total batches: {self.evaluation_model_batch_sizes[snapshot]}"
3746            )
3747
3748    def update_snapshot_evaluation_progress(
3749        self,
3750        snapshot: Snapshot,
3751        interval: Interval,
3752        batch_idx: int,
3753        duration_ms: t.Optional[int],
3754        num_audits_passed: int,
3755        num_audits_failed: int,
3756        audit_only: bool = False,
3757        execution_stats: t.Optional[QueryExecutionStats] = None,
3758        auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None,
3759    ) -> None:
3760        view_name, loaded_batches = self.evaluation_batch_progress[snapshot.snapshot_id]
3761
3762        if audit_only:
3763            print(f"Completed Auditing {view_name}")
3764            return
3765
3766        total_batches = self.evaluation_model_batch_sizes[snapshot]
3767
3768        loaded_batches += 1
3769        self.evaluation_batch_progress[snapshot.snapshot_id] = (view_name, loaded_batches)
3770
3771        finished_loading = loaded_batches == total_batches
3772        status = "Loaded" if finished_loading else "Loading"
3773        print(f"{status} '{view_name}', Completed Batches: {loaded_batches}/{total_batches}")
3774        if finished_loading:
3775            total_finished_loading = len(
3776                [
3777                    s
3778                    for s, total in self.evaluation_model_batch_sizes.items()
3779                    if self.evaluation_batch_progress.get(s.snapshot_id, (None, -1))[1] == total
3780                ]
3781            )
3782            total = len(self.evaluation_batch_progress)
3783            print(f"Completed Loading {total_finished_loading}/{total} Models")
3784
3785    def stop_evaluation_progress(self, success: bool = True) -> None:
3786        self.evaluation_batch_progress = {}
3787        super().stop_evaluation_progress(success)
3788        print(f"Loading {'succeeded' if success else 'failed'}")
3789
3790    def start_creation_progress(
3791        self,
3792        snapshots: t.List[Snapshot],
3793        environment_naming_info: EnvironmentNamingInfo,
3794        default_catalog: t.Optional[str],
3795    ) -> None:
3796        """Indicates that a new creation progress has begun."""
3797        self.model_creation_status = (0, len(snapshots))
3798        print("Starting Creating New Model Versions")
3799
3800    def update_creation_progress(self, snapshot: SnapshotInfoLike) -> None:
3801        """Update the snapshot creation progress."""
3802        num_creations, total_creations = self.model_creation_status
3803        num_creations += 1
3804        self.model_creation_status = (num_creations, total_creations)
3805        if num_creations % 5 == 0:
3806            print(f"Created New Model Versions: {num_creations}/{total_creations}")
3807
3808    def stop_creation_progress(self, success: bool = True) -> None:
3809        """Stop the snapshot creation progress."""
3810        self.model_creation_status = (0, 0)
3811        print(f"New Model Creation {'succeeded' if success else 'failed'}")
3812
3813    def start_promotion_progress(
3814        self,
3815        snapshots: t.List[SnapshotTableInfo],
3816        environment_naming_info: EnvironmentNamingInfo,
3817        default_catalog: t.Optional[str],
3818    ) -> None:
3819        """Indicates that a new snapshot promotion progress has begun."""
3820        self.promotion_status = (0, len(snapshots))
3821        print(f"Virtually Updating '{environment_naming_info.name}'")
3822
3823    def update_promotion_progress(self, snapshot: SnapshotInfoLike, promoted: bool) -> None:
3824        """Update the snapshot promotion progress."""
3825        num_promotions, total_promotions = self.promotion_status
3826        num_promotions += 1
3827        self.promotion_status = (num_promotions, total_promotions)
3828        if num_promotions % 5 == 0:
3829            print(f"Virtually Updated {num_promotions}/{total_promotions}")
3830
3831    def stop_promotion_progress(self, success: bool = True) -> None:
3832        """Stop the snapshot promotion progress."""
3833        self.promotion_status = (0, 0)
3834        print(f"Virtual Update {'succeeded' if success else 'failed'}")
3835
3836    def start_snapshot_migration_progress(self, total_tasks: int) -> None:
3837        """Indicates that a new migration progress has begun."""
3838        self.migration_status = (0, total_tasks)
3839        print("Starting Migration")
3840
3841    def update_snapshot_migration_progress(self, num_tasks: int) -> None:
3842        """Update the migration progress."""
3843        num_migrations, total_migrations = self.migration_status
3844        num_migrations += num_tasks
3845        self.migration_status = (num_migrations, total_migrations)
3846        if num_migrations % 5 == 0:
3847            print(f"Migration Updated {num_migrations}/{total_migrations}")
3848
3849    def log_migration_status(self, success: bool = True) -> None:
3850        """Log the migration status."""
3851        print(f"Migration {'succeeded' if success else 'failed'}")
3852
3853    def stop_snapshot_migration_progress(self, success: bool = True) -> None:
3854        """Stop the migration progress."""
3855        self.migration_status = (0, 0)
3856        print(f"Snapshot migration {'succeeded' if success else 'failed'}")
3857
3858    def start_env_migration_progress(self, total_tasks: int) -> None:
3859        """Indicates that a new migration progress has begun."""
3860        self.env_migration_status = (0, total_tasks)
3861        print("Starting Environment migration")
3862
3863    def update_env_migration_progress(self, num_tasks: int) -> None:
3864        """Update the migration progress."""
3865        num_migrations, total_migrations = self.env_migration_status
3866        num_migrations += num_tasks
3867        self.env_migration_status = (num_migrations, total_migrations)
3868        if num_migrations % 5 == 0:
3869            print(f"Environment migration Updated {num_migrations}/{total_migrations}")
3870
3871    def stop_env_migration_progress(self, success: bool = True) -> None:
3872        """Stop the migration progress."""
3873        self.env_migration_status = (0, 0)
3874        print(f"Environment migration {'succeeded' if success else 'failed'}")

Note: Databricks Magic Console currently does not support progress bars while a plan is being applied. The NotebookMagicConsole does support progress bars, but they will time out after 5 minutes of execution and it makes it difficult to see the progress of the plan.

DatabricksMagicConsole(*args: Any, **kwargs: Any)
3699    def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
3700        super().__init__(*args, **kwargs)
3701        self.evaluation_batch_progress: t.Dict[SnapshotId, t.Tuple[str, int]] = {}
3702        self.promotion_status: t.Tuple[int, int] = (0, 0)
3703        self.model_creation_status: t.Tuple[int, int] = (0, 0)
3704        self.migration_status: t.Tuple[int, int] = (0, 0)
evaluation_batch_progress: Dict[sqlmesh.core.snapshot.definition.SnapshotId, Tuple[str, int]]
promotion_status: Tuple[int, int]
model_creation_status: Tuple[int, int]
migration_status: Tuple[int, int]
def start_evaluation_progress( self, batched_intervals: Dict[sqlmesh.core.snapshot.definition.Snapshot, List[Tuple[int, int]]], environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str], audit_only: bool = False) -> None:
3721    def start_evaluation_progress(
3722        self,
3723        batched_intervals: t.Dict[Snapshot, Intervals],
3724        environment_naming_info: EnvironmentNamingInfo,
3725        default_catalog: t.Optional[str],
3726        audit_only: bool = False,
3727    ) -> None:
3728        self.evaluation_model_batch_sizes = {
3729            snapshot: len(intervals) for snapshot, intervals in batched_intervals.items()
3730        }
3731        self.evaluation_environment_naming_info = environment_naming_info
3732        self.default_catalog = default_catalog

Indicates that a new snapshot evaluation/auditing progress has begun.

def start_snapshot_evaluation_progress( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, audit_only: bool = False) -> None:
3734    def start_snapshot_evaluation_progress(
3735        self, snapshot: Snapshot, audit_only: bool = False
3736    ) -> None:
3737        if not self.evaluation_batch_progress.get(snapshot.snapshot_id):
3738            display_name = snapshot.display_name(
3739                self.evaluation_environment_naming_info,
3740                self.default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
3741                dialect=self.dialect,
3742            )
3743            self.evaluation_batch_progress[snapshot.snapshot_id] = (display_name, 0)
3744            print(
3745                f"Starting '{display_name}', Total batches: {self.evaluation_model_batch_sizes[snapshot]}"
3746            )

Starts the snapshot evaluation progress.

def update_snapshot_evaluation_progress( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, interval: Tuple[int, int], batch_idx: int, duration_ms: Optional[int], num_audits_passed: int, num_audits_failed: int, audit_only: bool = False, execution_stats: Optional[sqlmesh.core.snapshot.execution_tracker.QueryExecutionStats] = None, auto_restatement_triggers: Optional[List[sqlmesh.core.snapshot.definition.SnapshotId]] = None) -> None:
3748    def update_snapshot_evaluation_progress(
3749        self,
3750        snapshot: Snapshot,
3751        interval: Interval,
3752        batch_idx: int,
3753        duration_ms: t.Optional[int],
3754        num_audits_passed: int,
3755        num_audits_failed: int,
3756        audit_only: bool = False,
3757        execution_stats: t.Optional[QueryExecutionStats] = None,
3758        auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None,
3759    ) -> None:
3760        view_name, loaded_batches = self.evaluation_batch_progress[snapshot.snapshot_id]
3761
3762        if audit_only:
3763            print(f"Completed Auditing {view_name}")
3764            return
3765
3766        total_batches = self.evaluation_model_batch_sizes[snapshot]
3767
3768        loaded_batches += 1
3769        self.evaluation_batch_progress[snapshot.snapshot_id] = (view_name, loaded_batches)
3770
3771        finished_loading = loaded_batches == total_batches
3772        status = "Loaded" if finished_loading else "Loading"
3773        print(f"{status} '{view_name}', Completed Batches: {loaded_batches}/{total_batches}")
3774        if finished_loading:
3775            total_finished_loading = len(
3776                [
3777                    s
3778                    for s, total in self.evaluation_model_batch_sizes.items()
3779                    if self.evaluation_batch_progress.get(s.snapshot_id, (None, -1))[1] == total
3780                ]
3781            )
3782            total = len(self.evaluation_batch_progress)
3783            print(f"Completed Loading {total_finished_loading}/{total} Models")

Update the snapshot evaluation progress.

def stop_evaluation_progress(self, success: bool = True) -> None:
3785    def stop_evaluation_progress(self, success: bool = True) -> None:
3786        self.evaluation_batch_progress = {}
3787        super().stop_evaluation_progress(success)
3788        print(f"Loading {'succeeded' if success else 'failed'}")

Stop the snapshot evaluation progress.

def start_creation_progress( self, snapshots: List[sqlmesh.core.snapshot.definition.Snapshot], environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str]) -> None:
3790    def start_creation_progress(
3791        self,
3792        snapshots: t.List[Snapshot],
3793        environment_naming_info: EnvironmentNamingInfo,
3794        default_catalog: t.Optional[str],
3795    ) -> None:
3796        """Indicates that a new creation progress has begun."""
3797        self.model_creation_status = (0, len(snapshots))
3798        print("Starting Creating New Model Versions")

Indicates that a new creation progress has begun.

def update_creation_progress( self, snapshot: Union[sqlmesh.core.snapshot.definition.SnapshotTableInfo, sqlmesh.core.snapshot.definition.Snapshot]) -> None:
3800    def update_creation_progress(self, snapshot: SnapshotInfoLike) -> None:
3801        """Update the snapshot creation progress."""
3802        num_creations, total_creations = self.model_creation_status
3803        num_creations += 1
3804        self.model_creation_status = (num_creations, total_creations)
3805        if num_creations % 5 == 0:
3806            print(f"Created New Model Versions: {num_creations}/{total_creations}")

Update the snapshot creation progress.

def stop_creation_progress(self, success: bool = True) -> None:
3808    def stop_creation_progress(self, success: bool = True) -> None:
3809        """Stop the snapshot creation progress."""
3810        self.model_creation_status = (0, 0)
3811        print(f"New Model Creation {'succeeded' if success else 'failed'}")

Stop the snapshot creation progress.

def start_promotion_progress( self, snapshots: List[sqlmesh.core.snapshot.definition.SnapshotTableInfo], environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str]) -> None:
3813    def start_promotion_progress(
3814        self,
3815        snapshots: t.List[SnapshotTableInfo],
3816        environment_naming_info: EnvironmentNamingInfo,
3817        default_catalog: t.Optional[str],
3818    ) -> None:
3819        """Indicates that a new snapshot promotion progress has begun."""
3820        self.promotion_status = (0, len(snapshots))
3821        print(f"Virtually Updating '{environment_naming_info.name}'")

Indicates that a new snapshot promotion progress has begun.

def update_promotion_progress( self, snapshot: Union[sqlmesh.core.snapshot.definition.SnapshotTableInfo, sqlmesh.core.snapshot.definition.Snapshot], promoted: bool) -> None:
3823    def update_promotion_progress(self, snapshot: SnapshotInfoLike, promoted: bool) -> None:
3824        """Update the snapshot promotion progress."""
3825        num_promotions, total_promotions = self.promotion_status
3826        num_promotions += 1
3827        self.promotion_status = (num_promotions, total_promotions)
3828        if num_promotions % 5 == 0:
3829            print(f"Virtually Updated {num_promotions}/{total_promotions}")

Update the snapshot promotion progress.

def stop_promotion_progress(self, success: bool = True) -> None:
3831    def stop_promotion_progress(self, success: bool = True) -> None:
3832        """Stop the snapshot promotion progress."""
3833        self.promotion_status = (0, 0)
3834        print(f"Virtual Update {'succeeded' if success else 'failed'}")

Stop the snapshot promotion progress.

def start_snapshot_migration_progress(self, total_tasks: int) -> None:
3836    def start_snapshot_migration_progress(self, total_tasks: int) -> None:
3837        """Indicates that a new migration progress has begun."""
3838        self.migration_status = (0, total_tasks)
3839        print("Starting Migration")

Indicates that a new migration progress has begun.

def update_snapshot_migration_progress(self, num_tasks: int) -> None:
3841    def update_snapshot_migration_progress(self, num_tasks: int) -> None:
3842        """Update the migration progress."""
3843        num_migrations, total_migrations = self.migration_status
3844        num_migrations += num_tasks
3845        self.migration_status = (num_migrations, total_migrations)
3846        if num_migrations % 5 == 0:
3847            print(f"Migration Updated {num_migrations}/{total_migrations}")

Update the migration progress.

def log_migration_status(self, success: bool = True) -> None:
3849    def log_migration_status(self, success: bool = True) -> None:
3850        """Log the migration status."""
3851        print(f"Migration {'succeeded' if success else 'failed'}")

Log the migration status.

def stop_snapshot_migration_progress(self, success: bool = True) -> None:
3853    def stop_snapshot_migration_progress(self, success: bool = True) -> None:
3854        """Stop the migration progress."""
3855        self.migration_status = (0, 0)
3856        print(f"Snapshot migration {'succeeded' if success else 'failed'}")

Stop the migration progress.

def start_env_migration_progress(self, total_tasks: int) -> None:
3858    def start_env_migration_progress(self, total_tasks: int) -> None:
3859        """Indicates that a new migration progress has begun."""
3860        self.env_migration_status = (0, total_tasks)
3861        print("Starting Environment migration")

Indicates that a new migration progress has begun.

def update_env_migration_progress(self, num_tasks: int) -> None:
3863    def update_env_migration_progress(self, num_tasks: int) -> None:
3864        """Update the migration progress."""
3865        num_migrations, total_migrations = self.env_migration_status
3866        num_migrations += num_tasks
3867        self.env_migration_status = (num_migrations, total_migrations)
3868        if num_migrations % 5 == 0:
3869            print(f"Environment migration Updated {num_migrations}/{total_migrations}")

Update the migration progress.

def stop_env_migration_progress(self, success: bool = True) -> None:
3871    def stop_env_migration_progress(self, success: bool = True) -> None:
3872        """Stop the migration progress."""
3873        self.env_migration_status = (0, 0)
3874        print(f"Environment migration {'succeeded' if success else 'failed'}")

Stop the migration progress.

Inherited Members
CaptureTerminalConsole
captured_output
captured_warnings
captured_errors
consume_captured_output
consume_captured_warnings
consume_captured_errors
log_warning
log_error
log_skipped_models
log_failed_models
TerminalConsole
TABLE_DIFF_SOURCE_BLUE
TABLE_DIFF_TARGET_GREEN
AUDIT_PASS_MARK
GREEN_AUDIT_PASS_MARK
AUDIT_FAIL_MARK
AUDIT_PADDING
CHECK_MARK
console
evaluation_progress_live
evaluation_total_progress
evaluation_total_task
evaluation_model_progress
evaluation_model_tasks
evaluation_model_batch_sizes
evaluation_column_widths
environment_naming_info
default_catalog
creation_progress
creation_column_widths
creation_task
promotion_progress
promotion_column_widths
promotion_task
migration_progress
migration_task
env_migration_progress
env_migration_task
loading_status
state_export_progress
state_export_version_task
state_export_snapshot_task
state_export_environment_task
state_import_progress
state_import_version_task
state_import_snapshot_task
state_import_environment_task
table_diff_progress
table_diff_model_progress
table_diff_model_tasks
table_diff_progress_live
signal_progress_logged
signal_status_tree
verbosity
dialect
ignore_warnings
start_plan_evaluation
stop_plan_evaluation
start_signal_progress
update_signal_progress
stop_signal_progress
start_cleanup
update_cleanup_progress
stop_cleanup
start_destroy
stop_destroy
start_state_export
update_state_export_progress
stop_state_export
start_state_import
update_state_import_progress
stop_state_import
show_environment_difference_summary
show_model_difference_summary
plan
log_test_results
show_sql
log_status_update
log_models_updated_during_restatement
log_destructive_change
log_additive_change
log_success
loading_start
loading_stop
show_table_diff_details
start_table_diff_progress
start_table_diff_model_progress
update_table_diff_progress
stop_table_diff_progress
show_table_diff_summary
show_schema_diff
show_row_diff
show_table_diff
print_environments
show_intervals
print_connection_config
show_linter_violations
Console
INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD
class DebuggerTerminalConsole(TerminalConsole):
3877class DebuggerTerminalConsole(TerminalConsole):
3878    """A terminal console to use while debugging with no fluff, progress bars, etc."""
3879
3880    def __init__(
3881        self,
3882        console: t.Optional[RichConsole],
3883        *args: t.Any,
3884        dialect: DialectType = None,
3885        ignore_warnings: bool = False,
3886        **kwargs: t.Any,
3887    ) -> None:
3888        self.console: RichConsole = console or srich.console
3889        self.dialect = dialect
3890        self.verbosity = Verbosity.DEFAULT
3891        self.ignore_warnings = ignore_warnings
3892
3893    def _write(self, msg: t.Any, *args: t.Any, **kwargs: t.Any) -> None:
3894        self.console.log(msg, *args, **kwargs)
3895
3896    def start_plan_evaluation(self, plan: EvaluatablePlan) -> None:
3897        self._write("Starting plan", plan.plan_id)
3898
3899    def stop_plan_evaluation(self) -> None:
3900        self._write("Stopping plan")
3901
3902    def start_evaluation_progress(
3903        self,
3904        batched_intervals: t.Dict[Snapshot, Intervals],
3905        environment_naming_info: EnvironmentNamingInfo,
3906        default_catalog: t.Optional[str],
3907        audit_only: bool = False,
3908    ) -> None:
3909        message = "evaluation" if not audit_only else "auditing"
3910        self._write(
3911            f"Starting {message} for {sum(len(intervals) for intervals in batched_intervals.values())} snapshots"
3912        )
3913
3914    def start_snapshot_evaluation_progress(
3915        self, snapshot: Snapshot, audit_only: bool = False
3916    ) -> None:
3917        self._write(f"{'Evaluating' if not audit_only else 'Auditing'} {snapshot.name}")
3918
3919    def update_snapshot_evaluation_progress(
3920        self,
3921        snapshot: Snapshot,
3922        interval: Interval,
3923        batch_idx: int,
3924        duration_ms: t.Optional[int],
3925        num_audits_passed: int,
3926        num_audits_failed: int,
3927        audit_only: bool = False,
3928        execution_stats: t.Optional[QueryExecutionStats] = None,
3929        auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None,
3930    ) -> None:
3931        message = f"Evaluated {snapshot.name} | batch={batch_idx} | duration={duration_ms}ms | num_audits_passed={num_audits_passed} | num_audits_failed={num_audits_failed}"
3932
3933        if auto_restatement_triggers:
3934            message += f" | auto_restatement_triggers=[{', '.join(trigger.name for trigger in auto_restatement_triggers)}]"
3935
3936        if audit_only:
3937            message = f"Audited {snapshot.name} | duration={duration_ms}ms | num_audits_passed={num_audits_passed} | num_audits_failed={num_audits_failed}"
3938
3939        self._write(message)
3940
3941    def stop_evaluation_progress(self, success: bool = True) -> None:
3942        self._write(f"Stopping evaluation with success={success}")
3943
3944    def start_creation_progress(
3945        self,
3946        snapshots: t.List[Snapshot],
3947        environment_naming_info: EnvironmentNamingInfo,
3948        default_catalog: t.Optional[str],
3949    ) -> None:
3950        self._write(f"Starting creation for {len(snapshots)} snapshots")
3951
3952    def update_creation_progress(self, snapshot: SnapshotInfoLike) -> None:
3953        self._write(f"Creating {snapshot.name}")
3954
3955    def stop_creation_progress(self, success: bool = True) -> None:
3956        self._write(f"Stopping creation with success={success}")
3957
3958    def update_cleanup_progress(self, object_name: str) -> None:
3959        self._write(f"Cleaning up {object_name}")
3960
3961    def start_promotion_progress(
3962        self,
3963        snapshots: t.List[SnapshotTableInfo],
3964        environment_naming_info: EnvironmentNamingInfo,
3965        default_catalog: t.Optional[str],
3966    ) -> None:
3967        if snapshots:
3968            self._write(f"Starting promotion for {len(snapshots)} snapshots")
3969
3970    def update_promotion_progress(self, snapshot: SnapshotInfoLike, promoted: bool) -> None:
3971        self._write(f"Promoting {snapshot.name}")
3972
3973    def stop_promotion_progress(self, success: bool = True) -> None:
3974        self._write(f"Stopping promotion with success={success}")
3975
3976    def start_snapshot_migration_progress(self, total_tasks: int) -> None:
3977        self._write(f"Starting migration for {total_tasks} snapshots")
3978
3979    def update_snapshot_migration_progress(self, num_tasks: int) -> None:
3980        self._write(f"Migration {num_tasks}")
3981
3982    def log_migration_status(self, success: bool = True) -> None:
3983        self._write(f"Migration finished with success={success}")
3984
3985    def stop_snapshot_migration_progress(self, success: bool = True) -> None:
3986        self._write(f"Stopping snapshot migration with success={success}")
3987
3988    def start_env_migration_progress(self, total_tasks: int) -> None:
3989        self._write(f"Starting migration for {total_tasks} environments")
3990
3991    def update_env_migration_progress(self, num_tasks: int) -> None:
3992        self._write(f"Environment migration {num_tasks}")
3993
3994    def stop_env_migration_progress(self, success: bool = True) -> None:
3995        self._write(f"Stopping environment migration with success={success}")
3996
3997    def show_environment_difference_summary(
3998        self,
3999        context_diff: ContextDiff,
4000        no_diff: bool = True,
4001    ) -> None:
4002        self._write("Environment Difference Summary:")
4003
4004        if context_diff.has_requirement_changes:
4005            self._write(f"Requirements:\n{context_diff.requirements_diff()}")
4006
4007        if context_diff.has_environment_statements_changes and not no_diff:
4008            self._write("Environment statements:\n")
4009            for _, diff in context_diff.environment_statements_diff(
4010                include_python_env=not context_diff.is_new_environment
4011            ):
4012                self._write(diff)
4013
4014    def show_model_difference_summary(
4015        self,
4016        context_diff: ContextDiff,
4017        environment_naming_info: EnvironmentNamingInfo,
4018        default_catalog: t.Optional[str],
4019        no_diff: bool = True,
4020    ) -> None:
4021        self._write("Model Difference Summary:")
4022
4023        for added in context_diff.new_snapshots:
4024            self._write(f"  Added: {added}")
4025        for removed in context_diff.removed_snapshots:
4026            self._write(f"  Removed: {removed}")
4027        for modified in context_diff.modified_snapshots:
4028            self._write(f"  Modified: {modified}")
4029
4030    def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
4031        self._write("Test Results:", result)
4032
4033    def show_sql(self, sql: str) -> None:
4034        self._write(sql)
4035
4036    def log_status_update(self, message: str) -> None:
4037        self._write(message, style="bold blue")
4038
4039    def log_error(self, message: str) -> None:
4040        self._write(message, style="bold red")
4041
4042    def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None:
4043        logger.warning(long_message or short_message)
4044        if not self.ignore_warnings:
4045            self._write(short_message, style="bold yellow")
4046
4047    def log_success(self, message: str) -> None:
4048        self._write(message, style="bold green")
4049
4050    def loading_start(self, message: t.Optional[str] = None) -> uuid.UUID:
4051        self._write(message)
4052        return uuid.uuid4()
4053
4054    def loading_stop(self, id: uuid.UUID) -> None:
4055        self._write("Done")
4056
4057    def show_schema_diff(self, schema_diff: SchemaDiff) -> None:
4058        self._write(schema_diff)
4059
4060    def show_row_diff(
4061        self, row_diff: RowDiff, show_sample: bool = True, skip_grain_check: bool = False
4062    ) -> None:
4063        self._write(row_diff)
4064
4065    def show_table_diff(
4066        self,
4067        table_diffs: t.List[TableDiff],
4068        show_sample: bool = True,
4069        skip_grain_check: bool = False,
4070        temp_schema: t.Optional[str] = None,
4071    ) -> None:
4072        for table_diff in table_diffs:
4073            self.show_table_diff_summary(table_diff)
4074            self.show_schema_diff(table_diff.schema_diff())
4075            self.show_row_diff(
4076                table_diff.row_diff(temp_schema=temp_schema, skip_grain_check=skip_grain_check),
4077                show_sample=show_sample,
4078                skip_grain_check=skip_grain_check,
4079            )
4080
4081    def update_table_diff_progress(self, model: str) -> None:
4082        self._write(f"Finished table diff for: {model}")
4083
4084    def start_table_diff_progress(self, models_to_diff: int) -> None:
4085        self._write("Table diff started")
4086
4087    def start_table_diff_model_progress(self, model: str) -> None:
4088        self._write(f"Calculating differences for: {model}")
4089
4090    def stop_table_diff_progress(self, success: bool) -> None:
4091        self._write(f"Table diff finished with success={success}")
4092
4093    def show_table_diff_details(
4094        self,
4095        models_to_diff: t.List[str],
4096    ) -> None:
4097        if models_to_diff:
4098            models = "\n".join(models_to_diff)
4099            self._write(f"Models to compare: {models}")
4100
4101    def show_table_diff_summary(self, table_diff: TableDiff) -> None:
4102        if table_diff.model_name:
4103            self._write(f"Model: {table_diff.model_name}")
4104            self._write(f"Source env: {table_diff.source_alias}")
4105            self._write(f"Target env: {table_diff.target_alias}")
4106        self._write(f"Source table: {table_diff.source}")
4107        self._write(f"Target table: {table_diff.target}")
4108        _, _, key_column_names = table_diff.key_columns
4109        keys = ", ".join(key_column_names)
4110        self._write(f"Join On: {keys}")

A terminal console to use while debugging with no fluff, progress bars, etc.

DebuggerTerminalConsole( console: Optional[rich.console.Console], *args: Any, dialect: Union[str, sqlglot.dialects.dialect.Dialect, Type[sqlglot.dialects.dialect.Dialect], NoneType] = None, ignore_warnings: bool = False, **kwargs: Any)
3880    def __init__(
3881        self,
3882        console: t.Optional[RichConsole],
3883        *args: t.Any,
3884        dialect: DialectType = None,
3885        ignore_warnings: bool = False,
3886        **kwargs: t.Any,
3887    ) -> None:
3888        self.console: RichConsole = console or srich.console
3889        self.dialect = dialect
3890        self.verbosity = Verbosity.DEFAULT
3891        self.ignore_warnings = ignore_warnings
console: rich.console.Console
dialect
verbosity
ignore_warnings
def start_plan_evaluation(self, plan: sqlmesh.core.plan.definition.EvaluatablePlan) -> None:
3896    def start_plan_evaluation(self, plan: EvaluatablePlan) -> None:
3897        self._write("Starting plan", plan.plan_id)

Indicates that a new evaluation has begun.

def stop_plan_evaluation(self) -> None:
3899    def stop_plan_evaluation(self) -> None:
3900        self._write("Stopping plan")

Indicates that the evaluation has ended.

def start_evaluation_progress( self, batched_intervals: Dict[sqlmesh.core.snapshot.definition.Snapshot, List[Tuple[int, int]]], environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str], audit_only: bool = False) -> None:
3902    def start_evaluation_progress(
3903        self,
3904        batched_intervals: t.Dict[Snapshot, Intervals],
3905        environment_naming_info: EnvironmentNamingInfo,
3906        default_catalog: t.Optional[str],
3907        audit_only: bool = False,
3908    ) -> None:
3909        message = "evaluation" if not audit_only else "auditing"
3910        self._write(
3911            f"Starting {message} for {sum(len(intervals) for intervals in batched_intervals.values())} snapshots"
3912        )

Indicates that a new snapshot evaluation/auditing progress has begun.

def start_snapshot_evaluation_progress( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, audit_only: bool = False) -> None:
3914    def start_snapshot_evaluation_progress(
3915        self, snapshot: Snapshot, audit_only: bool = False
3916    ) -> None:
3917        self._write(f"{'Evaluating' if not audit_only else 'Auditing'} {snapshot.name}")

Starts the snapshot evaluation progress.

def update_snapshot_evaluation_progress( self, snapshot: sqlmesh.core.snapshot.definition.Snapshot, interval: Tuple[int, int], batch_idx: int, duration_ms: Optional[int], num_audits_passed: int, num_audits_failed: int, audit_only: bool = False, execution_stats: Optional[sqlmesh.core.snapshot.execution_tracker.QueryExecutionStats] = None, auto_restatement_triggers: Optional[List[sqlmesh.core.snapshot.definition.SnapshotId]] = None) -> None:
3919    def update_snapshot_evaluation_progress(
3920        self,
3921        snapshot: Snapshot,
3922        interval: Interval,
3923        batch_idx: int,
3924        duration_ms: t.Optional[int],
3925        num_audits_passed: int,
3926        num_audits_failed: int,
3927        audit_only: bool = False,
3928        execution_stats: t.Optional[QueryExecutionStats] = None,
3929        auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None,
3930    ) -> None:
3931        message = f"Evaluated {snapshot.name} | batch={batch_idx} | duration={duration_ms}ms | num_audits_passed={num_audits_passed} | num_audits_failed={num_audits_failed}"
3932
3933        if auto_restatement_triggers:
3934            message += f" | auto_restatement_triggers=[{', '.join(trigger.name for trigger in auto_restatement_triggers)}]"
3935
3936        if audit_only:
3937            message = f"Audited {snapshot.name} | duration={duration_ms}ms | num_audits_passed={num_audits_passed} | num_audits_failed={num_audits_failed}"
3938
3939        self._write(message)

Update the snapshot evaluation progress.

def stop_evaluation_progress(self, success: bool = True) -> None:
3941    def stop_evaluation_progress(self, success: bool = True) -> None:
3942        self._write(f"Stopping evaluation with success={success}")

Stop the snapshot evaluation progress.

def start_creation_progress( self, snapshots: List[sqlmesh.core.snapshot.definition.Snapshot], environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str]) -> None:
3944    def start_creation_progress(
3945        self,
3946        snapshots: t.List[Snapshot],
3947        environment_naming_info: EnvironmentNamingInfo,
3948        default_catalog: t.Optional[str],
3949    ) -> None:
3950        self._write(f"Starting creation for {len(snapshots)} snapshots")

Indicates that a new creation progress has begun.

def update_creation_progress( self, snapshot: Union[sqlmesh.core.snapshot.definition.SnapshotTableInfo, sqlmesh.core.snapshot.definition.Snapshot]) -> None:
3952    def update_creation_progress(self, snapshot: SnapshotInfoLike) -> None:
3953        self._write(f"Creating {snapshot.name}")

Update the snapshot creation progress.

def stop_creation_progress(self, success: bool = True) -> None:
3955    def stop_creation_progress(self, success: bool = True) -> None:
3956        self._write(f"Stopping creation with success={success}")

Stop the snapshot creation progress.

def update_cleanup_progress(self, object_name: str) -> None:
3958    def update_cleanup_progress(self, object_name: str) -> None:
3959        self._write(f"Cleaning up {object_name}")

Update the snapshot cleanup progress.

def start_promotion_progress( self, snapshots: List[sqlmesh.core.snapshot.definition.SnapshotTableInfo], environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str]) -> None:
3961    def start_promotion_progress(
3962        self,
3963        snapshots: t.List[SnapshotTableInfo],
3964        environment_naming_info: EnvironmentNamingInfo,
3965        default_catalog: t.Optional[str],
3966    ) -> None:
3967        if snapshots:
3968            self._write(f"Starting promotion for {len(snapshots)} snapshots")

Indicates that a new snapshot promotion progress has begun.

def update_promotion_progress( self, snapshot: Union[sqlmesh.core.snapshot.definition.SnapshotTableInfo, sqlmesh.core.snapshot.definition.Snapshot], promoted: bool) -> None:
3970    def update_promotion_progress(self, snapshot: SnapshotInfoLike, promoted: bool) -> None:
3971        self._write(f"Promoting {snapshot.name}")

Update the snapshot promotion progress.

def stop_promotion_progress(self, success: bool = True) -> None:
3973    def stop_promotion_progress(self, success: bool = True) -> None:
3974        self._write(f"Stopping promotion with success={success}")

Stop the snapshot promotion progress.

def start_snapshot_migration_progress(self, total_tasks: int) -> None:
3976    def start_snapshot_migration_progress(self, total_tasks: int) -> None:
3977        self._write(f"Starting migration for {total_tasks} snapshots")

Indicates that a new snapshot migration progress has begun.

def update_snapshot_migration_progress(self, num_tasks: int) -> None:
3979    def update_snapshot_migration_progress(self, num_tasks: int) -> None:
3980        self._write(f"Migration {num_tasks}")

Update the migration progress.

def log_migration_status(self, success: bool = True) -> None:
3982    def log_migration_status(self, success: bool = True) -> None:
3983        self._write(f"Migration finished with success={success}")

Log the migration status.

def stop_snapshot_migration_progress(self, success: bool = True) -> None:
3985    def stop_snapshot_migration_progress(self, success: bool = True) -> None:
3986        self._write(f"Stopping snapshot migration with success={success}")

Stop the migration progress.

def start_env_migration_progress(self, total_tasks: int) -> None:
3988    def start_env_migration_progress(self, total_tasks: int) -> None:
3989        self._write(f"Starting migration for {total_tasks} environments")

Indicates that a new environment migration has begun.

def update_env_migration_progress(self, num_tasks: int) -> None:
3991    def update_env_migration_progress(self, num_tasks: int) -> None:
3992        self._write(f"Environment migration {num_tasks}")

Update the environment migration progress.

def stop_env_migration_progress(self, success: bool = True) -> None:
3994    def stop_env_migration_progress(self, success: bool = True) -> None:
3995        self._write(f"Stopping environment migration with success={success}")

Stop the environment migration progress.

def show_environment_difference_summary( self, context_diff: sqlmesh.core.context_diff.ContextDiff, no_diff: bool = True) -> None:
3997    def show_environment_difference_summary(
3998        self,
3999        context_diff: ContextDiff,
4000        no_diff: bool = True,
4001    ) -> None:
4002        self._write("Environment Difference Summary:")
4003
4004        if context_diff.has_requirement_changes:
4005            self._write(f"Requirements:\n{context_diff.requirements_diff()}")
4006
4007        if context_diff.has_environment_statements_changes and not no_diff:
4008            self._write("Environment statements:\n")
4009            for _, diff in context_diff.environment_statements_diff(
4010                include_python_env=not context_diff.is_new_environment
4011            ):
4012                self._write(diff)

Shows a summary of the environment differences.

Arguments:
  • context_diff: The context diff to use to print the summary
  • no_diff: Hide the actual environment statement differences.
def show_model_difference_summary( self, context_diff: sqlmesh.core.context_diff.ContextDiff, environment_naming_info: sqlmesh.core.environment.EnvironmentNamingInfo, default_catalog: Optional[str], no_diff: bool = True) -> None:
4014    def show_model_difference_summary(
4015        self,
4016        context_diff: ContextDiff,
4017        environment_naming_info: EnvironmentNamingInfo,
4018        default_catalog: t.Optional[str],
4019        no_diff: bool = True,
4020    ) -> None:
4021        self._write("Model Difference Summary:")
4022
4023        for added in context_diff.new_snapshots:
4024            self._write(f"  Added: {added}")
4025        for removed in context_diff.removed_snapshots:
4026            self._write(f"  Removed: {removed}")
4027        for modified in context_diff.modified_snapshots:
4028            self._write(f"  Modified: {modified}")

Shows a summary of the model differences.

Arguments:
  • context_diff: The context diff to use to print the summary
  • environment_naming_info: The environment naming info to reference when printing model names
  • default_catalog: The default catalog to reference when deciding to remove catalog from display names
  • no_diff: Hide the actual SQL differences.
def log_test_results( self, result: sqlmesh.core.test.result.ModelTextTestResult, target_dialect: str) -> None:
4030    def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
4031        self._write("Test Results:", result)

Display the test result and output.

Arguments:
  • result: The unittest test result that contains metrics like num success, fails, ect.
  • target_dialect: The dialect that tests were run against. Assumes all tests run against the same dialect.
def show_sql(self, sql: str) -> None:
4033    def show_sql(self, sql: str) -> None:
4034        self._write(sql)

Display to the user SQL.

def log_status_update(self, message: str) -> None:
4036    def log_status_update(self, message: str) -> None:
4037        self._write(message, style="bold blue")

Display general status update to the user.

def log_error(self, message: str) -> None:
4039    def log_error(self, message: str) -> None:
4040        self._write(message, style="bold red")

Display error info to the user.

def log_warning(self, short_message: str, long_message: Optional[str] = None) -> None:
4042    def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None:
4043        logger.warning(long_message or short_message)
4044        if not self.ignore_warnings:
4045            self._write(short_message, style="bold yellow")

Display warning info to the user.

Arguments:
  • short_message: The warning message to print to console.
  • long_message: The warning message to log to file. If not provided, short_message is used.
def log_success(self, message: str) -> None:
4047    def log_success(self, message: str) -> None:
4048        self._write(message, style="bold green")

Display a general successful message to the user.

def loading_start(self, message: Optional[str] = None) -> uuid.UUID:
4050    def loading_start(self, message: t.Optional[str] = None) -> uuid.UUID:
4051        self._write(message)
4052        return uuid.uuid4()

Starts loading and returns a unique ID that can be used to stop the loading. Optionally can display a message.

def loading_stop(self, id: uuid.UUID) -> None:
4054    def loading_stop(self, id: uuid.UUID) -> None:
4055        self._write("Done")

Stop loading for the given id.

def show_schema_diff(self, schema_diff: sqlmesh.core.table_diff.SchemaDiff) -> None:
4057    def show_schema_diff(self, schema_diff: SchemaDiff) -> None:
4058        self._write(schema_diff)

Show table schema diff.

def show_row_diff( self, row_diff: sqlmesh.core.table_diff.RowDiff, show_sample: bool = True, skip_grain_check: bool = False) -> None:
4060    def show_row_diff(
4061        self, row_diff: RowDiff, show_sample: bool = True, skip_grain_check: bool = False
4062    ) -> None:
4063        self._write(row_diff)

Show table summary diff.

def show_table_diff( self, table_diffs: List[sqlmesh.core.table_diff.TableDiff], show_sample: bool = True, skip_grain_check: bool = False, temp_schema: Optional[str] = None) -> None:
4065    def show_table_diff(
4066        self,
4067        table_diffs: t.List[TableDiff],
4068        show_sample: bool = True,
4069        skip_grain_check: bool = False,
4070        temp_schema: t.Optional[str] = None,
4071    ) -> None:
4072        for table_diff in table_diffs:
4073            self.show_table_diff_summary(table_diff)
4074            self.show_schema_diff(table_diff.schema_diff())
4075            self.show_row_diff(
4076                table_diff.row_diff(temp_schema=temp_schema, skip_grain_check=skip_grain_check),
4077                show_sample=show_sample,
4078                skip_grain_check=skip_grain_check,
4079            )

Display the table diff between all mismatched tables.

def update_table_diff_progress(self, model: str) -> None:
4081    def update_table_diff_progress(self, model: str) -> None:
4082        self._write(f"Finished table diff for: {model}")

Update table diff progress bar

def start_table_diff_progress(self, models_to_diff: int) -> None:
4084    def start_table_diff_progress(self, models_to_diff: int) -> None:
4085        self._write("Table diff started")

Start table diff progress bar

def start_table_diff_model_progress(self, model: str) -> None:
4087    def start_table_diff_model_progress(self, model: str) -> None:
4088        self._write(f"Calculating differences for: {model}")

Start table diff model progress

def stop_table_diff_progress(self, success: bool) -> None:
4090    def stop_table_diff_progress(self, success: bool) -> None:
4091        self._write(f"Table diff finished with success={success}")

Stop table diff progress bar

def show_table_diff_details(self, models_to_diff: List[str]) -> None:
4093    def show_table_diff_details(
4094        self,
4095        models_to_diff: t.List[str],
4096    ) -> None:
4097        if models_to_diff:
4098            models = "\n".join(models_to_diff)
4099            self._write(f"Models to compare: {models}")

Display information about which tables are going to be diffed

def show_table_diff_summary(self, table_diff: sqlmesh.core.table_diff.TableDiff) -> None:
4101    def show_table_diff_summary(self, table_diff: TableDiff) -> None:
4102        if table_diff.model_name:
4103            self._write(f"Model: {table_diff.model_name}")
4104            self._write(f"Source env: {table_diff.source_alias}")
4105            self._write(f"Target env: {table_diff.target_alias}")
4106        self._write(f"Source table: {table_diff.source}")
4107        self._write(f"Target table: {table_diff.target}")
4108        _, _, key_column_names = table_diff.key_columns
4109        keys = ", ".join(key_column_names)
4110        self._write(f"Join On: {keys}")

Display information about the tables being diffed and how they are being joined

def set_console(console: Console) -> None:
4116def set_console(console: Console) -> None:
4117    """Sets the console instance."""
4118    global _CONSOLE
4119    _CONSOLE = console

Sets the console instance.

def configure_console(**kwargs: Any) -> None:
4122def configure_console(**kwargs: t.Any) -> None:
4123    """Configures the console instance."""
4124    global _CONSOLE
4125    _CONSOLE = create_console(**kwargs)

Configures the console instance.

def get_console() -> Console:
4128def get_console() -> Console:
4129    """Returns the console instance or creates a new one if it hasn't been created yet."""
4130    return _CONSOLE

Returns the console instance or creates a new one if it hasn't been created yet.

def create_console( **kwargs: Any) -> TerminalConsole | DatabricksMagicConsole | NotebookMagicConsole:
4133def create_console(
4134    **kwargs: t.Any,
4135) -> TerminalConsole | DatabricksMagicConsole | NotebookMagicConsole:
4136    """
4137    Creates a new console instance that is appropriate for the current runtime environment.
4138
4139    Note: Google Colab environment is untested and currently assumes is compatible with the base
4140    NotebookMagicConsole.
4141    """
4142    from sqlmesh import RuntimeEnv
4143
4144    runtime_env = RuntimeEnv.get()
4145
4146    runtime_env_mapping = {
4147        RuntimeEnv.DATABRICKS: DatabricksMagicConsole,
4148        RuntimeEnv.JUPYTER: NotebookMagicConsole,
4149        RuntimeEnv.TERMINAL: TerminalConsole,
4150        RuntimeEnv.GOOGLE_COLAB: NotebookMagicConsole,
4151        RuntimeEnv.DEBUGGER: DebuggerTerminalConsole,
4152        RuntimeEnv.CI: MarkdownConsole,
4153    }
4154    rich_console_kwargs: t.Dict[str, t.Any] = {"theme": srich.theme}
4155    if runtime_env.is_jupyter or runtime_env.is_google_colab:
4156        rich_console_kwargs["force_jupyter"] = True
4157    return runtime_env_mapping[runtime_env](
4158        **{**{"console": RichConsole(**rich_console_kwargs)}, **kwargs}
4159    )

Creates a new console instance that is appropriate for the current runtime environment.

Note: Google Colab environment is untested and currently assumes is compatible with the base NotebookMagicConsole.