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