Edit on GitHub

sqlmesh.cli.main

  1from __future__ import annotations
  2
  3import logging
  4import os
  5import sys
  6import typing as t
  7
  8import click
  9
 10from sqlmesh import configure_logging
 11from sqlmesh.cli import error_handler
 12from sqlmesh.cli import options as opt
 13from sqlmesh.cli.example_project import ProjectTemplate, init_example_project
 14from sqlmesh.core.config import load_configs
 15from sqlmesh.core.context import Context
 16from sqlmesh.utils.date import TimeLike
 17from sqlmesh.utils.errors import MissingDependencyError
 18
 19logger = logging.getLogger(__name__)
 20
 21SKIP_LOAD_COMMANDS = ("create_external_models", "migrate", "rollback")
 22
 23
 24def _sqlmesh_version() -> str:
 25    try:
 26        from sqlmesh import __version__
 27
 28        return __version__
 29    except ImportError:
 30        return "0.0.0"
 31
 32
 33@click.group(no_args_is_help=True)
 34@click.version_option(version=_sqlmesh_version(), message="%(version)s")
 35@opt.paths
 36@opt.config
 37@click.option(
 38    "--gateway",
 39    type=str,
 40    help="The name of the gateway.",
 41)
 42@click.option(
 43    "--ignore-warnings",
 44    is_flag=True,
 45    help="Ignore warnings.",
 46)
 47@click.option(
 48    "--debug",
 49    is_flag=True,
 50    help="Enable debug mode.",
 51)
 52@click.option(
 53    "--log-to-stdout",
 54    is_flag=True,
 55    help="Display logs in stdout.",
 56)
 57@click.option(
 58    "--log-file-dir",
 59    type=str,
 60    help="The directory to write log files to.",
 61)
 62@click.pass_context
 63@error_handler
 64def cli(
 65    ctx: click.Context,
 66    paths: t.List[str],
 67    config: t.Optional[str] = None,
 68    gateway: t.Optional[str] = None,
 69    ignore_warnings: bool = False,
 70    debug: bool = False,
 71    log_to_stdout: bool = False,
 72    log_file_dir: t.Optional[str] = None,
 73) -> None:
 74    """SQLMesh command line tool."""
 75    if "--help" in sys.argv:
 76        return
 77
 78    load = True
 79
 80    if len(paths) == 1:
 81        path = os.path.abspath(paths[0])
 82        if ctx.invoked_subcommand in ("init", "ui"):
 83            ctx.obj = path
 84            return
 85        elif ctx.invoked_subcommand in SKIP_LOAD_COMMANDS:
 86            load = False
 87
 88    configs = load_configs(config, Context.CONFIG_TYPE, paths)
 89    log_limit = list(configs.values())[0].log_limit
 90    configure_logging(
 91        debug, ignore_warnings, log_to_stdout, log_limit=log_limit, log_file_dir=log_file_dir
 92    )
 93
 94    try:
 95        context = Context(
 96            paths=paths,
 97            config=configs,
 98            gateway=gateway,
 99            load=load,
100        )
101    except Exception:
102        if debug:
103            logger.exception("Failed to initialize SQLMesh context")
104        raise
105
106    if load and not context.models:
107        raise click.ClickException(
108            f"`{paths}` doesn't seem to have any models... cd into the proper directory or specify the path(s) with -p."
109        )
110
111    ctx.obj = context
112
113
114@cli.command("init")
115@click.argument("sql_dialect", required=False)
116@click.option(
117    "-t",
118    "--template",
119    type=str,
120    help="Project template. Supported values: airflow, dbt, default, empty.",
121)
122@click.pass_context
123@error_handler
124def init(
125    ctx: click.Context, sql_dialect: t.Optional[str] = None, template: t.Optional[str] = None
126) -> None:
127    """Create a new SQLMesh repository."""
128    try:
129        project_template = ProjectTemplate(template.lower() if template else "default")
130    except ValueError:
131        raise click.ClickException(f"Invalid project template '{template}'")
132    init_example_project(ctx.obj, dialect=sql_dialect, template=project_template)
133
134
135@cli.command("render")
136@click.argument("model")
137@opt.start_time
138@opt.end_time
139@opt.execution_time
140@opt.expand
141@click.option(
142    "--dialect",
143    type=str,
144    help="The SQL dialect to render the query as.",
145)
146@click.option("--no-format", is_flag=True, help="Disable fancy formatting of the query.")
147@click.pass_context
148@error_handler
149def render(
150    ctx: click.Context,
151    model: str,
152    start: TimeLike,
153    end: TimeLike,
154    execution_time: t.Optional[TimeLike] = None,
155    expand: t.Optional[t.Union[bool, t.Iterable[str]]] = None,
156    dialect: t.Optional[str] = None,
157    no_format: bool = False,
158) -> None:
159    """Render a model's query, optionally expanding referenced models."""
160    rendered = ctx.obj.render(
161        model,
162        start=start,
163        end=end,
164        execution_time=execution_time,
165        expand=expand,
166    )
167
168    sql = rendered.sql(pretty=True, dialect=ctx.obj.config.dialect if dialect is None else dialect)
169    if no_format:
170        print(sql)
171    else:
172        ctx.obj.console.show_sql(sql)
173
174
175@cli.command("evaluate")
176@click.argument("model")
177@opt.start_time
178@opt.end_time
179@opt.execution_time
180@click.option(
181    "--limit",
182    type=int,
183    help="The number of rows which the query should be limited to.",
184)
185@click.pass_context
186@error_handler
187def evaluate(
188    ctx: click.Context,
189    model: str,
190    start: TimeLike,
191    end: TimeLike,
192    execution_time: t.Optional[TimeLike] = None,
193    limit: t.Optional[int] = None,
194) -> None:
195    """Evaluate a model and return a dataframe with a default limit of 1000."""
196    df = ctx.obj.evaluate(
197        model,
198        start=start,
199        end=end,
200        execution_time=execution_time,
201        limit=limit,
202    )
203    ctx.obj.console.log_success(df)
204
205
206@cli.command("format")
207@click.option(
208    "-t",
209    "--transpile",
210    type=str,
211    help="Transpile project models to the specified dialect.",
212)
213@click.option(
214    "--append-newline",
215    is_flag=True,
216    help="Include a newline at the end of each file.",
217    default=None,
218)
219@click.option(
220    "--normalize",
221    is_flag=True,
222    help="Whether or not to normalize identifiers to lowercase.",
223    default=None,
224)
225@click.option(
226    "--pad",
227    type=int,
228    help="Determines the pad size in a formatted string.",
229)
230@click.option(
231    "--indent",
232    type=int,
233    help="Determines the indentation size in a formatted string.",
234)
235@click.option(
236    "--normalize-functions",
237    type=str,
238    help="Whether or not to normalize all function names. Possible values are: 'upper', 'lower'",
239)
240@click.option(
241    "--leading-comma",
242    is_flag=True,
243    help="Determines whether or not the comma is leading or trailing in select expressions. Default is trailing.",
244    default=None,
245)
246@click.option(
247    "--max-text-width",
248    type=int,
249    help="The max number of characters in a segment before creating new lines in pretty mode.",
250)
251@click.pass_context
252@error_handler
253def format(ctx: click.Context, **kwargs: t.Any) -> None:
254    """Format all SQL models."""
255    ctx.obj.format(**{k: v for k, v in kwargs.items() if v is not None})
256
257
258@cli.command("diff")
259@click.argument("environment")
260@click.pass_context
261@error_handler
262def diff(ctx: click.Context, environment: t.Optional[str] = None) -> None:
263    """Show the diff between the local state and the target environment."""
264    if ctx.obj.diff(environment, detailed=True):
265        exit(1)
266
267
268@cli.command("plan")
269@click.argument("environment", required=False)
270@opt.start_time
271@opt.end_time
272@opt.execution_time
273@click.option(
274    "--create-from",
275    type=str,
276    help="The environment to create the target environment from if it doesn't exist. Default: prod.",
277)
278@click.option(
279    "--skip-tests",
280    is_flag=True,
281    help="Skip tests prior to generating the plan if they are defined.",
282)
283@click.option(
284    "--restate-model",
285    "-r",
286    type=str,
287    multiple=True,
288    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.",
289)
290@click.option(
291    "--no-gaps",
292    is_flag=True,
293    help="Ensure that new snapshots have no data gaps when comparing to existing snapshots for matching models in the target environment.",
294)
295@click.option(
296    "--skip-backfill",
297    is_flag=True,
298    help="Skip the backfill step.",
299)
300@click.option(
301    "--forward-only",
302    is_flag=True,
303    help="Create a plan for forward-only changes.",
304    default=None,
305)
306@click.option(
307    "--effective-from",
308    type=str,
309    required=False,
310    help="The effective date from which to apply forward-only changes on production.",
311)
312@click.option(
313    "--no-prompts",
314    is_flag=True,
315    help="Disable interactive prompts for the backfill time range. Please note that if this flag is set and there are uncategorized changes, plan creation will fail.",
316    default=None,
317)
318@click.option(
319    "--auto-apply",
320    is_flag=True,
321    help="Automatically apply the new plan after creation.",
322    default=None,
323)
324@click.option(
325    "--no-auto-categorization",
326    is_flag=True,
327    help="Disable automatic change categorization.",
328    default=None,
329)
330@click.option(
331    "--include-unmodified",
332    is_flag=True,
333    help="Include unmodified models in the target environment.",
334    default=None,
335)
336@click.option(
337    "--select-model",
338    type=str,
339    multiple=True,
340    help="Select specific model changes that should be included in the plan.",
341)
342@click.option(
343    "--backfill-model",
344    type=str,
345    multiple=True,
346    help="Backfill only the models whose names match the expression. This is supported only when targeting a development environment.",
347)
348@click.option(
349    "--no-diff",
350    is_flag=True,
351    help="Hide text differences for changed models.",
352    default=None,
353)
354@click.option(
355    "--run",
356    is_flag=True,
357    help="Run latest intervals as part of the plan application (prod environment only).",
358)
359@click.option(
360    "--enable-preview",
361    is_flag=True,
362    help="Enable preview for forward-only models when targeting a development environment.",
363    default=None,
364)
365@opt.verbose
366@click.pass_context
367@error_handler
368def plan(
369    ctx: click.Context, verbose: bool, environment: t.Optional[str] = None, **kwargs: t.Any
370) -> None:
371    """Apply local changes to the target environment."""
372    context = ctx.obj
373    restate_models = kwargs.pop("restate_model") or None
374    select_models = kwargs.pop("select_model") or None
375    backfill_models = kwargs.pop("backfill_model") or None
376    context.console.verbose = verbose
377    context.plan(
378        environment,
379        restate_models=restate_models,
380        select_models=select_models,
381        backfill_models=backfill_models,
382        **kwargs,
383    )
384
385
386@cli.command("run")
387@click.argument("environment", required=False)
388@opt.start_time
389@opt.end_time
390@click.option("--skip-janitor", is_flag=True, help="Skip the janitor task.")
391@click.option(
392    "--ignore-cron",
393    is_flag=True,
394    help="Run for all missing intervals, ignoring individual cron schedules.",
395)
396@click.pass_context
397@error_handler
398def run(ctx: click.Context, environment: t.Optional[str] = None, **kwargs: t.Any) -> None:
399    """Evaluate missing intervals for the target environment."""
400    context = ctx.obj
401    success = context.run(environment, **kwargs)
402    if not success:
403        raise click.ClickException("Run DAG Failed. See output for details.")
404
405
406@cli.command("invalidate")
407@click.argument("environment", required=True)
408@click.option(
409    "--sync",
410    "-s",
411    is_flag=True,
412    help="Wait for the environment to be deleted before returning. If not specified, the environment will be deleted asynchronously by the janitor process. This option requires a connection to the data warehouse.",
413)
414@click.pass_context
415@error_handler
416def invalidate(ctx: click.Context, environment: str, **kwargs: t.Any) -> None:
417    """Invalidate the target environment, forcing its removal during the next run of the janitor process."""
418    context = ctx.obj
419    context.invalidate_environment(environment, **kwargs)
420
421
422@cli.command("dag")
423@click.argument("file", required=True)
424@click.option(
425    "--select-model",
426    type=str,
427    multiple=True,
428    help="Select specific models to include in the dag.",
429)
430@click.pass_context
431@error_handler
432def dag(ctx: click.Context, file: str, select_model: t.List[str]) -> None:
433    """Render the DAG as an html file."""
434    rendered_dag_path = ctx.obj.render_dag(file, select_model)
435    if rendered_dag_path:
436        ctx.obj.console.log_success(f"Generated the dag to {rendered_dag_path}")
437
438
439@cli.command("create_test")
440@click.argument("model")
441@click.option(
442    "-q",
443    "--query",
444    "queries",
445    type=(str, str),
446    multiple=True,
447    required=True,
448    help="Queries that will be used to generate data for the model's dependencies.",
449)
450@click.option(
451    "-o",
452    "--overwrite",
453    "overwrite",
454    is_flag=True,
455    default=False,
456    help="When true, the fixture file will be overwritten in case it already exists.",
457)
458@click.option(
459    "-v",
460    "--var",
461    "variables",
462    type=(str, str),
463    multiple=True,
464    help="Key-value pairs that will define variables needed by the model.",
465)
466@click.option(
467    "-p",
468    "--path",
469    "path",
470    help=(
471        "The file path corresponding to the fixture, relative to the test directory. "
472        "By default, the fixture will be created under the test directory and the file "
473        "name will be inferred based on the test's name."
474    ),
475)
476@click.option(
477    "-n",
478    "--name",
479    "name",
480    help="The name of the test that will be created. By default, it's inferred based on the model's name.",
481)
482@click.option(
483    "--include-ctes",
484    "include_ctes",
485    is_flag=True,
486    default=False,
487    help="When true, CTE fixtures will also be generated.",
488)
489@click.pass_obj
490@error_handler
491def create_test(
492    obj: Context,
493    model: str,
494    queries: t.List[t.Tuple[str, str]],
495    overwrite: bool = False,
496    variables: t.Optional[t.List[t.Tuple[str, str]]] = None,
497    path: t.Optional[str] = None,
498    name: t.Optional[str] = None,
499    include_ctes: bool = False,
500) -> None:
501    """Generate a unit test fixture for a given model."""
502    obj.create_test(
503        model,
504        input_queries=dict(queries),
505        overwrite=overwrite,
506        variables=dict(variables) if variables else None,
507        path=path,
508        name=name,
509        include_ctes=include_ctes,
510    )
511
512
513@cli.command("test")
514@opt.match_pattern
515@opt.verbose
516@click.option(
517    "--preserve-fixtures",
518    is_flag=True,
519    default=False,
520    help="Preserve the fixture tables in the testing database, useful for debugging.",
521)
522@click.argument("tests", nargs=-1)
523@click.pass_obj
524@error_handler
525def test(
526    obj: Context,
527    k: t.List[str],
528    verbose: bool,
529    preserve_fixtures: bool,
530    tests: t.List[str],
531) -> None:
532    """Run model unit tests."""
533    result = obj.test(
534        match_patterns=k,
535        tests=tests,
536        verbose=verbose,
537        preserve_fixtures=preserve_fixtures,
538    )
539    if not result.wasSuccessful():
540        exit(1)
541
542
543@cli.command("audit")
544@click.option(
545    "--model",
546    "models",
547    multiple=True,
548    help="A model to audit. Multiple models can be audited.",
549)
550@opt.start_time
551@opt.end_time
552@opt.execution_time
553@click.pass_obj
554@error_handler
555def audit(
556    obj: Context,
557    models: t.Iterator[str],
558    start: TimeLike,
559    end: TimeLike,
560    execution_time: t.Optional[TimeLike] = None,
561) -> None:
562    """Run audits for the target model(s)."""
563    obj.audit(models=models, start=start, end=end, execution_time=execution_time)
564
565
566@cli.command("fetchdf")
567@click.argument("sql")
568@click.pass_context
569@error_handler
570def fetchdf(ctx: click.Context, sql: str) -> None:
571    """Run a SQL query and display the results."""
572    context = ctx.obj
573    context.console.log_success(context.fetchdf(sql))
574
575
576@cli.command("info")
577@click.pass_obj
578@error_handler
579def info(obj: Context) -> None:
580    """
581    Print information about a SQLMesh project.
582
583    Includes counts of project models and macros and connection tests for the data warehouse.
584    """
585    obj.print_info()
586
587
588@cli.command("ui")
589@click.option(
590    "--host",
591    type=str,
592    default="127.0.0.1",
593    help="Bind socket to this host. Default: 127.0.0.1",
594)
595@click.option(
596    "--port",
597    type=int,
598    default=8000,
599    help="Bind socket to this port. Default: 8000",
600)
601@click.option(
602    "--mode",
603    type=click.Choice(["ide", "default", "docs", "plan"], case_sensitive=False),
604    default="default",
605    help="Mode to start the UI in. Default: default",
606)
607@click.pass_context
608@error_handler
609def ui(ctx: click.Context, host: str, port: int, mode: str) -> None:
610    """Start a browser-based SQLMesh UI."""
611    try:
612        import uvicorn
613    except ModuleNotFoundError as e:
614        raise MissingDependencyError(
615            "Missing UI dependencies. Run `pip install 'sqlmesh[web]'` to install them."
616        ) from e
617
618    os.environ["PROJECT_PATH"] = ctx.obj
619    os.environ["UI_MODE"] = mode
620    if ctx.parent:
621        config = ctx.parent.params.get("config")
622        gateway = ctx.parent.params.get("gateway")
623        if config:
624            os.environ["CONFIG"] = config
625        if gateway:
626            os.environ["GATEWAY"] = gateway
627    uvicorn.run(
628        "web.server.main:app",
629        host=host,
630        port=port,
631        log_level="info",
632        timeout_keep_alive=300,
633    )
634
635
636@cli.command("migrate")
637@click.pass_context
638@error_handler
639def migrate(ctx: click.Context) -> None:
640    """Migrate SQLMesh to the current running version."""
641    ctx.obj.migrate()
642
643
644@cli.command("rollback")
645@click.pass_obj
646@error_handler
647def rollback(obj: Context) -> None:
648    """Rollback SQLMesh to the previous migration."""
649    obj.rollback()
650
651
652@cli.command("create_external_models")
653@click.pass_obj
654@error_handler
655def create_external_models(obj: Context) -> None:
656    """Create a schema file containing external model schemas."""
657    obj.create_external_models()
658
659
660@cli.command("table_diff")
661@click.argument("source_to_target", required=True, metavar="SOURCE:TARGET")
662@click.argument("model", required=False)
663@click.option(
664    "-o",
665    "--on",
666    type=str,
667    multiple=True,
668    help="The column to join on. Can be specified multiple times. The model grain will be used if not specified.",
669)
670@click.option(
671    "--where",
672    type=str,
673    help="An optional where statement to filter results.",
674)
675@click.option(
676    "--limit",
677    type=int,
678    default=20,
679    help="The limit of the sample dataframe.",
680)
681@click.option(
682    "--show-sample",
683    is_flag=True,
684    help="Show a sample of the rows that differ. With many columns, the output can be very wide.",
685)
686@click.pass_obj
687@error_handler
688def table_diff(
689    obj: Context, source_to_target: str, model: t.Optional[str], **kwargs: t.Any
690) -> None:
691    """Show the diff between two tables."""
692    source, target = source_to_target.split(":")
693    obj.table_diff(
694        source=source,
695        target=target,
696        model_or_snapshot=model,
697        **kwargs,
698    )
699
700
701@cli.command("rewrite")
702@click.argument("sql")
703@click.option(
704    "--read",
705    type=str,
706    help="The input dialect of the sql string.",
707)
708@click.option(
709    "--write",
710    type=str,
711    help="The output dialect of the sql string.",
712)
713@click.pass_obj
714@error_handler
715def rewrite(obj: Context, sql: str, read: str = "", write: str = "") -> None:
716    """Rewrite a SQL expression with semantic references into an executable query.
717
718    https://sqlmesh.readthedocs.io/en/latest/concepts/metrics/overview/
719    """
720    obj.console.show_sql(
721        obj.rewrite(sql, dialect=read).sql(pretty=True, dialect=write or obj.config.dialect),
722    )
723
724
725@cli.command("prompt")
726@click.argument("prompt")
727@click.option(
728    "-e",
729    "--evaluate",
730    is_flag=True,
731    help="Evaluate the generated SQL query and display the results.",
732)
733@click.option(
734    "-t",
735    "--temperature",
736    type=float,
737    help="Sampling temperature. 0.0 - precise and predictable, 0.5 - balanced, 1.0 - creative. Default: 0.7",
738    default=0.7,
739)
740@opt.verbose
741@click.pass_context
742@error_handler
743def prompt(
744    ctx: click.Context, prompt: str, evaluate: bool, temperature: float, verbose: bool
745) -> None:
746    """Uses LLM to generate a SQL query from a prompt."""
747    from sqlmesh.integrations.llm import LLMIntegration
748
749    context = ctx.obj
750
751    llm_integration = LLMIntegration(
752        context.models.values(),
753        context.engine_adapter.dialect,
754        temperature=temperature,
755        verbose=verbose,
756    )
757    query = llm_integration.query(prompt)
758
759    context.console.log_status_update(query)
760    if evaluate:
761        context.console.log_success(context.fetchdf(query))
762
763
764@cli.command("clean")
765@click.pass_obj
766@error_handler
767def clean(obj: Context) -> None:
768    """Clears the SQLMesh cache and any build artifacts."""
769    obj.clear_caches()
770
771
772@cli.command("table_name")
773@click.argument("model_name", required=True)
774@click.option(
775    "--dev",
776    is_flag=True,
777    help="Print the name of the snapshot table used for previews in development environments.",
778    default=False,
779)
780@click.pass_obj
781@error_handler
782def table_name(obj: Context, model_name: str, dev: bool) -> None:
783    """Prints the name of the physical table for the given model."""
784    print(obj.table_name(model_name, dev))
cli = <Group cli>

SQLMesh command line tool.

init = <Command init>

Create a new SQLMesh repository.

render = <Command render>

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

evaluate = <Command evaluate>

Evaluate a model and return a dataframe with a default limit of 1000.

format = <Command format>

Format all SQL models.

diff = <Command diff>

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

plan = <Command plan>

Apply local changes to the target environment.

run = <Command run>

Evaluate missing intervals for the target environment.

invalidate = <Command invalidate>

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

dag = <Command dag>

Render the DAG as an html file.

create_test = <Command create_test>

Generate a unit test fixture for a given model.

test = <Command test>

Run model unit tests.

audit = <Command audit>

Run audits for the target model(s).

fetchdf = <Command fetchdf>

Run a SQL query and display the results.

info = <Command info>

Print information about a SQLMesh project.

Includes counts of project models and macros and connection tests for the data warehouse.

ui = <Command ui>

Start a browser-based SQLMesh UI.

migrate = <Command migrate>

Migrate SQLMesh to the current running version.

rollback = <Command rollback>

Rollback SQLMesh to the previous migration.

create_external_models = <Command create_external_models>

Create a schema file containing external model schemas.

table_diff = <Command table_diff>

Show the diff between two tables.

rewrite = <Command rewrite>

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

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

prompt = <Command prompt>

Uses LLM to generate a SQL query from a prompt.

clean = <Command clean>

Clears the SQLMesh cache and any build artifacts.

table_name = <Command table_name>

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