Edit on GitHub

sqlmesh.magics

   1from __future__ import annotations
   2
   3from io import StringIO
   4
   5import functools
   6import logging
   7import typing as t
   8from argparse import Namespace, SUPPRESS
   9from collections import defaultdict
  10from copy import deepcopy
  11from pathlib import Path
  12
  13from hyperscript import h
  14
  15try:
  16    from IPython.core.display import display  # type: ignore
  17except ImportError:
  18    from IPython.display import display
  19
  20from IPython.core.magic import (
  21    Magics,
  22    cell_magic,
  23    line_cell_magic,
  24    line_magic,
  25    magics_class,
  26)
  27from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring
  28from IPython.utils.process import arg_split
  29from rich.jupyter import JupyterRenderable
  30from sqlmesh.cli.project_init import ProjectTemplate, init_example_project
  31from sqlmesh.core import analytics
  32from sqlmesh.core.config import load_configs
  33from sqlmesh.core.config.connection import INIT_DISPLAY_INFO_TO_TYPE
  34from sqlmesh.core.console import create_console, set_console, configure_console
  35from sqlmesh.core.context import Context
  36from sqlmesh.core.dialect import format_model_expressions, parse
  37from sqlmesh.core.model import load_sql_based_model
  38from sqlmesh.core.test import ModelTestMetadata
  39from sqlmesh.utils import yaml, Verbosity, optional_import
  40from sqlmesh.utils.errors import MagicError, MissingContextException, SQLMeshError
  41
  42logger = logging.getLogger(__name__)
  43
  44CONTEXT_VARIABLE_NAMES = [
  45    "context",
  46    "ctx",
  47    "sqlmesh",
  48]
  49
  50
  51def pass_sqlmesh_context(func: t.Callable) -> t.Callable:
  52    @functools.wraps(func)
  53    def wrapper(self: SQLMeshMagics, *args: t.Any, **kwargs: t.Any) -> None:
  54        for variable_name in CONTEXT_VARIABLE_NAMES:
  55            context = self._shell.user_ns.get(variable_name)
  56            if isinstance(context, Context):
  57                break
  58        else:
  59            raise MissingContextException(
  60                f"Context must be defined and initialized with one of these names: {', '.join(CONTEXT_VARIABLE_NAMES)}"
  61            )
  62        old_console = context.console
  63        new_console = create_console(display=self.display)
  64        context.console = new_console
  65        set_console(new_console)
  66        context.refresh()
  67
  68        magic_name = func.__name__
  69        bound_method = getattr(self, magic_name, None)
  70        if bound_method:
  71            args_split = arg_split(args[0])
  72            parser = bound_method.parser
  73
  74            original_parser_actions = deepcopy(parser._actions)
  75            original_parser_defaults = parser._defaults
  76
  77            # Temporarily supress default values, otherwise any missing arg would be set and affect analytics
  78            parser._defaults = {}
  79            for action in parser._actions:
  80                action.default = SUPPRESS
  81
  82            parsed_args, _ = parser.parse_known_args(args_split, Namespace())
  83
  84            parser._actions = original_parser_actions
  85            parser._defaults = original_parser_defaults
  86
  87            command_args = {k for k, v in parsed_args.__dict__.items() if v is not None}
  88            analytics.collector.on_magic_command(command_name=magic_name, command_args=command_args)
  89
  90        func(self, context, *args, **kwargs)
  91
  92        context.console = old_console
  93        set_console(old_console)
  94
  95    return wrapper
  96
  97
  98def parse_expand(value: str) -> t.Union[bool, t.List[str]]:
  99    if value.lower() == "true":
 100        return True
 101    if value.lower() == "false":
 102        return False
 103    return [name.strip() for name in value.split(",") if name.strip()]
 104
 105
 106def format_arguments(func: t.Callable) -> t.Callable:
 107    """Decorator to add common format arguments to magic commands."""
 108    func = argument(
 109        "--normalize",
 110        action="store_true",
 111        help="Whether or not to normalize identifiers to lowercase.",
 112        default=None,
 113    )(func)
 114    func = argument(
 115        "--pad",
 116        type=int,
 117        help="Determines the pad size in a formatted string.",
 118    )(func)
 119    func = argument(
 120        "--indent",
 121        type=int,
 122        help="Determines the indentation size in a formatted string.",
 123    )(func)
 124    func = argument(
 125        "--normalize-functions",
 126        type=str,
 127        help="Whether or not to normalize all function names. Possible values are: 'upper', 'lower'",
 128    )(func)
 129    func = argument(
 130        "--leading-comma",
 131        action="store_true",
 132        help="Determines whether or not the comma is leading or trailing in select expressions. Default is trailing.",
 133        default=None,
 134    )(func)
 135    func = argument(
 136        "--max-text-width",
 137        type=int,
 138        help="The max number of characters in a segment before creating new lines in pretty mode.",
 139    )(func)
 140    return func
 141
 142
 143@magics_class
 144class SQLMeshMagics(Magics):
 145    @property
 146    def display(self) -> t.Callable:
 147        from sqlmesh import RuntimeEnv
 148
 149        if RuntimeEnv.get().is_databricks:
 150            # Use Databricks' special display instead of the normal IPython display
 151            return self._shell.user_ns["display"]
 152        return display
 153
 154    @property
 155    def _shell(self) -> t.Any:
 156        # Make mypy happy.
 157        if not self.shell:
 158            raise RuntimeError("IPython Magics are in invalid state")
 159        return self.shell
 160
 161    @magic_arguments()
 162    @argument(
 163        "paths",
 164        type=str,
 165        nargs="+",
 166        default="",
 167        help="The path(s) to the SQLMesh project(s).",
 168    )
 169    @argument(
 170        "--config",
 171        type=str,
 172        help="Name of the config object. Only applicable to configuration defined using Python script.",
 173    )
 174    @argument("--gateway", type=str, help="The name of the gateway.")
 175    @argument("--ignore-warnings", action="store_true", help="Ignore warnings.")
 176    @argument("--debug", action="store_true", help="Enable debug mode.")
 177    @argument("--log-file-dir", type=str, help="The directory to write the log file to.")
 178    @argument(
 179        "--dotenv", type=str, help="Path to a custom .env file to load environment variables from."
 180    )
 181    @line_magic
 182    def context(self, line: str) -> None:
 183        """Sets the context in the user namespace."""
 184        from sqlmesh import configure_logging, remove_excess_logs
 185
 186        args = parse_argstring(self.context, line)
 187        log_file_dir = args.log_file_dir
 188
 189        configure_logging(
 190            args.debug,
 191            log_file_dir=log_file_dir,
 192            ignore_warnings=args.ignore_warnings,
 193        )
 194        configure_console(ignore_warnings=args.ignore_warnings)
 195
 196        dotenv_path = Path(args.dotenv) if args.dotenv else None
 197        configs = load_configs(
 198            args.config, Context.CONFIG_TYPE, args.paths, dotenv_path=dotenv_path
 199        )
 200        log_limit = list(configs.values())[0].log_limit
 201
 202        remove_excess_logs(log_file_dir, log_limit)
 203
 204        try:
 205            context = Context(paths=args.paths, config=configs, gateway=args.gateway)
 206            self._shell.user_ns["context"] = context
 207        except Exception:
 208            if args.debug:
 209                logger.exception("Failed to initialize SQLMesh context")
 210            raise
 211
 212        context.console.log_success(f"SQLMesh project context set to: {', '.join(args.paths)}")
 213
 214    @magic_arguments()
 215    @argument("path", type=str, help="The path where the new SQLMesh project should be created.")
 216    @argument(
 217        "engine",
 218        type=str,
 219        help=f"Project SQL engine. Supported values: '{', '.join([info[1] for info in sorted(INIT_DISPLAY_INFO_TO_TYPE.values(), key=lambda x: x[0])])}'.",  # type: ignore
 220    )
 221    @argument(
 222        "--template",
 223        "-t",
 224        type=str,
 225        help="Project template. Supported values: dbt, default, empty.",
 226    )
 227    @argument(
 228        "--dlt-pipeline",
 229        type=str,
 230        help="DLT pipeline for which to generate a SQLMesh project. Use alongside template: dlt",
 231    )
 232    @argument(
 233        "--dlt-path",
 234        type=str,
 235        help="The DLT pipelines working directory, where DLT stores pipeline state (by default ~/.dlt/pipelines). Use alongside template: dlt",
 236    )
 237    @line_magic
 238    def init(self, line: str) -> None:
 239        """Creates a SQLMesh project scaffold with a default SQL dialect."""
 240        args = parse_argstring(self.init, line)
 241        try:
 242            project_template = ProjectTemplate(
 243                args.template.lower() if args.template else "default"
 244            )
 245        except ValueError:
 246            raise MagicError(f"Invalid project template '{args.template}'")
 247        init_example_project(
 248            path=args.path,
 249            engine_type=args.engine,
 250            dialect=None,
 251            template=project_template,
 252            pipeline=args.dlt_pipeline,
 253            dlt_path=args.dlt_path,
 254        )
 255        html = str(
 256            h(
 257                "div",
 258                h(
 259                    "span",
 260                    {"style": {"color": "green", "font-weight": "bold"}},
 261                    "SQLMesh project scaffold created",
 262                ),
 263            )
 264        )
 265        self.display(JupyterRenderable(html=html, text=""))
 266
 267    @magic_arguments()
 268    @argument("model", type=str, help="The model.")
 269    @argument("--start", "-s", type=str, help="Start date to render.")
 270    @argument("--end", "-e", type=str, help="End date to render.")
 271    @argument("--execution-time", type=str, help="Execution time.")
 272    @argument("--dialect", "-d", type=str, help="The rendered dialect.")
 273    @line_cell_magic
 274    @pass_sqlmesh_context
 275    def model(self, context: Context, line: str, sql: t.Optional[str] = None) -> None:
 276        """Renders the model and automatically fills in an editable cell with the model definition."""
 277        args = parse_argstring(self.model, line)
 278
 279        model = context.get_model(args.model, raise_if_missing=True)
 280        config = context.config_for_node(model)
 281
 282        if sql:
 283            expressions = parse(sql, default_dialect=config.dialect)
 284            loaded = load_sql_based_model(
 285                expressions,
 286                macros=context._macros,
 287                jinja_macros=context._jinja_macros,
 288                path=model._path,
 289                dialect=config.dialect,
 290                time_column_format=config.time_column_format,
 291                physical_schema_mapping=context.config.physical_schema_mapping,
 292                default_catalog=context.default_catalog,
 293            )
 294
 295            if loaded.name == args.model:
 296                model = loaded
 297        else:
 298            if model._path:
 299                with open(model._path, "r", encoding="utf-8") as file:
 300                    expressions = parse(file.read(), default_dialect=config.dialect)
 301
 302        formatted = format_model_expressions(
 303            expressions,
 304            model.dialect,
 305            rewrite_casts=not config.format.no_rewrite_casts,
 306            **config.format.generator_options,
 307        )
 308
 309        self._shell.set_next_input(
 310            "\n".join(
 311                [
 312                    " ".join(["%%model", line]),
 313                    formatted,
 314                ]
 315            ),
 316            replace=True,
 317        )
 318
 319        if model._path:
 320            with open(model._path, "w", encoding="utf-8") as file:
 321                file.write(formatted)
 322
 323        if sql:
 324            context.console.log_success(f"Model `{args.model}` updated")
 325
 326        context.upsert_model(model)
 327        context.console.show_sql(
 328            context.render(
 329                model.name,
 330                start=args.start,
 331                end=args.end,
 332                execution_time=args.execution_time,
 333            ).sql(pretty=True, dialect=args.dialect or model.dialect)
 334        )
 335
 336    @magic_arguments()
 337    @argument("model", type=str, help="The model.")
 338    @argument("test_name", type=str, nargs="?", default=None, help="The test name to display")
 339    @argument("--ls", action="store_true", help="List tests associated with a model")
 340    @line_cell_magic
 341    @pass_sqlmesh_context
 342    def test(self, context: Context, line: str, test_def_raw: t.Optional[str] = None) -> None:
 343        """Allow the user to list tests for a model, output a specific test, and then write their changes back"""
 344        args = parse_argstring(self.test, line)
 345        if not args.test_name and not args.ls:
 346            raise MagicError("Must provide either test name or `--ls` to list tests")
 347
 348        test_meta = context.select_tests()
 349
 350        tests: t.Dict[str, t.Dict[str, ModelTestMetadata]] = defaultdict(dict)
 351        for model_test_metadata in test_meta:
 352            model = model_test_metadata.body.get("model")
 353            if not model:
 354                context.console.log_error(
 355                    f"Test found that does not have `model` defined: {model_test_metadata.path}"
 356                )
 357            else:
 358                tests[model][model_test_metadata.test_name] = model_test_metadata
 359
 360        model = context.get_model(args.model, raise_if_missing=True)
 361
 362        if args.ls:
 363            # TODO: Provide better UI for displaying tests
 364            for test_name in tests[model.name]:
 365                context.console.log_status_update(test_name)
 366            return
 367
 368        test = tests[model.name][args.test_name]
 369        test_def = yaml.load(test_def_raw) if test_def_raw else test.body
 370        test_def_output = yaml.dump(test_def)
 371
 372        self._shell.set_next_input(
 373            "\n".join(
 374                [
 375                    " ".join(["%%test", line]),
 376                    test_def_output,
 377                ]
 378            ),
 379            replace=True,
 380        )
 381
 382        with open(test.path, "r+", encoding="utf-8") as file:
 383            content = yaml.load(file.read())
 384            content[args.test_name] = test_def
 385            file.seek(0)
 386            yaml.dump(content, file)
 387            file.truncate()
 388
 389    @magic_arguments()
 390    @argument(
 391        "environment",
 392        nargs="?",
 393        type=str,
 394        help="The environment to run the plan against",
 395    )
 396    @argument("--start", "-s", type=str, help="Start date to backfill.")
 397    @argument("--end", "-e", type=str, help="End date to backfill.")
 398    @argument("--execution-time", type=str, help="Execution time.")
 399    @argument(
 400        "--create-from",
 401        type=str,
 402        help="The environment to create the target environment from if it doesn't exist. Default: prod.",
 403    )
 404    @argument(
 405        "--skip-tests",
 406        "-t",
 407        action="store_true",
 408        help="Skip the unit tests defined for the model.",
 409    )
 410    @argument(
 411        "--skip-linter",
 412        action="store_true",
 413        help="Skip the linter for the model.",
 414    )
 415    @argument(
 416        "--restate-model",
 417        "-r",
 418        type=str,
 419        nargs="*",
 420        help="Restate data for specified models (and models downstream from the one specified). For production environment, all related model versions will have their intervals wiped, but only the current versions will be backfilled. For development environment, only the current model versions will be affected.",
 421    )
 422    @argument(
 423        "--no-gaps",
 424        "-g",
 425        action="store_true",
 426        help="Ensure that new snapshots have no data gaps when comparing to existing snapshots for matching models in the target environment.",
 427    )
 428    @argument(
 429        "--skip-backfill",
 430        "--dry-run",
 431        action="store_true",
 432        help="Skip the backfill step and only create a virtual update for the plan.",
 433    )
 434    @argument(
 435        "--empty-backfill",
 436        action="store_true",
 437        help="Produce empty backfill. Like --skip-backfill no models will be backfilled, unlike --skip-backfill missing intervals will be recorded as if they were backfilled.",
 438    )
 439    @argument(
 440        "--forward-only",
 441        action="store_true",
 442        help="Create a plan for forward-only changes.",
 443        default=None,
 444    )
 445    @argument(
 446        "--effective-from",
 447        type=str,
 448        help="The effective date from which to apply forward-only changes on production.",
 449    )
 450    @argument(
 451        "--no-prompts",
 452        action="store_true",
 453        help="Disables interactive prompts for the backfill time range. Please note that if this flag is set and there are uncategorized changes, plan creation will fail.",
 454        default=None,
 455    )
 456    @argument(
 457        "--auto-apply",
 458        action="store_true",
 459        help="Automatically applies the new plan after creation.",
 460        default=None,
 461    )
 462    @argument(
 463        "--no-auto-categorization",
 464        action="store_true",
 465        help="Disable automatic change categorization.",
 466        default=None,
 467    )
 468    @argument(
 469        "--include-unmodified",
 470        action="store_true",
 471        help="Include unmodified models in the target environment.",
 472        default=None,
 473    )
 474    @argument(
 475        "--select-model",
 476        type=str,
 477        nargs="*",
 478        help="Select specific model changes that should be included in the plan.",
 479    )
 480    @argument(
 481        "--backfill-model",
 482        type=str,
 483        nargs="*",
 484        help="Backfill only the models whose names match the expression.",
 485    )
 486    @argument(
 487        "--no-diff",
 488        action="store_true",
 489        help="Hide text differences for changed models.",
 490        default=None,
 491    )
 492    @argument(
 493        "--run",
 494        action="store_true",
 495        help="Run latest intervals as part of the plan application (prod environment only).",
 496    )
 497    @argument(
 498        "--ignore-cron",
 499        action="store_true",
 500        help="Run for all missing intervals, ignoring individual cron schedules. Only applies if --run is set.",
 501        default=None,
 502    )
 503    @argument(
 504        "--enable-preview",
 505        action="store_true",
 506        help="Enable preview for forward-only models when targeting a development environment.",
 507        default=None,
 508    )
 509    @argument(
 510        "--diff-rendered",
 511        action="store_true",
 512        help="Output text differences for the rendered versions of the models and standalone audits",
 513    )
 514    @argument(
 515        "--verbose",
 516        "-v",
 517        action="count",
 518        default=0,
 519        help="Verbose output. Use -vv for very verbose.",
 520    )
 521    @line_magic
 522    @pass_sqlmesh_context
 523    def plan(self, context: Context, line: str) -> None:
 524        """Goes through a set of prompts to both establish a plan and apply it"""
 525        args = parse_argstring(self.plan, line)
 526
 527        setattr(context.console, "verbosity", Verbosity(args.verbose))
 528
 529        context.plan(
 530            args.environment,
 531            start=args.start,
 532            end=args.end,
 533            execution_time=args.execution_time,
 534            create_from=args.create_from,
 535            skip_tests=args.skip_tests,
 536            restate_models=args.restate_model,
 537            backfill_models=args.backfill_model,
 538            no_gaps=args.no_gaps,
 539            skip_backfill=args.skip_backfill,
 540            empty_backfill=args.empty_backfill,
 541            forward_only=args.forward_only,
 542            no_prompts=args.no_prompts,
 543            auto_apply=args.auto_apply,
 544            no_auto_categorization=args.no_auto_categorization,
 545            effective_from=args.effective_from,
 546            include_unmodified=args.include_unmodified,
 547            select_models=args.select_model,
 548            no_diff=args.no_diff,
 549            run=args.run,
 550            ignore_cron=args.run,
 551            enable_preview=args.enable_preview,
 552            diff_rendered=args.diff_rendered,
 553        )
 554
 555    @magic_arguments()
 556    @argument(
 557        "environment",
 558        nargs="?",
 559        type=str,
 560        help="The environment to run against",
 561    )
 562    @argument("--start", "-s", type=str, help="Start date to evaluate.")
 563    @argument("--end", "-e", type=str, help="End date to evaluate.")
 564    @argument("--skip-janitor", action="store_true", help="Skip the janitor task.")
 565    @argument(
 566        "--ignore-cron",
 567        action="store_true",
 568        help="Run for all missing intervals, ignoring individual cron schedules.",
 569    )
 570    @argument(
 571        "--select-model",
 572        type=str,
 573        nargs="*",
 574        help="Select specific models to run. Note: this always includes upstream dependencies.",
 575    )
 576    @argument(
 577        "--exit-on-env-update",
 578        type=int,
 579        help="If set, the command will exit with the specified code if the run is interrupted by an update to the target environment.",
 580    )
 581    @argument(
 582        "--no-auto-upstream",
 583        action="store_true",
 584        help="Do not automatically include upstream models. Only applicable when --select-model is used. Note: this may result in missing / invalid data for the selected models.",
 585    )
 586    @line_magic
 587    @pass_sqlmesh_context
 588    def run_dag(self, context: Context, line: str) -> None:
 589        """Evaluate the DAG of models using the built-in scheduler."""
 590        args = parse_argstring(self.run_dag, line)
 591
 592        completion_status = context.run(
 593            args.environment,
 594            start=args.start,
 595            end=args.end,
 596            skip_janitor=args.skip_janitor,
 597            ignore_cron=args.ignore_cron,
 598            select_models=args.select_model,
 599            exit_on_env_update=args.exit_on_env_update,
 600            no_auto_upstream=args.no_auto_upstream,
 601        )
 602        if completion_status.is_failure:
 603            raise SQLMeshError("Error Running DAG. Check logs for details.")
 604
 605    @magic_arguments()
 606    @argument("model", type=str, help="The model.")
 607    @argument("--start", "-s", type=str, help="Start date to render.")
 608    @argument("--end", "-e", type=str, help="End date to render.")
 609    @argument("--execution-time", type=str, help="Execution time.")
 610    @argument(
 611        "--limit",
 612        type=int,
 613        help="The number of rows which the query should be limited to.",
 614    )
 615    @line_magic
 616    @pass_sqlmesh_context
 617    def evaluate(self, context: Context, line: str) -> None:
 618        """Evaluate a model query and fetches a dataframe."""
 619        context.refresh()
 620
 621        snowpark = optional_import("snowflake.snowpark")
 622        args = parse_argstring(self.evaluate, line)
 623
 624        df = context.evaluate(
 625            args.model,
 626            start=args.start,
 627            end=args.end,
 628            execution_time=args.execution_time,
 629            limit=args.limit,
 630        )
 631
 632        if snowpark and isinstance(df, snowpark.DataFrame):
 633            df = df.limit(args.limit or 100).to_pandas()
 634
 635        self.display(df)
 636
 637    @magic_arguments()
 638    @argument("model", type=str, help="The model.")
 639    @argument("--start", "-s", type=str, help="Start date to render.")
 640    @argument("--end", "-e", type=str, help="End date to render.")
 641    @argument("--execution-time", type=str, help="Execution time.")
 642    @argument(
 643        "--expand",
 644        type=parse_expand,
 645        help="Whether or not to use expand materialized models, defaults to False. If 'true', all referenced models are expanded as raw queries. If a comma-separated list of model names, only those models are expanded as raw queries.",
 646    )
 647    @argument("--dialect", type=str, help="SQL dialect to render.")
 648    @argument("--no-format", action="store_true", help="Disable fancy formatting of the query.")
 649    @format_arguments
 650    @line_magic
 651    @pass_sqlmesh_context
 652    def render(self, context: Context, line: str) -> None:
 653        """Renders a model's query, optionally expanding referenced models."""
 654        context.refresh()
 655        render_opts = vars(parse_argstring(self.render, line))
 656        model = render_opts.pop("model")
 657        dialect = render_opts.pop("dialect", None)
 658        expand = render_opts.pop("expand", False)
 659
 660        model = context.get_model(model, raise_if_missing=True)
 661
 662        query = context.render(
 663            model,
 664            start=render_opts.pop("start", None),
 665            end=render_opts.pop("end", None),
 666            execution_time=render_opts.pop("execution_time", None),
 667            expand=expand,
 668        )
 669
 670        no_format = render_opts.pop("no_format", False)
 671
 672        format_config = context.config_for_node(model).format
 673        format_options = {
 674            **format_config.generator_options,
 675            **{k: v for k, v in render_opts.items() if v is not None},
 676        }
 677
 678        sql = query.sql(
 679            pretty=True,
 680            dialect=context.config.dialect if dialect is None else dialect,
 681            **format_options,
 682        )
 683
 684        if no_format:
 685            context.console.log_status_update(sql)
 686        else:
 687            context.console.show_sql(sql)
 688
 689    @magic_arguments()
 690    @argument(
 691        "df_var",
 692        default=None,
 693        nargs="?",
 694        type=str,
 695        help="An optional variable name to store the resulting dataframe.",
 696    )
 697    @cell_magic
 698    @pass_sqlmesh_context
 699    def fetchdf(self, context: Context, line: str, sql: str) -> None:
 700        """Fetches a dataframe from sql, optionally storing it in a variable."""
 701        args = parse_argstring(self.fetchdf, line)
 702        df = context.fetchdf(sql)
 703        if args.df_var:
 704            self._shell.user_ns[args.df_var] = df
 705        self.display(df)
 706
 707    @magic_arguments()
 708    @argument("--file", "-f", type=str, help="An optional file path to write the HTML output to.")
 709    @argument(
 710        "--select-model",
 711        type=str,
 712        nargs="*",
 713        help="Select specific models to include in the dag.",
 714    )
 715    @line_magic
 716    @pass_sqlmesh_context
 717    def dag(self, context: Context, line: str) -> None:
 718        """Displays the HTML DAG."""
 719        args = parse_argstring(self.dag, line)
 720        dag = context.get_dag(args.select_model)
 721        if args.file:
 722            with open(args.file, "w", encoding="utf-8") as file:
 723                file.write(str(dag))
 724        # TODO: Have this go through console instead of calling display directly
 725        self.display(dag)
 726
 727    @magic_arguments()
 728    @line_magic
 729    @pass_sqlmesh_context
 730    def migrate(self, context: Context, line: str) -> None:
 731        """Migrate SQLMesh to the current running version."""
 732        context.migrate()
 733        context.console.log_success("Migration complete")
 734
 735    @magic_arguments()
 736    @argument(
 737        "--strict",
 738        action="store_true",
 739        help="Raise an error if the external model is missing in the database",
 740    )
 741    @line_magic
 742    @pass_sqlmesh_context
 743    def create_external_models(self, context: Context, line: str) -> None:
 744        """Create a schema file containing external model schemas."""
 745        args = parse_argstring(self.create_external_models, line)
 746        context.create_external_models(strict=args.strict)
 747
 748    @magic_arguments()
 749    @argument(
 750        "source_to_target",
 751        type=str,
 752        metavar="SOURCE:TARGET",
 753        help="Source and target in `SOURCE:TARGET` format",
 754    )
 755    @argument(
 756        "--on",
 757        type=str,
 758        nargs="*",
 759        help="The column to join on. Can be specified multiple times. The model grain will be used if not specified.",
 760    )
 761    @argument(
 762        "--skip-columns",
 763        type=str,
 764        nargs="*",
 765        help="The column(s) to skip when comparing the source and target table.",
 766    )
 767    @argument(
 768        "--model",
 769        type=str,
 770        help="The model to diff against when source and target are environments and not tables.",
 771    )
 772    @argument(
 773        "--where",
 774        type=str,
 775        help="An optional where statement to filter results.",
 776    )
 777    @argument(
 778        "--limit",
 779        type=int,
 780        default=20,
 781        help="The limit of the sample dataframe.",
 782    )
 783    @argument(
 784        "--show-sample",
 785        action="store_true",
 786        help="Show a sample of the rows that differ. With many columns, the output can be very wide.",
 787    )
 788    @argument(
 789        "--decimals",
 790        type=int,
 791        default=3,
 792        help="The number of decimal places to keep when comparing floating point columns. Default: 3",
 793    )
 794    @argument(
 795        "--select-model",
 796        type=str,
 797        nargs="*",
 798        help="Specify one or more models to data diff. Use wildcards to diff multiple models. Ex: '*' (all models with applied plan diffs), 'demo.model+' (this and downstream models), 'git:feature_branch' (models with direct modifications in this branch only)",
 799    )
 800    @argument(
 801        "--skip-grain-check",
 802        action="store_true",
 803        help="Disable the check for a primary key (grain) that is missing or is not unique.",
 804    )
 805    @argument(
 806        "--warn-grain-check",
 807        action="store_true",
 808        help="Warn if any selected model is missing a grain, and compute diffs for the remaining models.",
 809    )
 810    @argument(
 811        "--schema-diff-ignore-case",
 812        action="store_true",
 813        help="If set, when performing a schema diff the case of column names is ignored when matching between the two schemas. For example, 'col_a' in the source schema and 'COL_A' in the target schema will be treated as the same column.",
 814    )
 815    @line_magic
 816    @pass_sqlmesh_context
 817    def table_diff(self, context: Context, line: str) -> None:
 818        """Show the diff between two tables.
 819
 820        Can either be two tables or two environments and a model.
 821        """
 822        args = parse_argstring(self.table_diff, line)
 823        source, target = args.source_to_target.split(":")
 824        select_models = {args.model} if args.model else args.select_model or None
 825        context.table_diff(
 826            source=source,
 827            target=target,
 828            on=args.on,
 829            skip_columns=args.skip_columns,
 830            select_models=select_models,
 831            where=args.where,
 832            limit=args.limit,
 833            show_sample=args.show_sample,
 834            decimals=args.decimals,
 835            skip_grain_check=args.skip_grain_check,
 836            warn_grain_check=args.warn_grain_check,
 837            schema_diff_ignore_case=args.schema_diff_ignore_case,
 838        )
 839
 840    @magic_arguments()
 841    @argument(
 842        "model_name",
 843        nargs="?",
 844        type=str,
 845        help="The name of the model to get the table name for.",
 846    )
 847    @argument(
 848        "--environment",
 849        type=str,
 850        help="The environment to source the model version from.",
 851    )
 852    @argument(
 853        "--prod",
 854        action="store_true",
 855        help="If set, return the name of the physical table that will be used in production for the model version promoted in the target environment.",
 856    )
 857    @line_magic
 858    @pass_sqlmesh_context
 859    def table_name(self, context: Context, line: str) -> None:
 860        """Prints the name of the physical table for the given model."""
 861        args = parse_argstring(self.table_name, line)
 862        context.console.log_status_update(
 863            context.table_name(args.model_name, args.environment, args.prod)
 864        )
 865
 866    @magic_arguments()
 867    @argument(
 868        "pipeline",
 869        nargs="?",
 870        type=str,
 871        help="The dlt pipeline to attach for this SQLMesh project.",
 872    )
 873    @argument(
 874        "--table",
 875        "-t",
 876        type=str,
 877        nargs="*",
 878        help="The specific dlt tables to refresh in the SQLMesh models.",
 879    )
 880    @argument(
 881        "--force",
 882        "-f",
 883        action="store_true",
 884        help="If set, existing models are overwritten with the new DLT tables.",
 885    )
 886    @argument(
 887        "--dlt-path",
 888        type=str,
 889        help="The DLT pipelines working directory, where DLT stores pipeline state (by default ~/.dlt/pipelines).",
 890    )
 891    @line_magic
 892    @pass_sqlmesh_context
 893    def dlt_refresh(self, context: Context, line: str) -> None:
 894        """Attaches to a DLT pipeline with the option to update specific or all missing tables in the SQLMesh project."""
 895        from sqlmesh.integrations.dlt import generate_dlt_models
 896
 897        args = parse_argstring(self.dlt_refresh, line)
 898        sqlmesh_models = generate_dlt_models(
 899            context, args.pipeline, list(args.table or []), args.force, args.dlt_path
 900        )
 901        if sqlmesh_models:
 902            model_names = "\n".join([f"- {model_name}" for model_name in sqlmesh_models])
 903            context.console.log_success(f"Updated SQLMesh project with models:\n{model_names}")
 904        else:
 905            context.console.log_success("All SQLMesh models are up to date.")
 906
 907    @magic_arguments()
 908    @argument(
 909        "--read",
 910        type=str,
 911        default="",
 912        help="The input dialect of the sql string.",
 913    )
 914    @argument(
 915        "--write",
 916        type=str,
 917        default="",
 918        help="The output dialect of the sql string.",
 919    )
 920    @line_cell_magic
 921    @pass_sqlmesh_context
 922    def rewrite(self, context: Context, line: str, sql: str) -> None:
 923        """Rewrite a sql expression with semantic references into an executable query.
 924
 925        https://sqlmesh.readthedocs.io/en/latest/concepts/metrics/overview/
 926        """
 927        args = parse_argstring(self.rewrite, line)
 928        context.console.show_sql(
 929            context.rewrite(sql, args.read).sql(
 930                dialect=args.write or context.config.dialect, pretty=True
 931            )
 932        )
 933
 934    @magic_arguments()
 935    @argument(
 936        "--transpile",
 937        "-t",
 938        type=str,
 939        help="Transpile project models to the specified dialect.",
 940    )
 941    @argument(
 942        "--check",
 943        action="store_true",
 944        help="Whether or not to check formatting (but not actually format anything).",
 945        default=None,
 946    )
 947    @argument(
 948        "--append-newline",
 949        action="store_true",
 950        help="Include a newline at the end of the output.",
 951        default=None,
 952    )
 953    @argument(
 954        "--no-rewrite-casts",
 955        action="store_true",
 956        help="Preserve the existing casts, without rewriting them to use the :: syntax.",
 957        default=None,
 958    )
 959    @format_arguments
 960    @line_magic
 961    @pass_sqlmesh_context
 962    def format(self, context: Context, line: str) -> bool:
 963        """Format all SQL models and audits."""
 964        format_opts = vars(parse_argstring(self.format, line))
 965        if format_opts.pop("no_rewrite_casts", None):
 966            format_opts["rewrite_casts"] = False
 967
 968        return context.format(**{k: v for k, v in format_opts.items() if v is not None})
 969
 970    @magic_arguments()
 971    @argument("environment", type=str, help="The environment to diff local state against.")
 972    @line_magic
 973    @pass_sqlmesh_context
 974    def diff(self, context: Context, line: str) -> None:
 975        """Show the diff between the local state and the target environment."""
 976        args = parse_argstring(self.diff, line)
 977        context.diff(args.environment)
 978
 979    @magic_arguments()
 980    @argument("environment", type=str, help="The environment to invalidate.")
 981    @line_magic
 982    @pass_sqlmesh_context
 983    def invalidate(self, context: Context, line: str) -> None:
 984        """Invalidate the target environment, forcing its removal during the next run of the janitor process."""
 985        args = parse_argstring(self.invalidate, line)
 986        context.invalidate_environment(args.environment)
 987
 988    @magic_arguments()
 989    @argument(
 990        "--ignore-ttl",
 991        action="store_true",
 992        help="Cleanup snapshots that are not referenced in any environment, regardless of when they're set to expire",
 993    )
 994    @line_magic
 995    @pass_sqlmesh_context
 996    def janitor(self, context: Context, line: str) -> None:
 997        """Run the janitor process to clean up old environments and expired snapshots."""
 998        args = parse_argstring(self.janitor, line)
 999        context.run_janitor(ignore_ttl=args.ignore_ttl)
1000
1001    @magic_arguments()
1002    @argument("model", type=str)
1003    @argument(
1004        "--query",
1005        "-q",
1006        type=str,
1007        nargs="+",
1008        default=[],
1009        help="Queries that will be used to generate data for the model's dependencies.",
1010    )
1011    @argument(
1012        "--overwrite",
1013        "-o",
1014        action="store_true",
1015        help="When true, the fixture file will be overwritten in case it already exists.",
1016    )
1017    @argument(
1018        "--var",
1019        "-v",
1020        type=str,
1021        nargs="+",
1022        help="Key-value pairs that will define variables needed by the model.",
1023    )
1024    @argument(
1025        "--path",
1026        "-p",
1027        type=str,
1028        help="The file path corresponding to the fixture, relative to the test directory. "
1029        "By default, the fixture will be created under the test directory and the file "
1030        "name will be inferred based on the test's name.",
1031    )
1032    @argument(
1033        "--name",
1034        "-n",
1035        type=str,
1036        help="The name of the test that will be created. By default, it's inferred based on the model's name.",
1037    )
1038    @argument(
1039        "--include-ctes",
1040        action="store_true",
1041        help="When true, CTE fixtures will also be generated.",
1042    )
1043    @line_magic
1044    @pass_sqlmesh_context
1045    def create_test(self, context: Context, line: str) -> None:
1046        """Generate a unit test fixture for a given model."""
1047        args = parse_argstring(self.create_test, line)
1048        queries = iter(args.query)
1049        variables = iter(args.var) if args.var else None
1050        context.create_test(
1051            args.model,
1052            input_queries={k: v.strip('"') for k, v in dict(zip(queries, queries)).items()},
1053            overwrite=args.overwrite,
1054            variables=dict(zip(variables, variables)) if variables else None,
1055            path=args.path,
1056            name=args.name,
1057            include_ctes=args.include_ctes,
1058        )
1059
1060    @magic_arguments()
1061    @argument("tests", nargs="*", type=str)
1062    @argument(
1063        "--pattern",
1064        "-k",
1065        nargs="*",
1066        type=str,
1067        help="Only run tests that match the pattern of substring.",
1068    )
1069    @argument(
1070        "--verbose",
1071        "-v",
1072        action="count",
1073        default=0,
1074        help="Verbose output. Use -vv for very verbose.",
1075    )
1076    @argument(
1077        "--preserve-fixtures",
1078        action="store_true",
1079        help="Preserve the fixture tables in the testing database, useful for debugging.",
1080    )
1081    @line_magic
1082    @pass_sqlmesh_context
1083    def run_test(self, context: Context, line: str) -> None:
1084        """Run unit test(s)."""
1085        args = parse_argstring(self.run_test, line)
1086
1087        context.test(
1088            match_patterns=args.pattern,
1089            tests=args.tests,
1090            verbosity=Verbosity(args.verbose),
1091            preserve_fixtures=args.preserve_fixtures,
1092            stream=StringIO(),  # consume the output instead of redirecting to stdout
1093        )
1094
1095    @magic_arguments()
1096    @argument(
1097        "models", type=str, nargs="*", help="A model to audit. Multiple models can be audited."
1098    )
1099    @argument("--start", "-s", type=str, help="Start date to audit.")
1100    @argument("--end", "-e", type=str, help="End date to audit.")
1101    @argument("--execution-time", type=str, help="Execution time.")
1102    @line_magic
1103    @pass_sqlmesh_context
1104    def audit(self, context: Context, line: str) -> bool:
1105        """Run audit(s)"""
1106        args = parse_argstring(self.audit, line)
1107        return context.audit(
1108            models=args.models, start=args.start, end=args.end, execution_time=args.execution_time
1109        )
1110
1111    @magic_arguments()
1112    @argument("environment", nargs="?", type=str, help="The environment to check intervals for.")
1113    @argument(
1114        "--no-signals",
1115        action="store_true",
1116        help="Disable signal checks and only show missing intervals.",
1117        default=False,
1118    )
1119    @argument(
1120        "--select-model",
1121        type=str,
1122        nargs="*",
1123        help="Select specific model changes that should be included in the plan.",
1124    )
1125    @argument("--start", "-s", type=str, help="Start date of intervals to check for.")
1126    @argument("--end", "-e", type=str, help="End date of intervals to check for.")
1127    @line_magic
1128    @pass_sqlmesh_context
1129    def check_intervals(self, context: Context, line: str) -> None:
1130        """Show missing intervals in an environment, respecting signals."""
1131        args = parse_argstring(self.check_intervals, line)
1132
1133        context.console.show_intervals(
1134            context.check_intervals(
1135                environment=args.environment,
1136                no_signals=args.no_signals,
1137                select_models=args.select_model,
1138                start=args.start,
1139                end=args.end,
1140            )
1141        )
1142
1143    @magic_arguments()
1144    @argument(
1145        "--skip-connection",
1146        action="store_true",
1147        help="Skip the connection test.",
1148        default=False,
1149    )
1150    @argument(
1151        "--verbose",
1152        "-v",
1153        action="count",
1154        default=0,
1155        help="Verbose output. Use -vv for very verbose.",
1156    )
1157    @line_magic
1158    @pass_sqlmesh_context
1159    def info(self, context: Context, line: str) -> None:
1160        """Display SQLMesh project information."""
1161        args = parse_argstring(self.info, line)
1162        context.print_info(skip_connection=args.skip_connection, verbosity=Verbosity(args.verbose))
1163
1164    @magic_arguments()
1165    @line_magic
1166    @pass_sqlmesh_context
1167    def rollback(self, context: Context, line: str) -> None:
1168        """Rollback SQLMesh to the previous migration."""
1169        context.rollback()
1170
1171    @magic_arguments()
1172    @line_magic
1173    @pass_sqlmesh_context
1174    def clean(self, context: Context, line: str) -> None:
1175        """Clears the SQLMesh cache and any build artifacts."""
1176        context.clear_caches()
1177        context.console.log_success("SQLMesh cache and build artifacts cleared")
1178
1179    @magic_arguments()
1180    @line_magic
1181    @pass_sqlmesh_context
1182    def environments(self, context: Context, line: str) -> None:
1183        """Prints the list of SQLMesh environments with its expiry datetime."""
1184        context.print_environment_names()
1185
1186    @magic_arguments()
1187    @argument(
1188        "--models",
1189        "--model",
1190        type=str,
1191        nargs="*",
1192        help="A model to lint. Multiple models can be linted. If no models are specified, every model will be linted.",
1193    )
1194    @line_magic
1195    @pass_sqlmesh_context
1196    def lint(self, context: Context, line: str) -> None:
1197        """Run linter for target model(s)"""
1198        args = parse_argstring(self.lint, line)
1199        context.lint_models(args.models)
1200
1201    @magic_arguments()
1202    @line_magic
1203    @pass_sqlmesh_context
1204    def destroy(self, context: Context, line: str) -> None:
1205        """Removes all project resources, engine-managed objects, state tables and clears the SQLMesh cache."""
1206        context.destroy()
1207
1208
1209def register_magics() -> None:
1210    try:
1211        shell = get_ipython()  # type: ignore
1212        shell.register_magics(SQLMeshMagics)
1213    except NameError:
1214        pass
logger = <Logger sqlmesh.magics (WARNING)>
CONTEXT_VARIABLE_NAMES = ['context', 'ctx', 'sqlmesh']
def pass_sqlmesh_context(func: Callable) -> Callable:
52def pass_sqlmesh_context(func: t.Callable) -> t.Callable:
53    @functools.wraps(func)
54    def wrapper(self: SQLMeshMagics, *args: t.Any, **kwargs: t.Any) -> None:
55        for variable_name in CONTEXT_VARIABLE_NAMES:
56            context = self._shell.user_ns.get(variable_name)
57            if isinstance(context, Context):
58                break
59        else:
60            raise MissingContextException(
61                f"Context must be defined and initialized with one of these names: {', '.join(CONTEXT_VARIABLE_NAMES)}"
62            )
63        old_console = context.console
64        new_console = create_console(display=self.display)
65        context.console = new_console
66        set_console(new_console)
67        context.refresh()
68
69        magic_name = func.__name__
70        bound_method = getattr(self, magic_name, None)
71        if bound_method:
72            args_split = arg_split(args[0])
73            parser = bound_method.parser
74
75            original_parser_actions = deepcopy(parser._actions)
76            original_parser_defaults = parser._defaults
77
78            # Temporarily supress default values, otherwise any missing arg would be set and affect analytics
79            parser._defaults = {}
80            for action in parser._actions:
81                action.default = SUPPRESS
82
83            parsed_args, _ = parser.parse_known_args(args_split, Namespace())
84
85            parser._actions = original_parser_actions
86            parser._defaults = original_parser_defaults
87
88            command_args = {k for k, v in parsed_args.__dict__.items() if v is not None}
89            analytics.collector.on_magic_command(command_name=magic_name, command_args=command_args)
90
91        func(self, context, *args, **kwargs)
92
93        context.console = old_console
94        set_console(old_console)
95
96    return wrapper
def parse_expand(value: str) -> Union[bool, List[str]]:
 99def parse_expand(value: str) -> t.Union[bool, t.List[str]]:
100    if value.lower() == "true":
101        return True
102    if value.lower() == "false":
103        return False
104    return [name.strip() for name in value.split(",") if name.strip()]
def format_arguments(func: Callable) -> Callable:
107def format_arguments(func: t.Callable) -> t.Callable:
108    """Decorator to add common format arguments to magic commands."""
109    func = argument(
110        "--normalize",
111        action="store_true",
112        help="Whether or not to normalize identifiers to lowercase.",
113        default=None,
114    )(func)
115    func = argument(
116        "--pad",
117        type=int,
118        help="Determines the pad size in a formatted string.",
119    )(func)
120    func = argument(
121        "--indent",
122        type=int,
123        help="Determines the indentation size in a formatted string.",
124    )(func)
125    func = argument(
126        "--normalize-functions",
127        type=str,
128        help="Whether or not to normalize all function names. Possible values are: 'upper', 'lower'",
129    )(func)
130    func = argument(
131        "--leading-comma",
132        action="store_true",
133        help="Determines whether or not the comma is leading or trailing in select expressions. Default is trailing.",
134        default=None,
135    )(func)
136    func = argument(
137        "--max-text-width",
138        type=int,
139        help="The max number of characters in a segment before creating new lines in pretty mode.",
140    )(func)
141    return func

Decorator to add common format arguments to magic commands.

@magics_class
class SQLMeshMagics(IPython.core.magic.Magics):
 144@magics_class
 145class SQLMeshMagics(Magics):
 146    @property
 147    def display(self) -> t.Callable:
 148        from sqlmesh import RuntimeEnv
 149
 150        if RuntimeEnv.get().is_databricks:
 151            # Use Databricks' special display instead of the normal IPython display
 152            return self._shell.user_ns["display"]
 153        return display
 154
 155    @property
 156    def _shell(self) -> t.Any:
 157        # Make mypy happy.
 158        if not self.shell:
 159            raise RuntimeError("IPython Magics are in invalid state")
 160        return self.shell
 161
 162    @magic_arguments()
 163    @argument(
 164        "paths",
 165        type=str,
 166        nargs="+",
 167        default="",
 168        help="The path(s) to the SQLMesh project(s).",
 169    )
 170    @argument(
 171        "--config",
 172        type=str,
 173        help="Name of the config object. Only applicable to configuration defined using Python script.",
 174    )
 175    @argument("--gateway", type=str, help="The name of the gateway.")
 176    @argument("--ignore-warnings", action="store_true", help="Ignore warnings.")
 177    @argument("--debug", action="store_true", help="Enable debug mode.")
 178    @argument("--log-file-dir", type=str, help="The directory to write the log file to.")
 179    @argument(
 180        "--dotenv", type=str, help="Path to a custom .env file to load environment variables from."
 181    )
 182    @line_magic
 183    def context(self, line: str) -> None:
 184        """Sets the context in the user namespace."""
 185        from sqlmesh import configure_logging, remove_excess_logs
 186
 187        args = parse_argstring(self.context, line)
 188        log_file_dir = args.log_file_dir
 189
 190        configure_logging(
 191            args.debug,
 192            log_file_dir=log_file_dir,
 193            ignore_warnings=args.ignore_warnings,
 194        )
 195        configure_console(ignore_warnings=args.ignore_warnings)
 196
 197        dotenv_path = Path(args.dotenv) if args.dotenv else None
 198        configs = load_configs(
 199            args.config, Context.CONFIG_TYPE, args.paths, dotenv_path=dotenv_path
 200        )
 201        log_limit = list(configs.values())[0].log_limit
 202
 203        remove_excess_logs(log_file_dir, log_limit)
 204
 205        try:
 206            context = Context(paths=args.paths, config=configs, gateway=args.gateway)
 207            self._shell.user_ns["context"] = context
 208        except Exception:
 209            if args.debug:
 210                logger.exception("Failed to initialize SQLMesh context")
 211            raise
 212
 213        context.console.log_success(f"SQLMesh project context set to: {', '.join(args.paths)}")
 214
 215    @magic_arguments()
 216    @argument("path", type=str, help="The path where the new SQLMesh project should be created.")
 217    @argument(
 218        "engine",
 219        type=str,
 220        help=f"Project SQL engine. Supported values: '{', '.join([info[1] for info in sorted(INIT_DISPLAY_INFO_TO_TYPE.values(), key=lambda x: x[0])])}'.",  # type: ignore
 221    )
 222    @argument(
 223        "--template",
 224        "-t",
 225        type=str,
 226        help="Project template. Supported values: dbt, default, empty.",
 227    )
 228    @argument(
 229        "--dlt-pipeline",
 230        type=str,
 231        help="DLT pipeline for which to generate a SQLMesh project. Use alongside template: dlt",
 232    )
 233    @argument(
 234        "--dlt-path",
 235        type=str,
 236        help="The DLT pipelines working directory, where DLT stores pipeline state (by default ~/.dlt/pipelines). Use alongside template: dlt",
 237    )
 238    @line_magic
 239    def init(self, line: str) -> None:
 240        """Creates a SQLMesh project scaffold with a default SQL dialect."""
 241        args = parse_argstring(self.init, line)
 242        try:
 243            project_template = ProjectTemplate(
 244                args.template.lower() if args.template else "default"
 245            )
 246        except ValueError:
 247            raise MagicError(f"Invalid project template '{args.template}'")
 248        init_example_project(
 249            path=args.path,
 250            engine_type=args.engine,
 251            dialect=None,
 252            template=project_template,
 253            pipeline=args.dlt_pipeline,
 254            dlt_path=args.dlt_path,
 255        )
 256        html = str(
 257            h(
 258                "div",
 259                h(
 260                    "span",
 261                    {"style": {"color": "green", "font-weight": "bold"}},
 262                    "SQLMesh project scaffold created",
 263                ),
 264            )
 265        )
 266        self.display(JupyterRenderable(html=html, text=""))
 267
 268    @magic_arguments()
 269    @argument("model", type=str, help="The model.")
 270    @argument("--start", "-s", type=str, help="Start date to render.")
 271    @argument("--end", "-e", type=str, help="End date to render.")
 272    @argument("--execution-time", type=str, help="Execution time.")
 273    @argument("--dialect", "-d", type=str, help="The rendered dialect.")
 274    @line_cell_magic
 275    @pass_sqlmesh_context
 276    def model(self, context: Context, line: str, sql: t.Optional[str] = None) -> None:
 277        """Renders the model and automatically fills in an editable cell with the model definition."""
 278        args = parse_argstring(self.model, line)
 279
 280        model = context.get_model(args.model, raise_if_missing=True)
 281        config = context.config_for_node(model)
 282
 283        if sql:
 284            expressions = parse(sql, default_dialect=config.dialect)
 285            loaded = load_sql_based_model(
 286                expressions,
 287                macros=context._macros,
 288                jinja_macros=context._jinja_macros,
 289                path=model._path,
 290                dialect=config.dialect,
 291                time_column_format=config.time_column_format,
 292                physical_schema_mapping=context.config.physical_schema_mapping,
 293                default_catalog=context.default_catalog,
 294            )
 295
 296            if loaded.name == args.model:
 297                model = loaded
 298        else:
 299            if model._path:
 300                with open(model._path, "r", encoding="utf-8") as file:
 301                    expressions = parse(file.read(), default_dialect=config.dialect)
 302
 303        formatted = format_model_expressions(
 304            expressions,
 305            model.dialect,
 306            rewrite_casts=not config.format.no_rewrite_casts,
 307            **config.format.generator_options,
 308        )
 309
 310        self._shell.set_next_input(
 311            "\n".join(
 312                [
 313                    " ".join(["%%model", line]),
 314                    formatted,
 315                ]
 316            ),
 317            replace=True,
 318        )
 319
 320        if model._path:
 321            with open(model._path, "w", encoding="utf-8") as file:
 322                file.write(formatted)
 323
 324        if sql:
 325            context.console.log_success(f"Model `{args.model}` updated")
 326
 327        context.upsert_model(model)
 328        context.console.show_sql(
 329            context.render(
 330                model.name,
 331                start=args.start,
 332                end=args.end,
 333                execution_time=args.execution_time,
 334            ).sql(pretty=True, dialect=args.dialect or model.dialect)
 335        )
 336
 337    @magic_arguments()
 338    @argument("model", type=str, help="The model.")
 339    @argument("test_name", type=str, nargs="?", default=None, help="The test name to display")
 340    @argument("--ls", action="store_true", help="List tests associated with a model")
 341    @line_cell_magic
 342    @pass_sqlmesh_context
 343    def test(self, context: Context, line: str, test_def_raw: t.Optional[str] = None) -> None:
 344        """Allow the user to list tests for a model, output a specific test, and then write their changes back"""
 345        args = parse_argstring(self.test, line)
 346        if not args.test_name and not args.ls:
 347            raise MagicError("Must provide either test name or `--ls` to list tests")
 348
 349        test_meta = context.select_tests()
 350
 351        tests: t.Dict[str, t.Dict[str, ModelTestMetadata]] = defaultdict(dict)
 352        for model_test_metadata in test_meta:
 353            model = model_test_metadata.body.get("model")
 354            if not model:
 355                context.console.log_error(
 356                    f"Test found that does not have `model` defined: {model_test_metadata.path}"
 357                )
 358            else:
 359                tests[model][model_test_metadata.test_name] = model_test_metadata
 360
 361        model = context.get_model(args.model, raise_if_missing=True)
 362
 363        if args.ls:
 364            # TODO: Provide better UI for displaying tests
 365            for test_name in tests[model.name]:
 366                context.console.log_status_update(test_name)
 367            return
 368
 369        test = tests[model.name][args.test_name]
 370        test_def = yaml.load(test_def_raw) if test_def_raw else test.body
 371        test_def_output = yaml.dump(test_def)
 372
 373        self._shell.set_next_input(
 374            "\n".join(
 375                [
 376                    " ".join(["%%test", line]),
 377                    test_def_output,
 378                ]
 379            ),
 380            replace=True,
 381        )
 382
 383        with open(test.path, "r+", encoding="utf-8") as file:
 384            content = yaml.load(file.read())
 385            content[args.test_name] = test_def
 386            file.seek(0)
 387            yaml.dump(content, file)
 388            file.truncate()
 389
 390    @magic_arguments()
 391    @argument(
 392        "environment",
 393        nargs="?",
 394        type=str,
 395        help="The environment to run the plan against",
 396    )
 397    @argument("--start", "-s", type=str, help="Start date to backfill.")
 398    @argument("--end", "-e", type=str, help="End date to backfill.")
 399    @argument("--execution-time", type=str, help="Execution time.")
 400    @argument(
 401        "--create-from",
 402        type=str,
 403        help="The environment to create the target environment from if it doesn't exist. Default: prod.",
 404    )
 405    @argument(
 406        "--skip-tests",
 407        "-t",
 408        action="store_true",
 409        help="Skip the unit tests defined for the model.",
 410    )
 411    @argument(
 412        "--skip-linter",
 413        action="store_true",
 414        help="Skip the linter for the model.",
 415    )
 416    @argument(
 417        "--restate-model",
 418        "-r",
 419        type=str,
 420        nargs="*",
 421        help="Restate data for specified models (and models downstream from the one specified). For production environment, all related model versions will have their intervals wiped, but only the current versions will be backfilled. For development environment, only the current model versions will be affected.",
 422    )
 423    @argument(
 424        "--no-gaps",
 425        "-g",
 426        action="store_true",
 427        help="Ensure that new snapshots have no data gaps when comparing to existing snapshots for matching models in the target environment.",
 428    )
 429    @argument(
 430        "--skip-backfill",
 431        "--dry-run",
 432        action="store_true",
 433        help="Skip the backfill step and only create a virtual update for the plan.",
 434    )
 435    @argument(
 436        "--empty-backfill",
 437        action="store_true",
 438        help="Produce empty backfill. Like --skip-backfill no models will be backfilled, unlike --skip-backfill missing intervals will be recorded as if they were backfilled.",
 439    )
 440    @argument(
 441        "--forward-only",
 442        action="store_true",
 443        help="Create a plan for forward-only changes.",
 444        default=None,
 445    )
 446    @argument(
 447        "--effective-from",
 448        type=str,
 449        help="The effective date from which to apply forward-only changes on production.",
 450    )
 451    @argument(
 452        "--no-prompts",
 453        action="store_true",
 454        help="Disables interactive prompts for the backfill time range. Please note that if this flag is set and there are uncategorized changes, plan creation will fail.",
 455        default=None,
 456    )
 457    @argument(
 458        "--auto-apply",
 459        action="store_true",
 460        help="Automatically applies the new plan after creation.",
 461        default=None,
 462    )
 463    @argument(
 464        "--no-auto-categorization",
 465        action="store_true",
 466        help="Disable automatic change categorization.",
 467        default=None,
 468    )
 469    @argument(
 470        "--include-unmodified",
 471        action="store_true",
 472        help="Include unmodified models in the target environment.",
 473        default=None,
 474    )
 475    @argument(
 476        "--select-model",
 477        type=str,
 478        nargs="*",
 479        help="Select specific model changes that should be included in the plan.",
 480    )
 481    @argument(
 482        "--backfill-model",
 483        type=str,
 484        nargs="*",
 485        help="Backfill only the models whose names match the expression.",
 486    )
 487    @argument(
 488        "--no-diff",
 489        action="store_true",
 490        help="Hide text differences for changed models.",
 491        default=None,
 492    )
 493    @argument(
 494        "--run",
 495        action="store_true",
 496        help="Run latest intervals as part of the plan application (prod environment only).",
 497    )
 498    @argument(
 499        "--ignore-cron",
 500        action="store_true",
 501        help="Run for all missing intervals, ignoring individual cron schedules. Only applies if --run is set.",
 502        default=None,
 503    )
 504    @argument(
 505        "--enable-preview",
 506        action="store_true",
 507        help="Enable preview for forward-only models when targeting a development environment.",
 508        default=None,
 509    )
 510    @argument(
 511        "--diff-rendered",
 512        action="store_true",
 513        help="Output text differences for the rendered versions of the models and standalone audits",
 514    )
 515    @argument(
 516        "--verbose",
 517        "-v",
 518        action="count",
 519        default=0,
 520        help="Verbose output. Use -vv for very verbose.",
 521    )
 522    @line_magic
 523    @pass_sqlmesh_context
 524    def plan(self, context: Context, line: str) -> None:
 525        """Goes through a set of prompts to both establish a plan and apply it"""
 526        args = parse_argstring(self.plan, line)
 527
 528        setattr(context.console, "verbosity", Verbosity(args.verbose))
 529
 530        context.plan(
 531            args.environment,
 532            start=args.start,
 533            end=args.end,
 534            execution_time=args.execution_time,
 535            create_from=args.create_from,
 536            skip_tests=args.skip_tests,
 537            restate_models=args.restate_model,
 538            backfill_models=args.backfill_model,
 539            no_gaps=args.no_gaps,
 540            skip_backfill=args.skip_backfill,
 541            empty_backfill=args.empty_backfill,
 542            forward_only=args.forward_only,
 543            no_prompts=args.no_prompts,
 544            auto_apply=args.auto_apply,
 545            no_auto_categorization=args.no_auto_categorization,
 546            effective_from=args.effective_from,
 547            include_unmodified=args.include_unmodified,
 548            select_models=args.select_model,
 549            no_diff=args.no_diff,
 550            run=args.run,
 551            ignore_cron=args.run,
 552            enable_preview=args.enable_preview,
 553            diff_rendered=args.diff_rendered,
 554        )
 555
 556    @magic_arguments()
 557    @argument(
 558        "environment",
 559        nargs="?",
 560        type=str,
 561        help="The environment to run against",
 562    )
 563    @argument("--start", "-s", type=str, help="Start date to evaluate.")
 564    @argument("--end", "-e", type=str, help="End date to evaluate.")
 565    @argument("--skip-janitor", action="store_true", help="Skip the janitor task.")
 566    @argument(
 567        "--ignore-cron",
 568        action="store_true",
 569        help="Run for all missing intervals, ignoring individual cron schedules.",
 570    )
 571    @argument(
 572        "--select-model",
 573        type=str,
 574        nargs="*",
 575        help="Select specific models to run. Note: this always includes upstream dependencies.",
 576    )
 577    @argument(
 578        "--exit-on-env-update",
 579        type=int,
 580        help="If set, the command will exit with the specified code if the run is interrupted by an update to the target environment.",
 581    )
 582    @argument(
 583        "--no-auto-upstream",
 584        action="store_true",
 585        help="Do not automatically include upstream models. Only applicable when --select-model is used. Note: this may result in missing / invalid data for the selected models.",
 586    )
 587    @line_magic
 588    @pass_sqlmesh_context
 589    def run_dag(self, context: Context, line: str) -> None:
 590        """Evaluate the DAG of models using the built-in scheduler."""
 591        args = parse_argstring(self.run_dag, line)
 592
 593        completion_status = context.run(
 594            args.environment,
 595            start=args.start,
 596            end=args.end,
 597            skip_janitor=args.skip_janitor,
 598            ignore_cron=args.ignore_cron,
 599            select_models=args.select_model,
 600            exit_on_env_update=args.exit_on_env_update,
 601            no_auto_upstream=args.no_auto_upstream,
 602        )
 603        if completion_status.is_failure:
 604            raise SQLMeshError("Error Running DAG. Check logs for details.")
 605
 606    @magic_arguments()
 607    @argument("model", type=str, help="The model.")
 608    @argument("--start", "-s", type=str, help="Start date to render.")
 609    @argument("--end", "-e", type=str, help="End date to render.")
 610    @argument("--execution-time", type=str, help="Execution time.")
 611    @argument(
 612        "--limit",
 613        type=int,
 614        help="The number of rows which the query should be limited to.",
 615    )
 616    @line_magic
 617    @pass_sqlmesh_context
 618    def evaluate(self, context: Context, line: str) -> None:
 619        """Evaluate a model query and fetches a dataframe."""
 620        context.refresh()
 621
 622        snowpark = optional_import("snowflake.snowpark")
 623        args = parse_argstring(self.evaluate, line)
 624
 625        df = context.evaluate(
 626            args.model,
 627            start=args.start,
 628            end=args.end,
 629            execution_time=args.execution_time,
 630            limit=args.limit,
 631        )
 632
 633        if snowpark and isinstance(df, snowpark.DataFrame):
 634            df = df.limit(args.limit or 100).to_pandas()
 635
 636        self.display(df)
 637
 638    @magic_arguments()
 639    @argument("model", type=str, help="The model.")
 640    @argument("--start", "-s", type=str, help="Start date to render.")
 641    @argument("--end", "-e", type=str, help="End date to render.")
 642    @argument("--execution-time", type=str, help="Execution time.")
 643    @argument(
 644        "--expand",
 645        type=parse_expand,
 646        help="Whether or not to use expand materialized models, defaults to False. If 'true', all referenced models are expanded as raw queries. If a comma-separated list of model names, only those models are expanded as raw queries.",
 647    )
 648    @argument("--dialect", type=str, help="SQL dialect to render.")
 649    @argument("--no-format", action="store_true", help="Disable fancy formatting of the query.")
 650    @format_arguments
 651    @line_magic
 652    @pass_sqlmesh_context
 653    def render(self, context: Context, line: str) -> None:
 654        """Renders a model's query, optionally expanding referenced models."""
 655        context.refresh()
 656        render_opts = vars(parse_argstring(self.render, line))
 657        model = render_opts.pop("model")
 658        dialect = render_opts.pop("dialect", None)
 659        expand = render_opts.pop("expand", False)
 660
 661        model = context.get_model(model, raise_if_missing=True)
 662
 663        query = context.render(
 664            model,
 665            start=render_opts.pop("start", None),
 666            end=render_opts.pop("end", None),
 667            execution_time=render_opts.pop("execution_time", None),
 668            expand=expand,
 669        )
 670
 671        no_format = render_opts.pop("no_format", False)
 672
 673        format_config = context.config_for_node(model).format
 674        format_options = {
 675            **format_config.generator_options,
 676            **{k: v for k, v in render_opts.items() if v is not None},
 677        }
 678
 679        sql = query.sql(
 680            pretty=True,
 681            dialect=context.config.dialect if dialect is None else dialect,
 682            **format_options,
 683        )
 684
 685        if no_format:
 686            context.console.log_status_update(sql)
 687        else:
 688            context.console.show_sql(sql)
 689
 690    @magic_arguments()
 691    @argument(
 692        "df_var",
 693        default=None,
 694        nargs="?",
 695        type=str,
 696        help="An optional variable name to store the resulting dataframe.",
 697    )
 698    @cell_magic
 699    @pass_sqlmesh_context
 700    def fetchdf(self, context: Context, line: str, sql: str) -> None:
 701        """Fetches a dataframe from sql, optionally storing it in a variable."""
 702        args = parse_argstring(self.fetchdf, line)
 703        df = context.fetchdf(sql)
 704        if args.df_var:
 705            self._shell.user_ns[args.df_var] = df
 706        self.display(df)
 707
 708    @magic_arguments()
 709    @argument("--file", "-f", type=str, help="An optional file path to write the HTML output to.")
 710    @argument(
 711        "--select-model",
 712        type=str,
 713        nargs="*",
 714        help="Select specific models to include in the dag.",
 715    )
 716    @line_magic
 717    @pass_sqlmesh_context
 718    def dag(self, context: Context, line: str) -> None:
 719        """Displays the HTML DAG."""
 720        args = parse_argstring(self.dag, line)
 721        dag = context.get_dag(args.select_model)
 722        if args.file:
 723            with open(args.file, "w", encoding="utf-8") as file:
 724                file.write(str(dag))
 725        # TODO: Have this go through console instead of calling display directly
 726        self.display(dag)
 727
 728    @magic_arguments()
 729    @line_magic
 730    @pass_sqlmesh_context
 731    def migrate(self, context: Context, line: str) -> None:
 732        """Migrate SQLMesh to the current running version."""
 733        context.migrate()
 734        context.console.log_success("Migration complete")
 735
 736    @magic_arguments()
 737    @argument(
 738        "--strict",
 739        action="store_true",
 740        help="Raise an error if the external model is missing in the database",
 741    )
 742    @line_magic
 743    @pass_sqlmesh_context
 744    def create_external_models(self, context: Context, line: str) -> None:
 745        """Create a schema file containing external model schemas."""
 746        args = parse_argstring(self.create_external_models, line)
 747        context.create_external_models(strict=args.strict)
 748
 749    @magic_arguments()
 750    @argument(
 751        "source_to_target",
 752        type=str,
 753        metavar="SOURCE:TARGET",
 754        help="Source and target in `SOURCE:TARGET` format",
 755    )
 756    @argument(
 757        "--on",
 758        type=str,
 759        nargs="*",
 760        help="The column to join on. Can be specified multiple times. The model grain will be used if not specified.",
 761    )
 762    @argument(
 763        "--skip-columns",
 764        type=str,
 765        nargs="*",
 766        help="The column(s) to skip when comparing the source and target table.",
 767    )
 768    @argument(
 769        "--model",
 770        type=str,
 771        help="The model to diff against when source and target are environments and not tables.",
 772    )
 773    @argument(
 774        "--where",
 775        type=str,
 776        help="An optional where statement to filter results.",
 777    )
 778    @argument(
 779        "--limit",
 780        type=int,
 781        default=20,
 782        help="The limit of the sample dataframe.",
 783    )
 784    @argument(
 785        "--show-sample",
 786        action="store_true",
 787        help="Show a sample of the rows that differ. With many columns, the output can be very wide.",
 788    )
 789    @argument(
 790        "--decimals",
 791        type=int,
 792        default=3,
 793        help="The number of decimal places to keep when comparing floating point columns. Default: 3",
 794    )
 795    @argument(
 796        "--select-model",
 797        type=str,
 798        nargs="*",
 799        help="Specify one or more models to data diff. Use wildcards to diff multiple models. Ex: '*' (all models with applied plan diffs), 'demo.model+' (this and downstream models), 'git:feature_branch' (models with direct modifications in this branch only)",
 800    )
 801    @argument(
 802        "--skip-grain-check",
 803        action="store_true",
 804        help="Disable the check for a primary key (grain) that is missing or is not unique.",
 805    )
 806    @argument(
 807        "--warn-grain-check",
 808        action="store_true",
 809        help="Warn if any selected model is missing a grain, and compute diffs for the remaining models.",
 810    )
 811    @argument(
 812        "--schema-diff-ignore-case",
 813        action="store_true",
 814        help="If set, when performing a schema diff the case of column names is ignored when matching between the two schemas. For example, 'col_a' in the source schema and 'COL_A' in the target schema will be treated as the same column.",
 815    )
 816    @line_magic
 817    @pass_sqlmesh_context
 818    def table_diff(self, context: Context, line: str) -> None:
 819        """Show the diff between two tables.
 820
 821        Can either be two tables or two environments and a model.
 822        """
 823        args = parse_argstring(self.table_diff, line)
 824        source, target = args.source_to_target.split(":")
 825        select_models = {args.model} if args.model else args.select_model or None
 826        context.table_diff(
 827            source=source,
 828            target=target,
 829            on=args.on,
 830            skip_columns=args.skip_columns,
 831            select_models=select_models,
 832            where=args.where,
 833            limit=args.limit,
 834            show_sample=args.show_sample,
 835            decimals=args.decimals,
 836            skip_grain_check=args.skip_grain_check,
 837            warn_grain_check=args.warn_grain_check,
 838            schema_diff_ignore_case=args.schema_diff_ignore_case,
 839        )
 840
 841    @magic_arguments()
 842    @argument(
 843        "model_name",
 844        nargs="?",
 845        type=str,
 846        help="The name of the model to get the table name for.",
 847    )
 848    @argument(
 849        "--environment",
 850        type=str,
 851        help="The environment to source the model version from.",
 852    )
 853    @argument(
 854        "--prod",
 855        action="store_true",
 856        help="If set, return the name of the physical table that will be used in production for the model version promoted in the target environment.",
 857    )
 858    @line_magic
 859    @pass_sqlmesh_context
 860    def table_name(self, context: Context, line: str) -> None:
 861        """Prints the name of the physical table for the given model."""
 862        args = parse_argstring(self.table_name, line)
 863        context.console.log_status_update(
 864            context.table_name(args.model_name, args.environment, args.prod)
 865        )
 866
 867    @magic_arguments()
 868    @argument(
 869        "pipeline",
 870        nargs="?",
 871        type=str,
 872        help="The dlt pipeline to attach for this SQLMesh project.",
 873    )
 874    @argument(
 875        "--table",
 876        "-t",
 877        type=str,
 878        nargs="*",
 879        help="The specific dlt tables to refresh in the SQLMesh models.",
 880    )
 881    @argument(
 882        "--force",
 883        "-f",
 884        action="store_true",
 885        help="If set, existing models are overwritten with the new DLT tables.",
 886    )
 887    @argument(
 888        "--dlt-path",
 889        type=str,
 890        help="The DLT pipelines working directory, where DLT stores pipeline state (by default ~/.dlt/pipelines).",
 891    )
 892    @line_magic
 893    @pass_sqlmesh_context
 894    def dlt_refresh(self, context: Context, line: str) -> None:
 895        """Attaches to a DLT pipeline with the option to update specific or all missing tables in the SQLMesh project."""
 896        from sqlmesh.integrations.dlt import generate_dlt_models
 897
 898        args = parse_argstring(self.dlt_refresh, line)
 899        sqlmesh_models = generate_dlt_models(
 900            context, args.pipeline, list(args.table or []), args.force, args.dlt_path
 901        )
 902        if sqlmesh_models:
 903            model_names = "\n".join([f"- {model_name}" for model_name in sqlmesh_models])
 904            context.console.log_success(f"Updated SQLMesh project with models:\n{model_names}")
 905        else:
 906            context.console.log_success("All SQLMesh models are up to date.")
 907
 908    @magic_arguments()
 909    @argument(
 910        "--read",
 911        type=str,
 912        default="",
 913        help="The input dialect of the sql string.",
 914    )
 915    @argument(
 916        "--write",
 917        type=str,
 918        default="",
 919        help="The output dialect of the sql string.",
 920    )
 921    @line_cell_magic
 922    @pass_sqlmesh_context
 923    def rewrite(self, context: Context, line: str, sql: str) -> None:
 924        """Rewrite a sql expression with semantic references into an executable query.
 925
 926        https://sqlmesh.readthedocs.io/en/latest/concepts/metrics/overview/
 927        """
 928        args = parse_argstring(self.rewrite, line)
 929        context.console.show_sql(
 930            context.rewrite(sql, args.read).sql(
 931                dialect=args.write or context.config.dialect, pretty=True
 932            )
 933        )
 934
 935    @magic_arguments()
 936    @argument(
 937        "--transpile",
 938        "-t",
 939        type=str,
 940        help="Transpile project models to the specified dialect.",
 941    )
 942    @argument(
 943        "--check",
 944        action="store_true",
 945        help="Whether or not to check formatting (but not actually format anything).",
 946        default=None,
 947    )
 948    @argument(
 949        "--append-newline",
 950        action="store_true",
 951        help="Include a newline at the end of the output.",
 952        default=None,
 953    )
 954    @argument(
 955        "--no-rewrite-casts",
 956        action="store_true",
 957        help="Preserve the existing casts, without rewriting them to use the :: syntax.",
 958        default=None,
 959    )
 960    @format_arguments
 961    @line_magic
 962    @pass_sqlmesh_context
 963    def format(self, context: Context, line: str) -> bool:
 964        """Format all SQL models and audits."""
 965        format_opts = vars(parse_argstring(self.format, line))
 966        if format_opts.pop("no_rewrite_casts", None):
 967            format_opts["rewrite_casts"] = False
 968
 969        return context.format(**{k: v for k, v in format_opts.items() if v is not None})
 970
 971    @magic_arguments()
 972    @argument("environment", type=str, help="The environment to diff local state against.")
 973    @line_magic
 974    @pass_sqlmesh_context
 975    def diff(self, context: Context, line: str) -> None:
 976        """Show the diff between the local state and the target environment."""
 977        args = parse_argstring(self.diff, line)
 978        context.diff(args.environment)
 979
 980    @magic_arguments()
 981    @argument("environment", type=str, help="The environment to invalidate.")
 982    @line_magic
 983    @pass_sqlmesh_context
 984    def invalidate(self, context: Context, line: str) -> None:
 985        """Invalidate the target environment, forcing its removal during the next run of the janitor process."""
 986        args = parse_argstring(self.invalidate, line)
 987        context.invalidate_environment(args.environment)
 988
 989    @magic_arguments()
 990    @argument(
 991        "--ignore-ttl",
 992        action="store_true",
 993        help="Cleanup snapshots that are not referenced in any environment, regardless of when they're set to expire",
 994    )
 995    @line_magic
 996    @pass_sqlmesh_context
 997    def janitor(self, context: Context, line: str) -> None:
 998        """Run the janitor process to clean up old environments and expired snapshots."""
 999        args = parse_argstring(self.janitor, line)
1000        context.run_janitor(ignore_ttl=args.ignore_ttl)
1001
1002    @magic_arguments()
1003    @argument("model", type=str)
1004    @argument(
1005        "--query",
1006        "-q",
1007        type=str,
1008        nargs="+",
1009        default=[],
1010        help="Queries that will be used to generate data for the model's dependencies.",
1011    )
1012    @argument(
1013        "--overwrite",
1014        "-o",
1015        action="store_true",
1016        help="When true, the fixture file will be overwritten in case it already exists.",
1017    )
1018    @argument(
1019        "--var",
1020        "-v",
1021        type=str,
1022        nargs="+",
1023        help="Key-value pairs that will define variables needed by the model.",
1024    )
1025    @argument(
1026        "--path",
1027        "-p",
1028        type=str,
1029        help="The file path corresponding to the fixture, relative to the test directory. "
1030        "By default, the fixture will be created under the test directory and the file "
1031        "name will be inferred based on the test's name.",
1032    )
1033    @argument(
1034        "--name",
1035        "-n",
1036        type=str,
1037        help="The name of the test that will be created. By default, it's inferred based on the model's name.",
1038    )
1039    @argument(
1040        "--include-ctes",
1041        action="store_true",
1042        help="When true, CTE fixtures will also be generated.",
1043    )
1044    @line_magic
1045    @pass_sqlmesh_context
1046    def create_test(self, context: Context, line: str) -> None:
1047        """Generate a unit test fixture for a given model."""
1048        args = parse_argstring(self.create_test, line)
1049        queries = iter(args.query)
1050        variables = iter(args.var) if args.var else None
1051        context.create_test(
1052            args.model,
1053            input_queries={k: v.strip('"') for k, v in dict(zip(queries, queries)).items()},
1054            overwrite=args.overwrite,
1055            variables=dict(zip(variables, variables)) if variables else None,
1056            path=args.path,
1057            name=args.name,
1058            include_ctes=args.include_ctes,
1059        )
1060
1061    @magic_arguments()
1062    @argument("tests", nargs="*", type=str)
1063    @argument(
1064        "--pattern",
1065        "-k",
1066        nargs="*",
1067        type=str,
1068        help="Only run tests that match the pattern of substring.",
1069    )
1070    @argument(
1071        "--verbose",
1072        "-v",
1073        action="count",
1074        default=0,
1075        help="Verbose output. Use -vv for very verbose.",
1076    )
1077    @argument(
1078        "--preserve-fixtures",
1079        action="store_true",
1080        help="Preserve the fixture tables in the testing database, useful for debugging.",
1081    )
1082    @line_magic
1083    @pass_sqlmesh_context
1084    def run_test(self, context: Context, line: str) -> None:
1085        """Run unit test(s)."""
1086        args = parse_argstring(self.run_test, line)
1087
1088        context.test(
1089            match_patterns=args.pattern,
1090            tests=args.tests,
1091            verbosity=Verbosity(args.verbose),
1092            preserve_fixtures=args.preserve_fixtures,
1093            stream=StringIO(),  # consume the output instead of redirecting to stdout
1094        )
1095
1096    @magic_arguments()
1097    @argument(
1098        "models", type=str, nargs="*", help="A model to audit. Multiple models can be audited."
1099    )
1100    @argument("--start", "-s", type=str, help="Start date to audit.")
1101    @argument("--end", "-e", type=str, help="End date to audit.")
1102    @argument("--execution-time", type=str, help="Execution time.")
1103    @line_magic
1104    @pass_sqlmesh_context
1105    def audit(self, context: Context, line: str) -> bool:
1106        """Run audit(s)"""
1107        args = parse_argstring(self.audit, line)
1108        return context.audit(
1109            models=args.models, start=args.start, end=args.end, execution_time=args.execution_time
1110        )
1111
1112    @magic_arguments()
1113    @argument("environment", nargs="?", type=str, help="The environment to check intervals for.")
1114    @argument(
1115        "--no-signals",
1116        action="store_true",
1117        help="Disable signal checks and only show missing intervals.",
1118        default=False,
1119    )
1120    @argument(
1121        "--select-model",
1122        type=str,
1123        nargs="*",
1124        help="Select specific model changes that should be included in the plan.",
1125    )
1126    @argument("--start", "-s", type=str, help="Start date of intervals to check for.")
1127    @argument("--end", "-e", type=str, help="End date of intervals to check for.")
1128    @line_magic
1129    @pass_sqlmesh_context
1130    def check_intervals(self, context: Context, line: str) -> None:
1131        """Show missing intervals in an environment, respecting signals."""
1132        args = parse_argstring(self.check_intervals, line)
1133
1134        context.console.show_intervals(
1135            context.check_intervals(
1136                environment=args.environment,
1137                no_signals=args.no_signals,
1138                select_models=args.select_model,
1139                start=args.start,
1140                end=args.end,
1141            )
1142        )
1143
1144    @magic_arguments()
1145    @argument(
1146        "--skip-connection",
1147        action="store_true",
1148        help="Skip the connection test.",
1149        default=False,
1150    )
1151    @argument(
1152        "--verbose",
1153        "-v",
1154        action="count",
1155        default=0,
1156        help="Verbose output. Use -vv for very verbose.",
1157    )
1158    @line_magic
1159    @pass_sqlmesh_context
1160    def info(self, context: Context, line: str) -> None:
1161        """Display SQLMesh project information."""
1162        args = parse_argstring(self.info, line)
1163        context.print_info(skip_connection=args.skip_connection, verbosity=Verbosity(args.verbose))
1164
1165    @magic_arguments()
1166    @line_magic
1167    @pass_sqlmesh_context
1168    def rollback(self, context: Context, line: str) -> None:
1169        """Rollback SQLMesh to the previous migration."""
1170        context.rollback()
1171
1172    @magic_arguments()
1173    @line_magic
1174    @pass_sqlmesh_context
1175    def clean(self, context: Context, line: str) -> None:
1176        """Clears the SQLMesh cache and any build artifacts."""
1177        context.clear_caches()
1178        context.console.log_success("SQLMesh cache and build artifacts cleared")
1179
1180    @magic_arguments()
1181    @line_magic
1182    @pass_sqlmesh_context
1183    def environments(self, context: Context, line: str) -> None:
1184        """Prints the list of SQLMesh environments with its expiry datetime."""
1185        context.print_environment_names()
1186
1187    @magic_arguments()
1188    @argument(
1189        "--models",
1190        "--model",
1191        type=str,
1192        nargs="*",
1193        help="A model to lint. Multiple models can be linted. If no models are specified, every model will be linted.",
1194    )
1195    @line_magic
1196    @pass_sqlmesh_context
1197    def lint(self, context: Context, line: str) -> None:
1198        """Run linter for target model(s)"""
1199        args = parse_argstring(self.lint, line)
1200        context.lint_models(args.models)
1201
1202    @magic_arguments()
1203    @line_magic
1204    @pass_sqlmesh_context
1205    def destroy(self, context: Context, line: str) -> None:
1206        """Removes all project resources, engine-managed objects, state tables and clears the SQLMesh cache."""
1207        context.destroy()

Base class for implementing magic functions.

Shell functions which can be reached as %function_name. All magic functions should accept a string, which they can parse for their own needs. This can make some functions easier to type, eg %cd ../ vs. %cd("../")

Classes providing magic functions need to subclass this class, and they MUST:

  • Use the method decorators @line_magic and @cell_magic to decorate individual methods as magic functions, AND

  • Use the class decorator @magics_class to ensure that the magic methods are properly registered at the instance level upon instance initialization.

See magic_functions for examples of actual implementation classes.

display: Callable
146    @property
147    def display(self) -> t.Callable:
148        from sqlmesh import RuntimeEnv
149
150        if RuntimeEnv.get().is_databricks:
151            # Use Databricks' special display instead of the normal IPython display
152            return self._shell.user_ns["display"]
153        return display
@magic_arguments()
@argument('paths', type=str, nargs='+', default='', help='The path(s) to the SQLMesh project(s).')
@argument('--config', type=str, help='Name of the config object. Only applicable to configuration defined using Python script.')
@argument('--gateway', type=str, help='The name of the gateway.')
@argument('--ignore-warnings', action='store_true', help='Ignore warnings.')
@argument('--debug', action='store_true', help='Enable debug mode.')
@argument('--log-file-dir', type=str, help='The directory to write the log file to.')
@argument('--dotenv', type=str, help='Path to a custom .env file to load environment variables from.')
@line_magic
def context(self, line: str) -> None:
162    @magic_arguments()
163    @argument(
164        "paths",
165        type=str,
166        nargs="+",
167        default="",
168        help="The path(s) to the SQLMesh project(s).",
169    )
170    @argument(
171        "--config",
172        type=str,
173        help="Name of the config object. Only applicable to configuration defined using Python script.",
174    )
175    @argument("--gateway", type=str, help="The name of the gateway.")
176    @argument("--ignore-warnings", action="store_true", help="Ignore warnings.")
177    @argument("--debug", action="store_true", help="Enable debug mode.")
178    @argument("--log-file-dir", type=str, help="The directory to write the log file to.")
179    @argument(
180        "--dotenv", type=str, help="Path to a custom .env file to load environment variables from."
181    )
182    @line_magic
183    def context(self, line: str) -> None:
184        """Sets the context in the user namespace."""
185        from sqlmesh import configure_logging, remove_excess_logs
186
187        args = parse_argstring(self.context, line)
188        log_file_dir = args.log_file_dir
189
190        configure_logging(
191            args.debug,
192            log_file_dir=log_file_dir,
193            ignore_warnings=args.ignore_warnings,
194        )
195        configure_console(ignore_warnings=args.ignore_warnings)
196
197        dotenv_path = Path(args.dotenv) if args.dotenv else None
198        configs = load_configs(
199            args.config, Context.CONFIG_TYPE, args.paths, dotenv_path=dotenv_path
200        )
201        log_limit = list(configs.values())[0].log_limit
202
203        remove_excess_logs(log_file_dir, log_limit)
204
205        try:
206            context = Context(paths=args.paths, config=configs, gateway=args.gateway)
207            self._shell.user_ns["context"] = context
208        except Exception:
209            if args.debug:
210                logger.exception("Failed to initialize SQLMesh context")
211            raise
212
213        context.console.log_success(f"SQLMesh project context set to: {', '.join(args.paths)}")

::

%context [--config CONFIG] [--gateway GATEWAY] [--ignore-warnings] [--debug] [--log-file-dir LOG_FILE_DIR] [--dotenv DOTENV] paths [paths ...]

Sets the context in the user namespace.

positional arguments: paths The path(s) to the SQLMesh project(s).

options: --config CONFIG Name of the config object. Only applicable to configuration defined using Python script. --gateway GATEWAY The name of the gateway. --ignore-warnings Ignore warnings. --debug Enable debug mode. --log-file-dir LOG_FILE_DIR The directory to write the log file to. --dotenv DOTENV Path to a custom .env file to load environment variables from.

@magic_arguments()
@argument('path', type=str, help='The path where the new SQLMesh project should be created.')
@argument('engine', type=str, help=f"Project SQL engine. Supported values: '{', '.join([info[1] for info in sorted(INIT_DISPLAY_INFO_TO_TYPE.values(), key=lambda x: x[0])])}'.")
@argument('--template', '-t', type=str, help='Project template. Supported values: dbt, default, empty.')
@argument('--dlt-pipeline', type=str, help='DLT pipeline for which to generate a SQLMesh project. Use alongside template: dlt')
@argument('--dlt-path', type=str, help='The DLT pipelines working directory, where DLT stores pipeline state (by default ~/.dlt/pipelines). Use alongside template: dlt')
@line_magic
def init(self, line: str) -> None:
215    @magic_arguments()
216    @argument("path", type=str, help="The path where the new SQLMesh project should be created.")
217    @argument(
218        "engine",
219        type=str,
220        help=f"Project SQL engine. Supported values: '{', '.join([info[1] for info in sorted(INIT_DISPLAY_INFO_TO_TYPE.values(), key=lambda x: x[0])])}'.",  # type: ignore
221    )
222    @argument(
223        "--template",
224        "-t",
225        type=str,
226        help="Project template. Supported values: dbt, default, empty.",
227    )
228    @argument(
229        "--dlt-pipeline",
230        type=str,
231        help="DLT pipeline for which to generate a SQLMesh project. Use alongside template: dlt",
232    )
233    @argument(
234        "--dlt-path",
235        type=str,
236        help="The DLT pipelines working directory, where DLT stores pipeline state (by default ~/.dlt/pipelines). Use alongside template: dlt",
237    )
238    @line_magic
239    def init(self, line: str) -> None:
240        """Creates a SQLMesh project scaffold with a default SQL dialect."""
241        args = parse_argstring(self.init, line)
242        try:
243            project_template = ProjectTemplate(
244                args.template.lower() if args.template else "default"
245            )
246        except ValueError:
247            raise MagicError(f"Invalid project template '{args.template}'")
248        init_example_project(
249            path=args.path,
250            engine_type=args.engine,
251            dialect=None,
252            template=project_template,
253            pipeline=args.dlt_pipeline,
254            dlt_path=args.dlt_path,
255        )
256        html = str(
257            h(
258                "div",
259                h(
260                    "span",
261                    {"style": {"color": "green", "font-weight": "bold"}},
262                    "SQLMesh project scaffold created",
263                ),
264            )
265        )
266        self.display(JupyterRenderable(html=html, text=""))

::

%init [--template TEMPLATE] [--dlt-pipeline DLT_PIPELINE] [--dlt-path DLT_PATH] path engine

Creates a SQLMesh project scaffold with a default SQL dialect.

positional arguments: path The path where the new SQLMesh project should be created. engine Project SQL engine. Supported values: 'DuckDB, Snowflake, Databricks, BigQuery, MotherDuck, ClickHouse, Redshift, Spark, Trino, Azure SQL, MSSQL, Postgres, GCP Postgres, MySQL, Athena, RisingWave, Fabric, StarRocks'.

options: --template TEMPLATE, -t TEMPLATE Project template. Supported values: dbt, default, empty. --dlt-pipeline DLT_PIPELINE DLT pipeline for which to generate a SQLMesh project. Use alongside template: dlt --dlt-path DLT_PATH The DLT pipelines working directory, where DLT stores pipeline state (by default ~/.dlt/pipelines). Use alongside template: dlt

@magic_arguments()
@argument('model', type=str, help='The model.')
@argument('--start', '-s', type=str, help='Start date to render.')
@argument('--end', '-e', type=str, help='End date to render.')
@argument('--execution-time', type=str, help='Execution time.')
@argument('--dialect', '-d', type=str, help='The rendered dialect.')
@line_cell_magic
@pass_sqlmesh_context
def model( self, context: sqlmesh.core.context.Context, line: str, sql: Optional[str] = None) -> None:
268    @magic_arguments()
269    @argument("model", type=str, help="The model.")
270    @argument("--start", "-s", type=str, help="Start date to render.")
271    @argument("--end", "-e", type=str, help="End date to render.")
272    @argument("--execution-time", type=str, help="Execution time.")
273    @argument("--dialect", "-d", type=str, help="The rendered dialect.")
274    @line_cell_magic
275    @pass_sqlmesh_context
276    def model(self, context: Context, line: str, sql: t.Optional[str] = None) -> None:
277        """Renders the model and automatically fills in an editable cell with the model definition."""
278        args = parse_argstring(self.model, line)
279
280        model = context.get_model(args.model, raise_if_missing=True)
281        config = context.config_for_node(model)
282
283        if sql:
284            expressions = parse(sql, default_dialect=config.dialect)
285            loaded = load_sql_based_model(
286                expressions,
287                macros=context._macros,
288                jinja_macros=context._jinja_macros,
289                path=model._path,
290                dialect=config.dialect,
291                time_column_format=config.time_column_format,
292                physical_schema_mapping=context.config.physical_schema_mapping,
293                default_catalog=context.default_catalog,
294            )
295
296            if loaded.name == args.model:
297                model = loaded
298        else:
299            if model._path:
300                with open(model._path, "r", encoding="utf-8") as file:
301                    expressions = parse(file.read(), default_dialect=config.dialect)
302
303        formatted = format_model_expressions(
304            expressions,
305            model.dialect,
306            rewrite_casts=not config.format.no_rewrite_casts,
307            **config.format.generator_options,
308        )
309
310        self._shell.set_next_input(
311            "\n".join(
312                [
313                    " ".join(["%%model", line]),
314                    formatted,
315                ]
316            ),
317            replace=True,
318        )
319
320        if model._path:
321            with open(model._path, "w", encoding="utf-8") as file:
322                file.write(formatted)
323
324        if sql:
325            context.console.log_success(f"Model `{args.model}` updated")
326
327        context.upsert_model(model)
328        context.console.show_sql(
329            context.render(
330                model.name,
331                start=args.start,
332                end=args.end,
333                execution_time=args.execution_time,
334            ).sql(pretty=True, dialect=args.dialect or model.dialect)
335        )

Renders the model and automatically fills in an editable cell with the model definition.

@magic_arguments()
@argument('model', type=str, help='The model.')
@argument('test_name', type=str, nargs='?', default=None, help='The test name to display')
@argument('--ls', action='store_true', help='List tests associated with a model')
@line_cell_magic
@pass_sqlmesh_context
def test( self, context: sqlmesh.core.context.Context, line: str, test_def_raw: Optional[str] = None) -> None:
337    @magic_arguments()
338    @argument("model", type=str, help="The model.")
339    @argument("test_name", type=str, nargs="?", default=None, help="The test name to display")
340    @argument("--ls", action="store_true", help="List tests associated with a model")
341    @line_cell_magic
342    @pass_sqlmesh_context
343    def test(self, context: Context, line: str, test_def_raw: t.Optional[str] = None) -> None:
344        """Allow the user to list tests for a model, output a specific test, and then write their changes back"""
345        args = parse_argstring(self.test, line)
346        if not args.test_name and not args.ls:
347            raise MagicError("Must provide either test name or `--ls` to list tests")
348
349        test_meta = context.select_tests()
350
351        tests: t.Dict[str, t.Dict[str, ModelTestMetadata]] = defaultdict(dict)
352        for model_test_metadata in test_meta:
353            model = model_test_metadata.body.get("model")
354            if not model:
355                context.console.log_error(
356                    f"Test found that does not have `model` defined: {model_test_metadata.path}"
357                )
358            else:
359                tests[model][model_test_metadata.test_name] = model_test_metadata
360
361        model = context.get_model(args.model, raise_if_missing=True)
362
363        if args.ls:
364            # TODO: Provide better UI for displaying tests
365            for test_name in tests[model.name]:
366                context.console.log_status_update(test_name)
367            return
368
369        test = tests[model.name][args.test_name]
370        test_def = yaml.load(test_def_raw) if test_def_raw else test.body
371        test_def_output = yaml.dump(test_def)
372
373        self._shell.set_next_input(
374            "\n".join(
375                [
376                    " ".join(["%%test", line]),
377                    test_def_output,
378                ]
379            ),
380            replace=True,
381        )
382
383        with open(test.path, "r+", encoding="utf-8") as file:
384            content = yaml.load(file.read())
385            content[args.test_name] = test_def
386            file.seek(0)
387            yaml.dump(content, file)
388            file.truncate()

Allow the user to list tests for a model, output a specific test, and then write their changes back

@magic_arguments()
@argument('environment', nargs='?', type=str, help='The environment to run the plan against')
@argument('--start', '-s', type=str, help='Start date to backfill.')
@argument('--end', '-e', type=str, help='End date to backfill.')
@argument('--execution-time', type=str, help='Execution time.')
@argument('--create-from', type=str, help="The environment to create the target environment from if it doesn't exist. Default: prod.")
@argument('--skip-tests', '-t', action='store_true', help='Skip the unit tests defined for the model.')
@argument('--skip-linter', action='store_true', help='Skip the linter for the model.')
@argument('--restate-model', '-r', type=str, nargs='*', help='Restate data for specified models (and models downstream from the one specified). For production environment, all related model versions will have their intervals wiped, but only the current versions will be backfilled. For development environment, only the current model versions will be affected.')
@argument('--no-gaps', '-g', action='store_true', help='Ensure that new snapshots have no data gaps when comparing to existing snapshots for matching models in the target environment.')
@argument('--skip-backfill', '--dry-run', action='store_true', help='Skip the backfill step and only create a virtual update for the plan.')
@argument('--empty-backfill', action='store_true', help='Produce empty backfill. Like --skip-backfill no models will be backfilled, unlike --skip-backfill missing intervals will be recorded as if they were backfilled.')
@argument('--forward-only', action='store_true', help='Create a plan for forward-only changes.', default=None)
@argument('--effective-from', type=str, help='The effective date from which to apply forward-only changes on production.')
@argument('--no-prompts', action='store_true', help='Disables interactive prompts for the backfill time range. Please note that if this flag is set and there are uncategorized changes, plan creation will fail.', default=None)
@argument('--auto-apply', action='store_true', help='Automatically applies the new plan after creation.', default=None)
@argument('--no-auto-categorization', action='store_true', help='Disable automatic change categorization.', default=None)
@argument('--include-unmodified', action='store_true', help='Include unmodified models in the target environment.', default=None)
@argument('--select-model', type=str, nargs='*', help='Select specific model changes that should be included in the plan.')
@argument('--backfill-model', type=str, nargs='*', help='Backfill only the models whose names match the expression.')
@argument('--no-diff', action='store_true', help='Hide text differences for changed models.', default=None)
@argument('--run', action='store_true', help='Run latest intervals as part of the plan application (prod environment only).')
@argument('--ignore-cron', action='store_true', help='Run for all missing intervals, ignoring individual cron schedules. Only applies if --run is set.', default=None)
@argument('--enable-preview', action='store_true', help='Enable preview for forward-only models when targeting a development environment.', default=None)
@argument('--diff-rendered', action='store_true', help='Output text differences for the rendered versions of the models and standalone audits')
@argument('--verbose', '-v', action='count', default=0, help='Verbose output. Use -vv for very verbose.')
@line_magic
@pass_sqlmesh_context
def plan(self, context: sqlmesh.core.context.Context, line: str) -> None:
390    @magic_arguments()
391    @argument(
392        "environment",
393        nargs="?",
394        type=str,
395        help="The environment to run the plan against",
396    )
397    @argument("--start", "-s", type=str, help="Start date to backfill.")
398    @argument("--end", "-e", type=str, help="End date to backfill.")
399    @argument("--execution-time", type=str, help="Execution time.")
400    @argument(
401        "--create-from",
402        type=str,
403        help="The environment to create the target environment from if it doesn't exist. Default: prod.",
404    )
405    @argument(
406        "--skip-tests",
407        "-t",
408        action="store_true",
409        help="Skip the unit tests defined for the model.",
410    )
411    @argument(
412        "--skip-linter",
413        action="store_true",
414        help="Skip the linter for the model.",
415    )
416    @argument(
417        "--restate-model",
418        "-r",
419        type=str,
420        nargs="*",
421        help="Restate data for specified models (and models downstream from the one specified). For production environment, all related model versions will have their intervals wiped, but only the current versions will be backfilled. For development environment, only the current model versions will be affected.",
422    )
423    @argument(
424        "--no-gaps",
425        "-g",
426        action="store_true",
427        help="Ensure that new snapshots have no data gaps when comparing to existing snapshots for matching models in the target environment.",
428    )
429    @argument(
430        "--skip-backfill",
431        "--dry-run",
432        action="store_true",
433        help="Skip the backfill step and only create a virtual update for the plan.",
434    )
435    @argument(
436        "--empty-backfill",
437        action="store_true",
438        help="Produce empty backfill. Like --skip-backfill no models will be backfilled, unlike --skip-backfill missing intervals will be recorded as if they were backfilled.",
439    )
440    @argument(
441        "--forward-only",
442        action="store_true",
443        help="Create a plan for forward-only changes.",
444        default=None,
445    )
446    @argument(
447        "--effective-from",
448        type=str,
449        help="The effective date from which to apply forward-only changes on production.",
450    )
451    @argument(
452        "--no-prompts",
453        action="store_true",
454        help="Disables interactive prompts for the backfill time range. Please note that if this flag is set and there are uncategorized changes, plan creation will fail.",
455        default=None,
456    )
457    @argument(
458        "--auto-apply",
459        action="store_true",
460        help="Automatically applies the new plan after creation.",
461        default=None,
462    )
463    @argument(
464        "--no-auto-categorization",
465        action="store_true",
466        help="Disable automatic change categorization.",
467        default=None,
468    )
469    @argument(
470        "--include-unmodified",
471        action="store_true",
472        help="Include unmodified models in the target environment.",
473        default=None,
474    )
475    @argument(
476        "--select-model",
477        type=str,
478        nargs="*",
479        help="Select specific model changes that should be included in the plan.",
480    )
481    @argument(
482        "--backfill-model",
483        type=str,
484        nargs="*",
485        help="Backfill only the models whose names match the expression.",
486    )
487    @argument(
488        "--no-diff",
489        action="store_true",
490        help="Hide text differences for changed models.",
491        default=None,
492    )
493    @argument(
494        "--run",
495        action="store_true",
496        help="Run latest intervals as part of the plan application (prod environment only).",
497    )
498    @argument(
499        "--ignore-cron",
500        action="store_true",
501        help="Run for all missing intervals, ignoring individual cron schedules. Only applies if --run is set.",
502        default=None,
503    )
504    @argument(
505        "--enable-preview",
506        action="store_true",
507        help="Enable preview for forward-only models when targeting a development environment.",
508        default=None,
509    )
510    @argument(
511        "--diff-rendered",
512        action="store_true",
513        help="Output text differences for the rendered versions of the models and standalone audits",
514    )
515    @argument(
516        "--verbose",
517        "-v",
518        action="count",
519        default=0,
520        help="Verbose output. Use -vv for very verbose.",
521    )
522    @line_magic
523    @pass_sqlmesh_context
524    def plan(self, context: Context, line: str) -> None:
525        """Goes through a set of prompts to both establish a plan and apply it"""
526        args = parse_argstring(self.plan, line)
527
528        setattr(context.console, "verbosity", Verbosity(args.verbose))
529
530        context.plan(
531            args.environment,
532            start=args.start,
533            end=args.end,
534            execution_time=args.execution_time,
535            create_from=args.create_from,
536            skip_tests=args.skip_tests,
537            restate_models=args.restate_model,
538            backfill_models=args.backfill_model,
539            no_gaps=args.no_gaps,
540            skip_backfill=args.skip_backfill,
541            empty_backfill=args.empty_backfill,
542            forward_only=args.forward_only,
543            no_prompts=args.no_prompts,
544            auto_apply=args.auto_apply,
545            no_auto_categorization=args.no_auto_categorization,
546            effective_from=args.effective_from,
547            include_unmodified=args.include_unmodified,
548            select_models=args.select_model,
549            no_diff=args.no_diff,
550            run=args.run,
551            ignore_cron=args.run,
552            enable_preview=args.enable_preview,
553            diff_rendered=args.diff_rendered,
554        )

Goes through a set of prompts to both establish a plan and apply it

@magic_arguments()
@argument('environment', nargs='?', type=str, help='The environment to run against')
@argument('--start', '-s', type=str, help='Start date to evaluate.')
@argument('--end', '-e', type=str, help='End date to evaluate.')
@argument('--skip-janitor', action='store_true', help='Skip the janitor task.')
@argument('--ignore-cron', action='store_true', help='Run for all missing intervals, ignoring individual cron schedules.')
@argument('--select-model', type=str, nargs='*', help='Select specific models to run. Note: this always includes upstream dependencies.')
@argument('--exit-on-env-update', type=int, help='If set, the command will exit with the specified code if the run is interrupted by an update to the target environment.')
@argument('--no-auto-upstream', action='store_true', help='Do not automatically include upstream models. Only applicable when --select-model is used. Note: this may result in missing / invalid data for the selected models.')
@line_magic
@pass_sqlmesh_context
def run_dag(self, context: sqlmesh.core.context.Context, line: str) -> None:
556    @magic_arguments()
557    @argument(
558        "environment",
559        nargs="?",
560        type=str,
561        help="The environment to run against",
562    )
563    @argument("--start", "-s", type=str, help="Start date to evaluate.")
564    @argument("--end", "-e", type=str, help="End date to evaluate.")
565    @argument("--skip-janitor", action="store_true", help="Skip the janitor task.")
566    @argument(
567        "--ignore-cron",
568        action="store_true",
569        help="Run for all missing intervals, ignoring individual cron schedules.",
570    )
571    @argument(
572        "--select-model",
573        type=str,
574        nargs="*",
575        help="Select specific models to run. Note: this always includes upstream dependencies.",
576    )
577    @argument(
578        "--exit-on-env-update",
579        type=int,
580        help="If set, the command will exit with the specified code if the run is interrupted by an update to the target environment.",
581    )
582    @argument(
583        "--no-auto-upstream",
584        action="store_true",
585        help="Do not automatically include upstream models. Only applicable when --select-model is used. Note: this may result in missing / invalid data for the selected models.",
586    )
587    @line_magic
588    @pass_sqlmesh_context
589    def run_dag(self, context: Context, line: str) -> None:
590        """Evaluate the DAG of models using the built-in scheduler."""
591        args = parse_argstring(self.run_dag, line)
592
593        completion_status = context.run(
594            args.environment,
595            start=args.start,
596            end=args.end,
597            skip_janitor=args.skip_janitor,
598            ignore_cron=args.ignore_cron,
599            select_models=args.select_model,
600            exit_on_env_update=args.exit_on_env_update,
601            no_auto_upstream=args.no_auto_upstream,
602        )
603        if completion_status.is_failure:
604            raise SQLMeshError("Error Running DAG. Check logs for details.")

Evaluate the DAG of models using the built-in scheduler.

@magic_arguments()
@argument('model', type=str, help='The model.')
@argument('--start', '-s', type=str, help='Start date to render.')
@argument('--end', '-e', type=str, help='End date to render.')
@argument('--execution-time', type=str, help='Execution time.')
@argument('--limit', type=int, help='The number of rows which the query should be limited to.')
@line_magic
@pass_sqlmesh_context
def evaluate(self, context: sqlmesh.core.context.Context, line: str) -> None:
606    @magic_arguments()
607    @argument("model", type=str, help="The model.")
608    @argument("--start", "-s", type=str, help="Start date to render.")
609    @argument("--end", "-e", type=str, help="End date to render.")
610    @argument("--execution-time", type=str, help="Execution time.")
611    @argument(
612        "--limit",
613        type=int,
614        help="The number of rows which the query should be limited to.",
615    )
616    @line_magic
617    @pass_sqlmesh_context
618    def evaluate(self, context: Context, line: str) -> None:
619        """Evaluate a model query and fetches a dataframe."""
620        context.refresh()
621
622        snowpark = optional_import("snowflake.snowpark")
623        args = parse_argstring(self.evaluate, line)
624
625        df = context.evaluate(
626            args.model,
627            start=args.start,
628            end=args.end,
629            execution_time=args.execution_time,
630            limit=args.limit,
631        )
632
633        if snowpark and isinstance(df, snowpark.DataFrame):
634            df = df.limit(args.limit or 100).to_pandas()
635
636        self.display(df)

Evaluate a model query and fetches a dataframe.

@magic_arguments()
@argument('model', type=str, help='The model.')
@argument('--start', '-s', type=str, help='Start date to render.')
@argument('--end', '-e', type=str, help='End date to render.')
@argument('--execution-time', type=str, help='Execution time.')
@argument('--expand', type=parse_expand, help="Whether or not to use expand materialized models, defaults to False. If 'true', all referenced models are expanded as raw queries. If a comma-separated list of model names, only those models are expanded as raw queries.")
@argument('--dialect', type=str, help='SQL dialect to render.')
@argument('--no-format', action='store_true', help='Disable fancy formatting of the query.')
@format_arguments
@line_magic
@pass_sqlmesh_context
def render(self, context: sqlmesh.core.context.Context, line: str) -> None:
638    @magic_arguments()
639    @argument("model", type=str, help="The model.")
640    @argument("--start", "-s", type=str, help="Start date to render.")
641    @argument("--end", "-e", type=str, help="End date to render.")
642    @argument("--execution-time", type=str, help="Execution time.")
643    @argument(
644        "--expand",
645        type=parse_expand,
646        help="Whether or not to use expand materialized models, defaults to False. If 'true', all referenced models are expanded as raw queries. If a comma-separated list of model names, only those models are expanded as raw queries.",
647    )
648    @argument("--dialect", type=str, help="SQL dialect to render.")
649    @argument("--no-format", action="store_true", help="Disable fancy formatting of the query.")
650    @format_arguments
651    @line_magic
652    @pass_sqlmesh_context
653    def render(self, context: Context, line: str) -> None:
654        """Renders a model's query, optionally expanding referenced models."""
655        context.refresh()
656        render_opts = vars(parse_argstring(self.render, line))
657        model = render_opts.pop("model")
658        dialect = render_opts.pop("dialect", None)
659        expand = render_opts.pop("expand", False)
660
661        model = context.get_model(model, raise_if_missing=True)
662
663        query = context.render(
664            model,
665            start=render_opts.pop("start", None),
666            end=render_opts.pop("end", None),
667            execution_time=render_opts.pop("execution_time", None),
668            expand=expand,
669        )
670
671        no_format = render_opts.pop("no_format", False)
672
673        format_config = context.config_for_node(model).format
674        format_options = {
675            **format_config.generator_options,
676            **{k: v for k, v in render_opts.items() if v is not None},
677        }
678
679        sql = query.sql(
680            pretty=True,
681            dialect=context.config.dialect if dialect is None else dialect,
682            **format_options,
683        )
684
685        if no_format:
686            context.console.log_status_update(sql)
687        else:
688            context.console.show_sql(sql)

Renders a model's query, optionally expanding referenced models.

@magic_arguments()
@argument('df_var', default=None, nargs='?', type=str, help='An optional variable name to store the resulting dataframe.')
@cell_magic
@pass_sqlmesh_context
def fetchdf(self, context: sqlmesh.core.context.Context, line: str, sql: str) -> None:
690    @magic_arguments()
691    @argument(
692        "df_var",
693        default=None,
694        nargs="?",
695        type=str,
696        help="An optional variable name to store the resulting dataframe.",
697    )
698    @cell_magic
699    @pass_sqlmesh_context
700    def fetchdf(self, context: Context, line: str, sql: str) -> None:
701        """Fetches a dataframe from sql, optionally storing it in a variable."""
702        args = parse_argstring(self.fetchdf, line)
703        df = context.fetchdf(sql)
704        if args.df_var:
705            self._shell.user_ns[args.df_var] = df
706        self.display(df)

Fetches a dataframe from sql, optionally storing it in a variable.

@magic_arguments()
@argument('--file', '-f', type=str, help='An optional file path to write the HTML output to.')
@argument('--select-model', type=str, nargs='*', help='Select specific models to include in the dag.')
@line_magic
@pass_sqlmesh_context
def dag(self, context: sqlmesh.core.context.Context, line: str) -> None:
708    @magic_arguments()
709    @argument("--file", "-f", type=str, help="An optional file path to write the HTML output to.")
710    @argument(
711        "--select-model",
712        type=str,
713        nargs="*",
714        help="Select specific models to include in the dag.",
715    )
716    @line_magic
717    @pass_sqlmesh_context
718    def dag(self, context: Context, line: str) -> None:
719        """Displays the HTML DAG."""
720        args = parse_argstring(self.dag, line)
721        dag = context.get_dag(args.select_model)
722        if args.file:
723            with open(args.file, "w", encoding="utf-8") as file:
724                file.write(str(dag))
725        # TODO: Have this go through console instead of calling display directly
726        self.display(dag)

Displays the HTML DAG.

@magic_arguments()
@line_magic
@pass_sqlmesh_context
def migrate(self, context: sqlmesh.core.context.Context, line: str) -> None:
728    @magic_arguments()
729    @line_magic
730    @pass_sqlmesh_context
731    def migrate(self, context: Context, line: str) -> None:
732        """Migrate SQLMesh to the current running version."""
733        context.migrate()
734        context.console.log_success("Migration complete")

Migrate SQLMesh to the current running version.

@magic_arguments()
@argument('--strict', action='store_true', help='Raise an error if the external model is missing in the database')
@line_magic
@pass_sqlmesh_context
def create_external_models(self, context: sqlmesh.core.context.Context, line: str) -> None:
736    @magic_arguments()
737    @argument(
738        "--strict",
739        action="store_true",
740        help="Raise an error if the external model is missing in the database",
741    )
742    @line_magic
743    @pass_sqlmesh_context
744    def create_external_models(self, context: Context, line: str) -> None:
745        """Create a schema file containing external model schemas."""
746        args = parse_argstring(self.create_external_models, line)
747        context.create_external_models(strict=args.strict)

Create a schema file containing external model schemas.

@magic_arguments()
@argument('source_to_target', type=str, metavar='SOURCE:TARGET', help='Source and target in `SOURCE:TARGET` format')
@argument('--on', type=str, nargs='*', help='The column to join on. Can be specified multiple times. The model grain will be used if not specified.')
@argument('--skip-columns', type=str, nargs='*', help='The column(s) to skip when comparing the source and target table.')
@argument('--model', type=str, help='The model to diff against when source and target are environments and not tables.')
@argument('--where', type=str, help='An optional where statement to filter results.')
@argument('--limit', type=int, default=20, help='The limit of the sample dataframe.')
@argument('--show-sample', action='store_true', help='Show a sample of the rows that differ. With many columns, the output can be very wide.')
@argument('--decimals', type=int, default=3, help='The number of decimal places to keep when comparing floating point columns. Default: 3')
@argument('--select-model', type=str, nargs='*', help="Specify one or more models to data diff. Use wildcards to diff multiple models. Ex: '*' (all models with applied plan diffs), 'demo.model+' (this and downstream models), 'git:feature_branch' (models with direct modifications in this branch only)")
@argument('--skip-grain-check', action='store_true', help='Disable the check for a primary key (grain) that is missing or is not unique.')
@argument('--warn-grain-check', action='store_true', help='Warn if any selected model is missing a grain, and compute diffs for the remaining models.')
@argument('--schema-diff-ignore-case', action='store_true', help="If set, when performing a schema diff the case of column names is ignored when matching between the two schemas. For example, 'col_a' in the source schema and 'COL_A' in the target schema will be treated as the same column.")
@line_magic
@pass_sqlmesh_context
def table_diff(self, context: sqlmesh.core.context.Context, line: str) -> None:
749    @magic_arguments()
750    @argument(
751        "source_to_target",
752        type=str,
753        metavar="SOURCE:TARGET",
754        help="Source and target in `SOURCE:TARGET` format",
755    )
756    @argument(
757        "--on",
758        type=str,
759        nargs="*",
760        help="The column to join on. Can be specified multiple times. The model grain will be used if not specified.",
761    )
762    @argument(
763        "--skip-columns",
764        type=str,
765        nargs="*",
766        help="The column(s) to skip when comparing the source and target table.",
767    )
768    @argument(
769        "--model",
770        type=str,
771        help="The model to diff against when source and target are environments and not tables.",
772    )
773    @argument(
774        "--where",
775        type=str,
776        help="An optional where statement to filter results.",
777    )
778    @argument(
779        "--limit",
780        type=int,
781        default=20,
782        help="The limit of the sample dataframe.",
783    )
784    @argument(
785        "--show-sample",
786        action="store_true",
787        help="Show a sample of the rows that differ. With many columns, the output can be very wide.",
788    )
789    @argument(
790        "--decimals",
791        type=int,
792        default=3,
793        help="The number of decimal places to keep when comparing floating point columns. Default: 3",
794    )
795    @argument(
796        "--select-model",
797        type=str,
798        nargs="*",
799        help="Specify one or more models to data diff. Use wildcards to diff multiple models. Ex: '*' (all models with applied plan diffs), 'demo.model+' (this and downstream models), 'git:feature_branch' (models with direct modifications in this branch only)",
800    )
801    @argument(
802        "--skip-grain-check",
803        action="store_true",
804        help="Disable the check for a primary key (grain) that is missing or is not unique.",
805    )
806    @argument(
807        "--warn-grain-check",
808        action="store_true",
809        help="Warn if any selected model is missing a grain, and compute diffs for the remaining models.",
810    )
811    @argument(
812        "--schema-diff-ignore-case",
813        action="store_true",
814        help="If set, when performing a schema diff the case of column names is ignored when matching between the two schemas. For example, 'col_a' in the source schema and 'COL_A' in the target schema will be treated as the same column.",
815    )
816    @line_magic
817    @pass_sqlmesh_context
818    def table_diff(self, context: Context, line: str) -> None:
819        """Show the diff between two tables.
820
821        Can either be two tables or two environments and a model.
822        """
823        args = parse_argstring(self.table_diff, line)
824        source, target = args.source_to_target.split(":")
825        select_models = {args.model} if args.model else args.select_model or None
826        context.table_diff(
827            source=source,
828            target=target,
829            on=args.on,
830            skip_columns=args.skip_columns,
831            select_models=select_models,
832            where=args.where,
833            limit=args.limit,
834            show_sample=args.show_sample,
835            decimals=args.decimals,
836            skip_grain_check=args.skip_grain_check,
837            warn_grain_check=args.warn_grain_check,
838            schema_diff_ignore_case=args.schema_diff_ignore_case,
839        )

Show the diff between two tables.

Can either be two tables or two environments and a model.

@magic_arguments()
@argument('model_name', nargs='?', type=str, help='The name of the model to get the table name for.')
@argument('--environment', type=str, help='The environment to source the model version from.')
@argument('--prod', action='store_true', help='If set, return the name of the physical table that will be used in production for the model version promoted in the target environment.')
@line_magic
@pass_sqlmesh_context
def table_name(self, context: sqlmesh.core.context.Context, line: str) -> None:
841    @magic_arguments()
842    @argument(
843        "model_name",
844        nargs="?",
845        type=str,
846        help="The name of the model to get the table name for.",
847    )
848    @argument(
849        "--environment",
850        type=str,
851        help="The environment to source the model version from.",
852    )
853    @argument(
854        "--prod",
855        action="store_true",
856        help="If set, return the name of the physical table that will be used in production for the model version promoted in the target environment.",
857    )
858    @line_magic
859    @pass_sqlmesh_context
860    def table_name(self, context: Context, line: str) -> None:
861        """Prints the name of the physical table for the given model."""
862        args = parse_argstring(self.table_name, line)
863        context.console.log_status_update(
864            context.table_name(args.model_name, args.environment, args.prod)
865        )

Prints the name of the physical table for the given model.

@magic_arguments()
@argument('pipeline', nargs='?', type=str, help='The dlt pipeline to attach for this SQLMesh project.')
@argument('--table', '-t', type=str, nargs='*', help='The specific dlt tables to refresh in the SQLMesh models.')
@argument('--force', '-f', action='store_true', help='If set, existing models are overwritten with the new DLT tables.')
@argument('--dlt-path', type=str, help='The DLT pipelines working directory, where DLT stores pipeline state (by default ~/.dlt/pipelines).')
@line_magic
@pass_sqlmesh_context
def dlt_refresh(self, context: sqlmesh.core.context.Context, line: str) -> None:
867    @magic_arguments()
868    @argument(
869        "pipeline",
870        nargs="?",
871        type=str,
872        help="The dlt pipeline to attach for this SQLMesh project.",
873    )
874    @argument(
875        "--table",
876        "-t",
877        type=str,
878        nargs="*",
879        help="The specific dlt tables to refresh in the SQLMesh models.",
880    )
881    @argument(
882        "--force",
883        "-f",
884        action="store_true",
885        help="If set, existing models are overwritten with the new DLT tables.",
886    )
887    @argument(
888        "--dlt-path",
889        type=str,
890        help="The DLT pipelines working directory, where DLT stores pipeline state (by default ~/.dlt/pipelines).",
891    )
892    @line_magic
893    @pass_sqlmesh_context
894    def dlt_refresh(self, context: Context, line: str) -> None:
895        """Attaches to a DLT pipeline with the option to update specific or all missing tables in the SQLMesh project."""
896        from sqlmesh.integrations.dlt import generate_dlt_models
897
898        args = parse_argstring(self.dlt_refresh, line)
899        sqlmesh_models = generate_dlt_models(
900            context, args.pipeline, list(args.table or []), args.force, args.dlt_path
901        )
902        if sqlmesh_models:
903            model_names = "\n".join([f"- {model_name}" for model_name in sqlmesh_models])
904            context.console.log_success(f"Updated SQLMesh project with models:\n{model_names}")
905        else:
906            context.console.log_success("All SQLMesh models are up to date.")

Attaches to a DLT pipeline with the option to update specific or all missing tables in the SQLMesh project.

@magic_arguments()
@argument('--read', type=str, default='', help='The input dialect of the sql string.')
@argument('--write', type=str, default='', help='The output dialect of the sql string.')
@line_cell_magic
@pass_sqlmesh_context
def rewrite(self, context: sqlmesh.core.context.Context, line: str, sql: str) -> None:
908    @magic_arguments()
909    @argument(
910        "--read",
911        type=str,
912        default="",
913        help="The input dialect of the sql string.",
914    )
915    @argument(
916        "--write",
917        type=str,
918        default="",
919        help="The output dialect of the sql string.",
920    )
921    @line_cell_magic
922    @pass_sqlmesh_context
923    def rewrite(self, context: Context, line: str, sql: str) -> None:
924        """Rewrite a sql expression with semantic references into an executable query.
925
926        https://sqlmesh.readthedocs.io/en/latest/concepts/metrics/overview/
927        """
928        args = parse_argstring(self.rewrite, line)
929        context.console.show_sql(
930            context.rewrite(sql, args.read).sql(
931                dialect=args.write or context.config.dialect, pretty=True
932            )
933        )

Rewrite a sql expression with semantic references into an executable query.

https://sqlmesh.readthedocs.io/en/latest/concepts/metrics/overview/

@magic_arguments()
@argument('--transpile', '-t', type=str, help='Transpile project models to the specified dialect.')
@argument('--check', action='store_true', help='Whether or not to check formatting (but not actually format anything).', default=None)
@argument('--append-newline', action='store_true', help='Include a newline at the end of the output.', default=None)
@argument('--no-rewrite-casts', action='store_true', help='Preserve the existing casts, without rewriting them to use the :: syntax.', default=None)
@format_arguments
@line_magic
@pass_sqlmesh_context
def format(self, context: sqlmesh.core.context.Context, line: str) -> bool:
935    @magic_arguments()
936    @argument(
937        "--transpile",
938        "-t",
939        type=str,
940        help="Transpile project models to the specified dialect.",
941    )
942    @argument(
943        "--check",
944        action="store_true",
945        help="Whether or not to check formatting (but not actually format anything).",
946        default=None,
947    )
948    @argument(
949        "--append-newline",
950        action="store_true",
951        help="Include a newline at the end of the output.",
952        default=None,
953    )
954    @argument(
955        "--no-rewrite-casts",
956        action="store_true",
957        help="Preserve the existing casts, without rewriting them to use the :: syntax.",
958        default=None,
959    )
960    @format_arguments
961    @line_magic
962    @pass_sqlmesh_context
963    def format(self, context: Context, line: str) -> bool:
964        """Format all SQL models and audits."""
965        format_opts = vars(parse_argstring(self.format, line))
966        if format_opts.pop("no_rewrite_casts", None):
967            format_opts["rewrite_casts"] = False
968
969        return context.format(**{k: v for k, v in format_opts.items() if v is not None})

Format all SQL models and audits.

@magic_arguments()
@argument('environment', type=str, help='The environment to diff local state against.')
@line_magic
@pass_sqlmesh_context
def diff(self, context: sqlmesh.core.context.Context, line: str) -> None:
971    @magic_arguments()
972    @argument("environment", type=str, help="The environment to diff local state against.")
973    @line_magic
974    @pass_sqlmesh_context
975    def diff(self, context: Context, line: str) -> None:
976        """Show the diff between the local state and the target environment."""
977        args = parse_argstring(self.diff, line)
978        context.diff(args.environment)

Show the diff between the local state and the target environment.

@magic_arguments()
@argument('environment', type=str, help='The environment to invalidate.')
@line_magic
@pass_sqlmesh_context
def invalidate(self, context: sqlmesh.core.context.Context, line: str) -> None:
980    @magic_arguments()
981    @argument("environment", type=str, help="The environment to invalidate.")
982    @line_magic
983    @pass_sqlmesh_context
984    def invalidate(self, context: Context, line: str) -> None:
985        """Invalidate the target environment, forcing its removal during the next run of the janitor process."""
986        args = parse_argstring(self.invalidate, line)
987        context.invalidate_environment(args.environment)

Invalidate the target environment, forcing its removal during the next run of the janitor process.

@magic_arguments()
@argument('--ignore-ttl', action='store_true', help="Cleanup snapshots that are not referenced in any environment, regardless of when they're set to expire")
@line_magic
@pass_sqlmesh_context
def janitor(self, context: sqlmesh.core.context.Context, line: str) -> None:
 989    @magic_arguments()
 990    @argument(
 991        "--ignore-ttl",
 992        action="store_true",
 993        help="Cleanup snapshots that are not referenced in any environment, regardless of when they're set to expire",
 994    )
 995    @line_magic
 996    @pass_sqlmesh_context
 997    def janitor(self, context: Context, line: str) -> None:
 998        """Run the janitor process to clean up old environments and expired snapshots."""
 999        args = parse_argstring(self.janitor, line)
1000        context.run_janitor(ignore_ttl=args.ignore_ttl)

Run the janitor process to clean up old environments and expired snapshots.

@magic_arguments()
@argument('model', type=str)
@argument('--query', '-q', type=str, nargs='+', default=[], help="Queries that will be used to generate data for the model's dependencies.")
@argument('--overwrite', '-o', action='store_true', help='When true, the fixture file will be overwritten in case it already exists.')
@argument('--var', '-v', type=str, nargs='+', help='Key-value pairs that will define variables needed by the model.')
@argument('--path', '-p', type=str, help="The file path corresponding to the fixture, relative to the test directory. By default, the fixture will be created under the test directory and the file name will be inferred based on the test's name.")
@argument('--name', '-n', type=str, help="The name of the test that will be created. By default, it's inferred based on the model's name.")
@argument('--include-ctes', action='store_true', help='When true, CTE fixtures will also be generated.')
@line_magic
@pass_sqlmesh_context
def create_test(self, context: sqlmesh.core.context.Context, line: str) -> None:
1002    @magic_arguments()
1003    @argument("model", type=str)
1004    @argument(
1005        "--query",
1006        "-q",
1007        type=str,
1008        nargs="+",
1009        default=[],
1010        help="Queries that will be used to generate data for the model's dependencies.",
1011    )
1012    @argument(
1013        "--overwrite",
1014        "-o",
1015        action="store_true",
1016        help="When true, the fixture file will be overwritten in case it already exists.",
1017    )
1018    @argument(
1019        "--var",
1020        "-v",
1021        type=str,
1022        nargs="+",
1023        help="Key-value pairs that will define variables needed by the model.",
1024    )
1025    @argument(
1026        "--path",
1027        "-p",
1028        type=str,
1029        help="The file path corresponding to the fixture, relative to the test directory. "
1030        "By default, the fixture will be created under the test directory and the file "
1031        "name will be inferred based on the test's name.",
1032    )
1033    @argument(
1034        "--name",
1035        "-n",
1036        type=str,
1037        help="The name of the test that will be created. By default, it's inferred based on the model's name.",
1038    )
1039    @argument(
1040        "--include-ctes",
1041        action="store_true",
1042        help="When true, CTE fixtures will also be generated.",
1043    )
1044    @line_magic
1045    @pass_sqlmesh_context
1046    def create_test(self, context: Context, line: str) -> None:
1047        """Generate a unit test fixture for a given model."""
1048        args = parse_argstring(self.create_test, line)
1049        queries = iter(args.query)
1050        variables = iter(args.var) if args.var else None
1051        context.create_test(
1052            args.model,
1053            input_queries={k: v.strip('"') for k, v in dict(zip(queries, queries)).items()},
1054            overwrite=args.overwrite,
1055            variables=dict(zip(variables, variables)) if variables else None,
1056            path=args.path,
1057            name=args.name,
1058            include_ctes=args.include_ctes,
1059        )

Generate a unit test fixture for a given model.

@magic_arguments()
@argument('tests', nargs='*', type=str)
@argument('--pattern', '-k', nargs='*', type=str, help='Only run tests that match the pattern of substring.')
@argument('--verbose', '-v', action='count', default=0, help='Verbose output. Use -vv for very verbose.')
@argument('--preserve-fixtures', action='store_true', help='Preserve the fixture tables in the testing database, useful for debugging.')
@line_magic
@pass_sqlmesh_context
def run_test(self, context: sqlmesh.core.context.Context, line: str) -> None:
1061    @magic_arguments()
1062    @argument("tests", nargs="*", type=str)
1063    @argument(
1064        "--pattern",
1065        "-k",
1066        nargs="*",
1067        type=str,
1068        help="Only run tests that match the pattern of substring.",
1069    )
1070    @argument(
1071        "--verbose",
1072        "-v",
1073        action="count",
1074        default=0,
1075        help="Verbose output. Use -vv for very verbose.",
1076    )
1077    @argument(
1078        "--preserve-fixtures",
1079        action="store_true",
1080        help="Preserve the fixture tables in the testing database, useful for debugging.",
1081    )
1082    @line_magic
1083    @pass_sqlmesh_context
1084    def run_test(self, context: Context, line: str) -> None:
1085        """Run unit test(s)."""
1086        args = parse_argstring(self.run_test, line)
1087
1088        context.test(
1089            match_patterns=args.pattern,
1090            tests=args.tests,
1091            verbosity=Verbosity(args.verbose),
1092            preserve_fixtures=args.preserve_fixtures,
1093            stream=StringIO(),  # consume the output instead of redirecting to stdout
1094        )

Run unit test(s).

@magic_arguments()
@argument('models', type=str, nargs='*', help='A model to audit. Multiple models can be audited.')
@argument('--start', '-s', type=str, help='Start date to audit.')
@argument('--end', '-e', type=str, help='End date to audit.')
@argument('--execution-time', type=str, help='Execution time.')
@line_magic
@pass_sqlmesh_context
def audit(self, context: sqlmesh.core.context.Context, line: str) -> bool:
1096    @magic_arguments()
1097    @argument(
1098        "models", type=str, nargs="*", help="A model to audit. Multiple models can be audited."
1099    )
1100    @argument("--start", "-s", type=str, help="Start date to audit.")
1101    @argument("--end", "-e", type=str, help="End date to audit.")
1102    @argument("--execution-time", type=str, help="Execution time.")
1103    @line_magic
1104    @pass_sqlmesh_context
1105    def audit(self, context: Context, line: str) -> bool:
1106        """Run audit(s)"""
1107        args = parse_argstring(self.audit, line)
1108        return context.audit(
1109            models=args.models, start=args.start, end=args.end, execution_time=args.execution_time
1110        )

Run audit(s)

@magic_arguments()
@argument('environment', nargs='?', type=str, help='The environment to check intervals for.')
@argument('--no-signals', action='store_true', help='Disable signal checks and only show missing intervals.', default=False)
@argument('--select-model', type=str, nargs='*', help='Select specific model changes that should be included in the plan.')
@argument('--start', '-s', type=str, help='Start date of intervals to check for.')
@argument('--end', '-e', type=str, help='End date of intervals to check for.')
@line_magic
@pass_sqlmesh_context
def check_intervals(self, context: sqlmesh.core.context.Context, line: str) -> None:
1112    @magic_arguments()
1113    @argument("environment", nargs="?", type=str, help="The environment to check intervals for.")
1114    @argument(
1115        "--no-signals",
1116        action="store_true",
1117        help="Disable signal checks and only show missing intervals.",
1118        default=False,
1119    )
1120    @argument(
1121        "--select-model",
1122        type=str,
1123        nargs="*",
1124        help="Select specific model changes that should be included in the plan.",
1125    )
1126    @argument("--start", "-s", type=str, help="Start date of intervals to check for.")
1127    @argument("--end", "-e", type=str, help="End date of intervals to check for.")
1128    @line_magic
1129    @pass_sqlmesh_context
1130    def check_intervals(self, context: Context, line: str) -> None:
1131        """Show missing intervals in an environment, respecting signals."""
1132        args = parse_argstring(self.check_intervals, line)
1133
1134        context.console.show_intervals(
1135            context.check_intervals(
1136                environment=args.environment,
1137                no_signals=args.no_signals,
1138                select_models=args.select_model,
1139                start=args.start,
1140                end=args.end,
1141            )
1142        )

Show missing intervals in an environment, respecting signals.

@magic_arguments()
@argument('--skip-connection', action='store_true', help='Skip the connection test.', default=False)
@argument('--verbose', '-v', action='count', default=0, help='Verbose output. Use -vv for very verbose.')
@line_magic
@pass_sqlmesh_context
def info(self, context: sqlmesh.core.context.Context, line: str) -> None:
1144    @magic_arguments()
1145    @argument(
1146        "--skip-connection",
1147        action="store_true",
1148        help="Skip the connection test.",
1149        default=False,
1150    )
1151    @argument(
1152        "--verbose",
1153        "-v",
1154        action="count",
1155        default=0,
1156        help="Verbose output. Use -vv for very verbose.",
1157    )
1158    @line_magic
1159    @pass_sqlmesh_context
1160    def info(self, context: Context, line: str) -> None:
1161        """Display SQLMesh project information."""
1162        args = parse_argstring(self.info, line)
1163        context.print_info(skip_connection=args.skip_connection, verbosity=Verbosity(args.verbose))

Display SQLMesh project information.

@magic_arguments()
@line_magic
@pass_sqlmesh_context
def rollback(self, context: sqlmesh.core.context.Context, line: str) -> None:
1165    @magic_arguments()
1166    @line_magic
1167    @pass_sqlmesh_context
1168    def rollback(self, context: Context, line: str) -> None:
1169        """Rollback SQLMesh to the previous migration."""
1170        context.rollback()

Rollback SQLMesh to the previous migration.

@magic_arguments()
@line_magic
@pass_sqlmesh_context
def clean(self, context: sqlmesh.core.context.Context, line: str) -> None:
1172    @magic_arguments()
1173    @line_magic
1174    @pass_sqlmesh_context
1175    def clean(self, context: Context, line: str) -> None:
1176        """Clears the SQLMesh cache and any build artifacts."""
1177        context.clear_caches()
1178        context.console.log_success("SQLMesh cache and build artifacts cleared")

Clears the SQLMesh cache and any build artifacts.

@magic_arguments()
@line_magic
@pass_sqlmesh_context
def environments(self, context: sqlmesh.core.context.Context, line: str) -> None:
1180    @magic_arguments()
1181    @line_magic
1182    @pass_sqlmesh_context
1183    def environments(self, context: Context, line: str) -> None:
1184        """Prints the list of SQLMesh environments with its expiry datetime."""
1185        context.print_environment_names()

Prints the list of SQLMesh environments with its expiry datetime.

@magic_arguments()
@argument('--models', '--model', type=str, nargs='*', help='A model to lint. Multiple models can be linted. If no models are specified, every model will be linted.')
@line_magic
@pass_sqlmesh_context
def lint(self, context: sqlmesh.core.context.Context, line: str) -> None:
1187    @magic_arguments()
1188    @argument(
1189        "--models",
1190        "--model",
1191        type=str,
1192        nargs="*",
1193        help="A model to lint. Multiple models can be linted. If no models are specified, every model will be linted.",
1194    )
1195    @line_magic
1196    @pass_sqlmesh_context
1197    def lint(self, context: Context, line: str) -> None:
1198        """Run linter for target model(s)"""
1199        args = parse_argstring(self.lint, line)
1200        context.lint_models(args.models)

Run linter for target model(s)

@magic_arguments()
@line_magic
@pass_sqlmesh_context
def destroy(self, context: sqlmesh.core.context.Context, line: str) -> None:
1202    @magic_arguments()
1203    @line_magic
1204    @pass_sqlmesh_context
1205    def destroy(self, context: Context, line: str) -> None:
1206        """Removes all project resources, engine-managed objects, state tables and clears the SQLMesh cache."""
1207        context.destroy()

Removes all project resources, engine-managed objects, state tables and clears the SQLMesh cache.

magics = {'line': {'context': 'context', 'init': 'init', 'model': 'model', 'test': 'test', 'plan': 'plan', 'run_dag': 'run_dag', 'evaluate': 'evaluate', 'render': 'render', 'dag': 'dag', 'migrate': 'migrate', 'create_external_models': 'create_external_models', 'table_diff': 'table_diff', 'table_name': 'table_name', 'dlt_refresh': 'dlt_refresh', 'rewrite': 'rewrite', 'format': 'format', 'diff': 'diff', 'invalidate': 'invalidate', 'janitor': 'janitor', 'create_test': 'create_test', 'run_test': 'run_test', 'audit': 'audit', 'check_intervals': 'check_intervals', 'info': 'info', 'rollback': 'rollback', 'clean': 'clean', 'environments': 'environments', 'lint': 'lint', 'destroy': 'destroy'}, 'cell': {'model': 'model', 'test': 'test', 'fetchdf': 'fetchdf', 'rewrite': 'rewrite'}}
registered = True
Inherited Members
IPython.core.magic.Magics
Magics
options_table
shell
arg_err
format_latex
parse_options
default_option
traitlets.config.configurable.Configurable
config
parent
section_names
update_config
class_get_help
class_get_trait_help
class_print_help
class_config_section
class_config_rst_doc
traitlets.traitlets.HasTraits
setup_instance
cross_validation_lock
hold_trait_notifications
notify_change
on_trait_change
observe
unobserve
unobserve_all
add_traits
set_trait
class_trait_names
class_traits
class_own_traits
has_trait
trait_has_value
trait_values
trait_defaults
trait_names
traits
trait_metadata
class_own_trait_events
trait_events
def register_magics() -> None:
1210def register_magics() -> None:
1211    try:
1212        shell = get_ipython()  # type: ignore
1213        shell.register_magics(SQLMeshMagics)
1214    except NameError:
1215        pass