From e18a83d8c940c28c0a521462d2d687c2d6bb06e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Fredh=C3=B8i?= Date: Fri, 24 Jul 2026 13:24:26 +0100 Subject: [PATCH 1/2] Perf: add opt-in project-index loading for render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andreas Fredhøi --- docs/reference/cli.md | 3 ++ sqlmesh/cli/main.py | 14 +++++-- sqlmesh/core/context.py | 31 ++++++++++++++-- tests/cli/test_cli.py | 13 +++++++ tests/core/test_context.py | 76 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 130 insertions(+), 7 deletions(-) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 82b4161277..2ea74259ab 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -447,6 +447,9 @@ Options: only they will be expanded as raw queries. --dialect TEXT The SQL dialect to render the query as. --no-format Disable fancy formatting of the query. + --use-project-index Use the persistent project index to load and + render only the target model and its upstream + dependencies. --max-text-width INTEGER The max number of characters in a segment before creating new lines in pretty mode. --leading-comma Determines whether or not the comma is leading diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index c2dc1e3dbb..406a4fe106 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -141,8 +141,8 @@ def cli( if ctx.invoked_subcommand in SKIP_LOAD_COMMANDS: load = False - # Unlike the other commands above, lint can scope its own load for multi-project contexts. - if ctx.invoked_subcommand == "lint": + # These commands can scope their own load for multi-project contexts. + if ctx.invoked_subcommand in ("lint", "render"): load = False configs = load_configs(config, Context.CONFIG_TYPE, paths, dotenv_path=dotenv) @@ -284,6 +284,11 @@ def init( help="The SQL dialect to render the query as.", ) @click.option("--no-format", is_flag=True, help="Disable fancy formatting of the query.") +@click.option( + "--use-project-index", + is_flag=True, + help="Use the persistent project index to load and render only the target model and its upstream dependencies.", +) @opt.format_options @click.pass_context @error_handler @@ -297,19 +302,20 @@ def render( expand: t.Optional[t.Union[bool, t.Iterable[str]]] = None, dialect: t.Optional[str] = None, no_format: bool = False, + use_project_index: bool = False, **format_kwargs: t.Any, ) -> None: """Render a model's query, optionally expanding referenced models.""" - model = ctx.obj.get_model(model, raise_if_missing=True) - rendered = ctx.obj.render( model, start=start, end=end, execution_time=execution_time, expand=expand, + use_project_index=use_project_index, ) + model = ctx.obj.get_model(model, raise_if_missing=True) format_config = ctx.obj.config_for_node(model).format format_kwargs = { **format_config.generator_options, diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index e335253016..6f9fcec683 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1199,6 +1199,7 @@ def render( end: t.Optional[TimeLike] = None, execution_time: t.Optional[TimeLike] = None, expand: t.Union[bool, t.Iterable[str]] = False, + use_project_index: bool = False, **kwargs: t.Any, ) -> exp.Expr: """Renders a model's query, expanding macros with provided kwargs, and optionally expanding referenced models. @@ -1211,12 +1212,20 @@ def render( expand: Whether or not to use expand materialized models, defaults to False. If True, all referenced models are expanded as raw queries. If a list, only referenced models are expanded as raw queries. + use_project_index: Whether to use the persistent project index to load and + render only the target model and its transitive upstream dependencies. Returns: The rendered expression. """ execution_time = execution_time or now() + if not self._loaded: + target_fqns = ( + {self._node_or_snapshot_to_fqn(model_or_snapshot)} if use_project_index else None + ) + self.load(model_fqns=target_fqns, use_project_index=use_project_index) + model = self.get_model(model_or_snapshot, raise_if_missing=True) if expand and not isinstance(expand, bool): @@ -1245,7 +1254,19 @@ def render( ) return next(pandas_to_sql(t.cast(pd.DataFrame, df), model.columns_to_types)) - snapshots = self.snapshots + if use_project_index: + # Only the target model and its transitive upstream dependencies can be referenced + # by the rendered query, so there is no need to create snapshots for the rest. + upstream_fqns = {model.fqn, *self.dag.upstream(model.fqn)} + upstream_models: UniqueKeyDict[str, Model] = UniqueKeyDict( + "models", {fqn: m for fqn, m in self._models.items() if fqn in upstream_fqns} + ) + snapshots = self._snapshots( + upstream_models, + include_standalone_audits=False, + ) + else: + snapshots = self.snapshots deployability_index = DeployabilityIndex.create(snapshots.values(), start=start) return model.render_query_or_raise( @@ -3009,9 +3030,13 @@ def _get_engine_adapter(self, gateway: t.Optional[str] = None) -> EngineAdapter: return self.engine_adapter def _snapshots( - self, models_override: t.Optional[UniqueKeyDict[str, Model]] = None + self, + models_override: t.Optional[UniqueKeyDict[str, Model]] = None, + include_standalone_audits: bool = True, ) -> t.Dict[str, Snapshot]: - nodes = {**(models_override or self._models), **self._standalone_audits} + nodes: t.Dict[str, Node] = dict(models_override or self._models) + if include_standalone_audits: + nodes.update(self._standalone_audits) snapshots = self._nodes_to_snapshots(nodes) stored_snapshots = self.state_reader.get_snapshots(snapshots.values()) diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index a8ee1aaa39..db6ac87a8f 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -2111,6 +2111,19 @@ def test_render(runner: CliRunner, tmp_path: Path): assert expected in cleaned_output + indexed_result = runner.invoke( + cli, + [ + "--paths", + str(tmp_path), + "render", + "sqlmesh_example.full_model", + "--use-project-index", + "--no-format", + ], + ) + assert indexed_result.exit_code == 0 + @time_machine.travel(FREEZE_TIME) def test_signals(runner: CliRunner, tmp_path: Path): diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 75737f1edb..bae4c8f985 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -276,6 +276,82 @@ def test_render_seed_model(sushi_context, assert_exp_eq): ) +@pytest.mark.slow +def test_render_only_creates_snapshots_for_upstream_models(sushi_context: Context): + model = sushi_context.get_model("sushi.top_waiters", raise_if_missing=True) + upstream_fqns = {model.fqn, *sushi_context.dag.upstream(model.fqn)} + + # Sanity check that the project contains models outside of the target model's subgraph. + assert set(sushi_context.models) - upstream_fqns + + with patch.object( + sushi_context.state_reader, + "get_snapshots", + wraps=sushi_context.state_reader.get_snapshots, + ) as default_get_snapshots_mock: + sushi_context.render("sushi.top_waiters") + + default_requested_names = { + snapshot.name + for call_args in default_get_snapshots_mock.call_args_list + for snapshot in call_args.args[0] + } + assert set(sushi_context.models) <= default_requested_names + + with patch.object( + sushi_context.state_reader, + "get_snapshots", + wraps=sushi_context.state_reader.get_snapshots, + ) as get_snapshots_mock: + sushi_context.render("sushi.top_waiters", use_project_index=True) + + requested_names = { + snapshot.name + for call_args in get_snapshots_mock.call_args_list + for snapshot in call_args.args[0] + } + assert model.fqn in requested_names + assert requested_names == upstream_fqns + + +def test_render_only_loads_upstream_model_files(tmp_path: pathlib.Path) -> None: + create_temp_file( + tmp_path, + pathlib.Path("models", "a.sql"), + "MODEL(name a, kind FULL); SELECT 1 AS col;", + ) + create_temp_file( + tmp_path, + pathlib.Path("models", "b.sql"), + "MODEL(name b, kind FULL); SELECT col FROM a;", + ) + create_temp_file( + tmp_path, + pathlib.Path("models", "c.sql"), + "MODEL(name c, kind FULL); SELECT col FROM b;", + ) + config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) + + # Populate the persistent model path/dependency index. + Context(config=config, paths=tmp_path, load=False).load(use_project_index=True) + + ctx = Context(config=config, paths=tmp_path, load=False) + loader = t.cast(SqlMeshLoader, ctx._loaders[0]) + with patch.object( + loader, + "_load_sql_models", + wraps=loader._load_sql_models, + ) as load_sql_models_mock: + ctx.render("b", use_project_index=True) + + selected_paths = load_sql_models_mock.call_args.kwargs["selected_paths"] + assert {path.name for path in selected_paths} == {"a.sql", "b.sql"} + assert set(ctx.models) == { + ctx.get_model("a", raise_if_missing=True).fqn, + ctx.get_model("b", raise_if_missing=True).fqn, + } + + @pytest.mark.slow def test_diff(sushi_context: Context, mocker: MockerFixture): mock_console = mocker.Mock() From 4e60e2c67926511ca96dedc7aa651836858598f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Fredh=C3=B8i?= Date: Thu, 27 Aug 2026 22:43:45 +0200 Subject: [PATCH 2/2] Config: add project-index default for render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andreas Fredhøi --- docs/examples/sqlmesh_cli_crash_course.md | 7 ++++- docs/guides/configuration.md | 32 ++++++++++++++++++++++- docs/reference/cli.md | 3 ++- docs/reference/configuration.md | 6 +++++ sqlmesh/cli/main.py | 5 ++-- sqlmesh/core/config/__init__.py | 1 + sqlmesh/core/config/render.py | 13 +++++++++ sqlmesh/core/config/root.py | 4 +++ sqlmesh/core/context.py | 8 ++++-- tests/core/test_config.py | 8 ++++++ tests/core/test_context.py | 20 ++++++++++++-- 11 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 sqlmesh/core/config/render.py diff --git a/docs/examples/sqlmesh_cli_crash_course.md b/docs/examples/sqlmesh_cli_crash_course.md index 0bf5780f12..e2a240f22d 100644 --- a/docs/examples/sqlmesh_cli_crash_course.md +++ b/docs/examples/sqlmesh_cli_crash_course.md @@ -738,6 +738,11 @@ You'll use these commands as needed to validate that your changes are behaving a This is a great way to verify that your model's SQL is looking as expected before applying the changes. It is especially important if you're migrating from one query engine to another (ex: postgres to databricks). +In large projects, add `--use-project-index` to load only the model being rendered and its upstream +dependencies. To enable this behavior by default, set +[`render.use_project_index`](../reference/configuration.md#render) to `true` in the project +configuration. + === "SQLMesh" ```bash @@ -1254,4 +1259,4 @@ If you notice you have a lot of old development schemas/data, you can clean them ```bash tcloud sqlmesh janitor - ``` \ No newline at end of file + ``` diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index 41f4b05594..851e59111d 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -90,6 +90,7 @@ The `config` sub-module API documentation describes the individual classes used - [Connection configuration](https://sqlmesh.readthedocs.io/en/latest/_readthedocs/html/sqlmesh/core/config/connection.html) (separate classes for each supported database/engine) - [Scheduler configuration](https://sqlmesh.readthedocs.io/en/latest/_readthedocs/html/sqlmesh/core/config/scheduler.html) (separate classes for each supported scheduler) - [Plan change categorization configuration](https://sqlmesh.readthedocs.io/en/latest/_readthedocs/html/sqlmesh/core/config/categorizer.html#CategorizerConfig): `CategorizerConfig()` +- [Render configuration](https://sqlmesh.readthedocs.io/en/latest/_readthedocs/html/sqlmesh/core/config/render.html): `RenderConfig()` - [User configuration](https://sqlmesh.readthedocs.io/en/latest/_readthedocs/html/sqlmesh/core/user.html#User): `User()` - [Notification configuration](https://sqlmesh.readthedocs.io/en/latest/_readthedocs/html/sqlmesh/core/notification_target.html) (separate classes for each notification target) @@ -331,7 +332,7 @@ The cache directory is automatically created if it doesn't exist. You can clear #### Project index -The `--use-project-index` option on supported commands maintains a persistent model dependency index in the cache directory. Each project writes a file named `__model_index.json`. +The `--use-project-index` option on the `lint` and `render` commands maintains a persistent model dependency index in the cache directory. Each project writes a file named `__model_index.json`. The option can be enabled by default for each command with `linter.use_project_index` or `render.use_project_index`, respectively. A full project load with the option enabled creates or refreshes the index. SQLMesh invalidates it when relevant configuration, gateway, macro, audit, or signal metadata changes, or when the set of model files changes. If the index is missing, invalid, or stale, SQLMesh safely falls back to a full project load and rebuilds it. @@ -1503,6 +1504,35 @@ SQLMesh provides a linter that checks for potential issues in your models' code. Learn more about linting configuration in the [linting guide](./linter.md). +### Rendering + +By default, `sqlmesh render` loads every model in the project. In large projects, you can use the +persistent project index to load only the model being rendered and its transitive upstream +dependencies. Enable indexed rendering for an individual command with `--use-project-index`, or +make it the project default with the `render.use_project_index` configuration option. + +=== "YAML" + + ```yaml linenums="1" + render: + use_project_index: true + ``` + +=== "Python" + + ```python linenums="1" + from sqlmesh.core.config import Config, ModelDefaultsConfig, RenderConfig + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + render=RenderConfig(use_project_index=True), + ) + ``` + +`Context.render` uses the configured value when `use_project_index` is omitted. Passing +`use_project_index=False` explicitly disables indexed rendering for that API call. See the +[`render` CLI reference](../reference/cli.md#render) for the other rendering options. + ### Debug mode To enable debug mode set the `SQLMESH_DEBUG` environment variable to one of the following values: "1", "true", "t", "yes" or "y". diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 2ea74259ab..07ec867a1c 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -449,7 +449,8 @@ Options: --no-format Disable fancy formatting of the query. --use-project-index Use the persistent project index to load and render only the target model and its upstream - dependencies. + dependencies. Can also be enabled with + render.use_project_index. --max-text-width INTEGER The max number of characters in a segment before creating new lines in pretty mode. --leading-comma Determines whether or not the comma is leading diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index a1bf400c32..ddcc59c0b1 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -67,6 +67,12 @@ See all the keys allowed in `model_defaults` at the [model configuration referen | `linter.enabled` | Whether linting is enabled (Default: `False`) | boolean | N | | `linter.use_project_index` | Whether to use the persistent project index for linting. Targeted linting loads selected models and their upstream dependencies. (Default: `False`) | boolean | N | +### Render + +| Option | Description | Type | Required | +|----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|----------| +| `render.use_project_index` | Whether to use the persistent project index when rendering. Only the target model and its upstream dependencies are loaded. (Default: `False`) | boolean | N | + ### Variables The `variables` key can be used to provide values for user-defined variables, accessed using the [`@VAR` macro function](../concepts/macros/sqlmesh_macros.md#global-variables) in SQL model definitions, [`context.var` method](../concepts/models/python_models.md#global-variables) in Python model definitions, and [`evaluator.var` method](../concepts/macros/sqlmesh_macros.md#accessing-global-variable-values) in Python macro functions. diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index 406a4fe106..a621163cc9 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -287,7 +287,8 @@ def init( @click.option( "--use-project-index", is_flag=True, - help="Use the persistent project index to load and render only the target model and its upstream dependencies.", + default=None, + help="Use the persistent project index to load and render only the target model and its upstream dependencies. Can also be enabled with render.use_project_index.", ) @opt.format_options @click.pass_context @@ -302,7 +303,7 @@ def render( expand: t.Optional[t.Union[bool, t.Iterable[str]]] = None, dialect: t.Optional[str] = None, no_format: bool = False, - use_project_index: bool = False, + use_project_index: t.Optional[bool] = None, **format_kwargs: t.Any, ) -> None: """Render a model's query, optionally expanding referenced models.""" diff --git a/sqlmesh/core/config/__init__.py b/sqlmesh/core/config/__init__.py index 50d2d9a5a2..83d958eb7e 100644 --- a/sqlmesh/core/config/__init__.py +++ b/sqlmesh/core/config/__init__.py @@ -37,6 +37,7 @@ from sqlmesh.core.config.naming import NameInferenceConfig as NameInferenceConfig from sqlmesh.core.config.linter import LinterConfig as LinterConfig from sqlmesh.core.config.plan import PlanConfig as PlanConfig +from sqlmesh.core.config.render import RenderConfig as RenderConfig from sqlmesh.core.config.root import Config as Config, DbtConfig as DbtConfig from sqlmesh.core.config.run import RunConfig as RunConfig from sqlmesh.core.config.scheduler import BuiltInSchedulerConfig as BuiltInSchedulerConfig diff --git a/sqlmesh/core/config/render.py b/sqlmesh/core/config/render.py new file mode 100644 index 0000000000..0b5ef84474 --- /dev/null +++ b/sqlmesh/core/config/render.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from sqlmesh.core.config.base import BaseConfig + + +class RenderConfig(BaseConfig): + """Configuration for rendering model queries. + + Args: + use_project_index: Whether to use the persistent project index when rendering. + """ + + use_project_index: bool = False diff --git a/sqlmesh/core/config/root.py b/sqlmesh/core/config/root.py index b36b7dadc1..9d1971b93a 100644 --- a/sqlmesh/core/config/root.py +++ b/sqlmesh/core/config/root.py @@ -35,6 +35,7 @@ from sqlmesh.core.config.naming import NameInferenceConfig as NameInferenceConfig from sqlmesh.core.config.linter import LinterConfig as LinterConfig from sqlmesh.core.config.plan import PlanConfig +from sqlmesh.core.config.render import RenderConfig from sqlmesh.core.config.run import RunConfig from sqlmesh.core.config.dbt import DbtConfig from sqlmesh.core.config.scheduler import ( @@ -141,6 +142,7 @@ class Config(BaseConfig): format: The formatting options for SQL code. ui: The UI configuration for SQLMesh. plan: The plan configuration. + render: The render configuration. migration: The migration configuration. variables: A dictionary of variables that can be used in models / macros. disable_anonymized_analytics: Whether to disable the anonymized analytics collection. @@ -183,6 +185,7 @@ class Config(BaseConfig): format: FormatConfig = FormatConfig() ui: UIConfig = UIConfig() plan: PlanConfig = PlanConfig() + render: RenderConfig = RenderConfig() migration: MigrationConfig = MigrationConfig() model_naming: NameInferenceConfig = NameInferenceConfig() variables: t.Dict[str, t.Any] = {} @@ -208,6 +211,7 @@ class Config(BaseConfig): "ui": UpdateStrategy.NESTED_UPDATE, "loader_kwargs": UpdateStrategy.KEY_UPDATE, "plan": UpdateStrategy.NESTED_UPDATE, + "render": UpdateStrategy.NESTED_UPDATE, "before_all": UpdateStrategy.EXTEND, "after_all": UpdateStrategy.EXTEND, "linter": UpdateStrategy.NESTED_UPDATE, diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 6f9fcec683..dc0b8804d2 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1199,7 +1199,7 @@ def render( end: t.Optional[TimeLike] = None, execution_time: t.Optional[TimeLike] = None, expand: t.Union[bool, t.Iterable[str]] = False, - use_project_index: bool = False, + use_project_index: t.Optional[bool] = None, **kwargs: t.Any, ) -> exp.Expr: """Renders a model's query, expanding macros with provided kwargs, and optionally expanding referenced models. @@ -1213,12 +1213,16 @@ def render( If True, all referenced models are expanded as raw queries. If a list, only referenced models are expanded as raw queries. use_project_index: Whether to use the persistent project index to load and - render only the target model and its transitive upstream dependencies. + render only the target model and its transitive upstream dependencies. If + omitted, the value of ``render.use_project_index`` is used. Returns: The rendered expression. """ execution_time = execution_time or now() + use_project_index = ( + self.config.render.use_project_index if use_project_index is None else use_project_index + ) if not self._loaded: target_fqns = ( diff --git a/tests/core/test_config.py b/tests/core/test_config.py index 0da5b6e22f..d75e8d8d03 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -16,6 +16,7 @@ ModelDefaultsConfig, BigQueryConnectionConfig, MotherDuckConnectionConfig, + RenderConfig, BuiltInSchedulerConfig, EnvironmentSuffixTarget, TableNamingConvention, @@ -70,6 +71,13 @@ def python_config_path(tmp_path_factory) -> Path: return config_path +def test_render_config() -> None: + config = Config.parse_obj({"render": {"use_project_index": True}}) + + assert config.render == RenderConfig(use_project_index=True) + assert Config().update_with(config).render.use_project_index is True + + def test_update_with_gateways(): gateway0_config = GatewayConfig(connection=DuckDBConnectionConfig()) gateway1_config = GatewayConfig(connection=DuckDBConnectionConfig(database="test")) diff --git a/tests/core/test_context.py b/tests/core/test_context.py index bae4c8f985..4b74f2917f 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -30,6 +30,7 @@ LinterConfig, ModelDefaultsConfig, PlanConfig, + RenderConfig, SnowflakeConnectionConfig, ) from sqlmesh.core.context import Context @@ -330,7 +331,10 @@ def test_render_only_loads_upstream_model_files(tmp_path: pathlib.Path) -> None: pathlib.Path("models", "c.sql"), "MODEL(name c, kind FULL); SELECT col FROM b;", ) - config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + render=RenderConfig(use_project_index=True), + ) # Populate the persistent model path/dependency index. Context(config=config, paths=tmp_path, load=False).load(use_project_index=True) @@ -342,7 +346,7 @@ def test_render_only_loads_upstream_model_files(tmp_path: pathlib.Path) -> None: "_load_sql_models", wraps=loader._load_sql_models, ) as load_sql_models_mock: - ctx.render("b", use_project_index=True) + ctx.render("b") selected_paths = load_sql_models_mock.call_args.kwargs["selected_paths"] assert {path.name for path in selected_paths} == {"a.sql", "b.sql"} @@ -351,6 +355,18 @@ def test_render_only_loads_upstream_model_files(tmp_path: pathlib.Path) -> None: ctx.get_model("b", raise_if_missing=True).fqn, } + non_indexed_ctx = Context(config=config, paths=tmp_path, load=False) + non_indexed_loader = t.cast(SqlMeshLoader, non_indexed_ctx._loaders[0]) + with patch.object( + non_indexed_loader, + "_load_sql_models", + wraps=non_indexed_loader._load_sql_models, + ) as load_sql_models_mock: + non_indexed_ctx.render("b", use_project_index=False) + + assert load_sql_models_mock.call_args.kwargs["selected_paths"] is None + assert len(non_indexed_ctx.models) == 3 + @pytest.mark.slow def test_diff(sushi_context: Context, mocker: MockerFixture):