Repository: TobikoData/sqlmesh
Pull request #3108 (2024-09-09 02:52:03)

Feat!: Add physical_schema_mapping property and deprecate physical_schema_override
This PR essentially upgrades `physical_schema_override` config property to allow the use of regex's, which brings it to feature parity with the existing `environment_catalog_mapping` property.

However, `physical_schema_override` couldn't have its semantics changed without breaking backwards compatibility, so it's been deprecated in favour of a new property `physical_schema_mapping` which has the regex sematics.

Note that this change only affects the project config / loader. The models themselves retain their existing `physical_schema_override` property which is a string containing the actual schema override relevant for that model and not the mapping from the config.


Patch:
diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md
index 8f5b12cd..9b345db8 100644
--- a/docs/guides/configuration.md
+++ b/docs/guides/configuration.md
@@ -198,20 +198,21 @@ The rest of this page provides additional detail for some of the configuration o
 
 SQLMesh creates schemas, physical tables, and views in the data warehouse/engine. Learn more about why and how SQLMesh creates schema in the ["Why does SQLMesh create schemas?" FAQ](../faq/faq.md#schema-question).
 
-The default SQLMesh behavior described in the FAQ is appropriate for most deployments, but you can override where SQLMesh creates physical tables and views with the `physical_schema_override`, `environment_suffix_target`, and `environment_catalog_mapping` configuration options. These options are in the [environments](../reference/configuration.md#environments) section of the configuration reference page.
+The default SQLMesh behavior described in the FAQ is appropriate for most deployments, but you can override where SQLMesh creates physical tables and views with the `physical_schema_mapping`, `environment_suffix_target`, and `environment_catalog_mapping` configuration options. These options are in the [environments](../reference/configuration.md#environments) section of the configuration reference page.
 
 #### Physical table schemas
 By default, SQLMesh creates physical tables for a model with a naming convention of `sqlmesh__[model schema]`.
 
-This can be overridden on a per-schema basis using the `physical_schema_override` option, which removes the `sqlmesh__` prefix and uses the name you provide.
+This can be overridden on a per-schema basis using the `physical_schema_mapping` option, which removes the `sqlmesh__` prefix and uses the [regex pattern](https://docs.python.org/3/library/re.html#regular-expression-syntax) you provide to map the schemas defined in your model to their corresponding physical schemas.
 
-This example configuration overrides the default physical schemas for the `my_schema` model schema:
+This example configuration overrides the default physical schemas for the `my_schema` model schema and any model schemas starting with `dev`:
 
 === "YAML"
 
     ```yaml linenums="1"
-    physical_schema_override:
-      my_schema: my_new_schema
+    physical_schema_mapping:
+      '^my_schema$': my_new_schema,
+      '^dev.*': development
     ```
 
 === "Python"
@@ -221,13 +222,23 @@ This example configuration overrides the default physical schemas for the `my_sc
 
     config = Config(
         model_defaults=ModelDefaultsConfig(dialect=<dialect>),
-        physical_schema_override={"my_schema":"my_new_schema"},
+        physical_schema_mapping={
+            "^my_schema$": "my_new_schema",
+            '^dev.*': "development"
+        },
     )
     ```
 
-If you had a model name of `my_schema.table`, the physical table would be created as `my_new_schema.table_<fingerprint>` instead of the default behavior of `sqlmesh__my_schema.table_<fingerprint>`.
+This config causes the following mapping behaviour:
+
+| Model name            | Default physical location                 | Resolved physical location
+| --------------------- | ----------------------------------------- | ------------------------------------ |
+| `my_schema.my_table`  | `sqlmesh__my_schema.table_<fingerprint>`  | `my_new_schema.table_<fingerprint>`  |
+| `dev_schema.my_table` | `sqlmesh__dev_schema.table_<fingerprint>` | `development.table_<fingerprint>`    |
+| `other.my_table`      | `sqlmesh__other.table_<fingerprint>`      | `sqlmesh__other.table_<fingerprint>` |
+
 
-This key only applies to the _physical tables_ that SQLMesh creates - the views are still created in `my_schema` (prod) or `my_schema__<env>`.
+This only applies to the _physical tables_ that SQLMesh creates - the views are still created in `my_schema` (prod) or `my_schema__<env>`.
 
 #### Disable environment-specific schemas
 
diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md
index 4734b39d..ba4fdb33 100644
--- a/docs/reference/configuration.md
+++ b/docs/reference/configuration.md
@@ -32,7 +32,8 @@ Configuration options for SQLMesh environment creation and promotion.
 | `pinned_environments`         | The list of development environments that are exempt from deletion due to expiration                                                                                                                                                                                                               | list[string]         | N        |
 | `time_column_format`          | The default format to use for all model time columns. This time format uses [python format codes](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) (Default: `%Y-%m-%d`)                                                                                        | string               | N        |
 | `default_target_environment`  | The name of the environment that will be the default target for the `sqlmesh plan` and `sqlmesh run` commands. (Default: `prod`)                                                                                                                                                                   | string               | N        |
-| `physical_schema_override`    | A mapping from model schema names to names of schemas in which physical tables for the corresponding models will be placed - [addition details](../guides/configuration.md#physical-schema-override). (Default physical schema name: `sqlmesh__[model schema]`)                                    | string               | N        |
+| `physical_schema_override`    | (Deprecated) Use `physical_schema_mapping` instead. A mapping from model schema names to names of schemas in which physical tables for the corresponding models will be placed.                                                                                                                    | dict[string, string] | N        |
+| `physical_schema_mapping`     | A mapping from regular expressions to names of schemas in which physical tables for the corresponding models [will be placed](../guides/configuration.md#physical-table-schemas). (Default physical schema name: `sqlmesh__[model schema]`)                                                        | dict[string, string] | N        |
 | `environment_suffix_target`   | Whether SQLMesh views should append their environment name to the `schema` or `table` - [additional details](../guides/configuration.md#view-schema-override). (Default: `schema`)                                                                                                                 | string               | N        |
 | `environment_catalog_mapping` | A mapping from regular expressions to catalog names. The catalog name is used to determine the target catalog for a given environment.                                                                                                                                                             | dict[string, string] | N        |
 | `log_limit`                   | The default number of logs to keep (Default: `20`)                                                                                                                                                                                                                                                 | int                  | N        |
diff --git a/sqlmesh/core/config/root.py b/sqlmesh/core/config/root.py
index 565ccc00..80edfd7e 100644
--- a/sqlmesh/core/config/root.py
+++ b/sqlmesh/core/config/root.py
@@ -4,6 +4,7 @@ import pickle
 import re
 import typing as t
 import zlib
+import logging
 
 from pydantic import Field
 from sqlglot import exp
@@ -41,6 +42,8 @@ from sqlmesh.utils.pydantic import (
     model_validator_v1_args,
 )
 
+logger = logging.getLogger(__name__)
+
 
 class Config(BaseConfig):
     """An object used by a Context to configure your SQLMesh project.
@@ -65,7 +68,7 @@ class Config(BaseConfig):
         loader_kwargs: Key-value arguments to pass to the loader instance.
         env_vars: A dictionary of environmental variable names and values.
         model_defaults: Default values for model definitions.
-        physical_schema_override: A mapping from model schema names to names of schemas in which physical tables for corresponding models will be placed.
+        physical_schema_mapping: A mapping from regular expressions to names of schemas in which physical tables for corresponding models will be placed.
         environment_suffix_target: Indicates whether to append the environment name to the schema or table name.
         environment_catalog_mapping: A mapping from regular expressions to catalog names. The catalog name is used to determine the target catalog for a given environment.
         default_target_environment: The name of the environment that will be the default target for the `sqlmesh plan` and `sqlmesh run` commands.
@@ -99,7 +102,7 @@ class Config(BaseConfig):
     loader_kwargs: t.Dict[str, t.Any] = {}
     env_vars: t.Dict[str, str] = {}
     username: str = ""
-    physical_schema_override: t.Dict[str, str] = {}
+    physical_schema_mapping: t.Dict[re.Pattern, str] = {}
     environment_suffix_target: EnvironmentSuffixTarget = Field(
         default=EnvironmentSuffixTarget.default
     )
@@ -146,7 +149,7 @@ class Config(BaseConfig):
         except Exception:
             return value
 
-    @field_validator("environment_catalog_mapping", mode="before")
+    @field_validator("environment_catalog_mapping", "physical_schema_mapping", mode="before")
     @classmethod
     def _validate_regex_keys(
         cls, value: t.Dict[str | re.Pattern, t.Any]
@@ -171,16 +174,38 @@ class Config(BaseConfig):
                     f"The `{plan_deprecated}` config is deprecated. Please use the `plan.{plan_deprecated}` config instead."
                 )
 
+        if "physical_schema_override" in values:
+            logger.warning(
+                "`physical_schema_override` is deprecated. Please use `physical_schema_mapping` instead"
+            )
+
+            if "physical_schema_mapping" in values:
+                raise ConfigError(
+                    "Only one of `physical_schema_override` and `physical_schema_mapping` can be specified"
+                )
+
+            physical_schema_override: t.Dict[str, str] = values.pop("physical_schema_override")
+            # translate physical_schema_override to physical_schema_mapping
+            values["physical_schema_mapping"] = {
+                f"^{k}$": v for k, v in physical_schema_override.items()
+            }
+
         return values
 
     @model_validator(mode="after")
     @model_validator_v1_args
     def _normalize_fields_after(cls, values: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
         dialect = values["model_defaults"].dialect
-        values["environment_catalog_mapping"] = {
-            k: normalize_identifiers(v, dialect=dialect).name
-            for k, v in values.get("environment_catalog_mapping", {}).items()
-        }
+
+        def _normalize_identifiers(key: str) -> None:
+            values[key] = {
+                k: normalize_identifiers(v, dialect=dialect).name
+                for k, v in values.get(key, {}).items()
+            }
+
+        _normalize_identifiers("environment_catalog_mapping")
+        _normalize_identifiers("physical_schema_mapping")
+
         return values
 
     def get_default_test_connection(
diff --git a/sqlmesh/core/loader.py b/sqlmesh/core/loader.py
index 76cdbd8a..697871f3 100644
--- a/sqlmesh/core/loader.py
+++ b/sqlmesh/core/loader.py
@@ -353,7 +353,7 @@ class SqlMeshLoader(Loader):
                         module_path=context_path,
                         dialect=config.model_defaults.dialect,
                         time_column_format=config.time_column_format,
-                        physical_schema_override=config.physical_schema_override,
+                        physical_schema_mapping=config.physical_schema_mapping,
                         project=config.project,
                         default_catalog=self._context.default_catalog,
                         variables=variables,
@@ -402,7 +402,7 @@ class SqlMeshLoader(Loader):
                             jinja_macros=jinja_macros,
                             dialect=config.model_defaults.dialect,
                             time_column_format=config.time_column_format,
-                            physical_schema_override=config.physical_schema_override,
+                            physical_schema_mapping=config.physical_schema_mapping,
                             project=config.project,
                             default_catalog=self._context.default_catalog,
                             variables=variables,
diff --git a/sqlmesh/core/model/decorator.py b/sqlmesh/core/model/decorator.py
index 23eac1ad..96d38b3c 100644
--- a/sqlmesh/core/model/decorator.py
+++ b/sqlmesh/core/model/decorator.py
@@ -4,6 +4,7 @@ import logging
 import typing as t
 from pathlib import Path
 import inspect
+import re
 
 from sqlglot import exp
 from sqlglot.dialects.dialect import DialectType
@@ -82,7 +83,7 @@ class model(registry_decorator):
         jinja_macros: t.Optional[JinjaMacroRegistry] = None,
         dialect: t.Optional[str] = None,
         time_column_format: str = c.DEFAULT_TIME_COLUMN_FORMAT,
-        physical_schema_override: t.Optional[t.Dict[str, str]] = None,
+        physical_schema_mapping: t.Optional[t.Dict[re.Pattern, str]] = None,
         project: str = "",
         default_catalog: t.Optional[str] = None,
         variables: t.Optional[t.Dict[str, t.Any]] = None,
@@ -117,7 +118,7 @@ class model(registry_decorator):
             path=path,
             time_column_format=time_column_format,
             python_env=serialize_env(env, path=module_path),
-            physical_schema_override=physical_schema_override,
+            physical_schema_mapping=physical_schema_mapping,
             project=project,
             default_catalog=default_catalog,
             variables=variables,
diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py
index b69eec2a..8b7b5d7d 100644
--- a/sqlmesh/core/model/definition.py
+++ b/sqlmesh/core/model/definition.py
@@ -5,6 +5,7 @@ import json
 import logging
 import sys
 import types
+import re
 import typing as t
 from functools import cached_property
 from pathlib import Path
@@ -1555,7 +1556,7 @@ def load_sql_based_model(
     default_audits: t.List[AuditReference] = [],
     python_env: t.Optional[t.Dict[str, Executable]] = None,
     dialect: t.Optional[str] = None,
-    physical_schema_override: t.Optional[t.Dict[str, str]] = None,
+    physical_schema_mapping: t.Optional[t.Dict[re.Pattern, str]] = None,
     default_catalog: t.Optional[str] = None,
     variables: t.Optional[t.Dict[str, t.Any]] = None,
     infer_names: t.Optional[bool] = False,
@@ -1575,7 +1576,7 @@ def load_sql_based_model(
             from the macro registry.
         dialect: The default dialect if no model dialect is configured.
             The format must adhere to Python's strftime codes.
-        physical_schema_override: The physical schema override for the model.
+        physical_schema_mapping: A mapping of regular expressions to match against the model schema to produce the corresponding physical schema
         default_catalog: The default catalog if no model catalog is configured.
         variables: The variables to pass to the model.
         kwargs: Additional kwargs to pass to the loader.
@@ -1692,7 +1693,7 @@ def load_sql_based_model(
         python_env=python_env,
         jinja_macros=jinja_macros,
         jinja_macro_references=jinja_macro_references,
-        physical_schema_override=physical_schema_override,
+        physical_schema_mapping=physical_schema_mapping,
         default_catalog=default_catalog,
         variables=variables,
         used_variables=used_variables,
@@ -1749,7 +1750,7 @@ def create_sql_model(
     jinja_macros: t.Optional[JinjaMacroRegistry] = None,
     jinja_macro_references: t.Optional[t.Set[MacroReference]] = None,
     dialect: t.Optional[str] = None,
-    physical_schema_override: t.Optional[t.Dict[str, str]] = None,
+    physical_schema_mapping: t.Optional[t.Dict[re.Pattern, str]] = None,
     variables: t.Optional[t.Dict[str, t.Any]] = None,
     used_variables: t.Optional[t.Set[str]] = None,
     **kwargs: t.Any,
@@ -1773,7 +1774,7 @@ def create_sql_model(
         jinja_macros: The registry of Jinja macros.
         jinja_macro_references: The set of Jinja macros referenced by this model.
         dialect: The default dialect if no model dialect is configured.
-        physical_schema_override: The physical schema override.
+        physical_schema_mapping: A mapping of regular expressions to match against the model schema to produce the corresponding physical schema
         variables: User-defined variables.
         used_variables: The set of variable names used by this model.
     """
@@ -1812,7 +1813,7 @@ def create_sql_model(
         query=query,
         pre_statements=pre_statements,
         post_statements=post_statements,
-        physical_schema_override=physical_schema_override,
+        physical_schema_mapping=physical_schema_mapping,
         **kwargs,
     )
 
@@ -1832,7 +1833,7 @@ def create_seed_model(
     python_env: t.Optional[t.Dict[str, Executable]] = None,
     jinja_macros: t.Optional[JinjaMacroRegistry] = None,
     jinja_macro_references: t.Optional[t.Set[MacroReference]] = None,
-    physical_schema_override: t.Optional[t.Dict[str, str]] = None,
+    physical_schema_mapping: t.Optional[t.Dict[re.Pattern, str]] = None,
     variables: t.Optional[t.Dict[str, t.Any]] = None,
     used_variables: t.Optional[t.Set[str]] = None,
     **kwargs: t.Any,
@@ -1853,7 +1854,7 @@ def create_seed_model(
             from the macro registry.
         jinja_macros: The registry of Jinja macros.
         jinja_macro_references: The set of Jinja macros referenced by this model.
-        physical_schema_override: The physical schema override.
+        physical_schema_mapping: A mapping of regular expressions to match against the model schema to produce the corresponding physical schema
         variables: User-defined variables.
         used_variables: The set of variable names used by this model.
     """
@@ -1897,7 +1898,7 @@ def create_seed_model(
         jinja_macros=jinja_macros,
         pre_statements=pre_statements,
         post_statements=post_statements,
-        physical_schema_override=physical_schema_override,
+        physical_schema_mapping=physical_schema_mapping,
         **kwargs,
     )
 
@@ -1914,7 +1915,7 @@ def create_python_model(
     module_path: Path = Path(),
     time_column_format: str = c.DEFAULT_TIME_COLUMN_FORMAT,
     depends_on: t.Optional[t.Set[str]] = None,
-    physical_schema_override: t.Optional[t.Dict[str, str]] = None,
+    physical_schema_mapping: t.Optional[t.Dict[re.Pattern, str]] = None,
     variables: t.Optional[t.Dict[str, t.Any]] = None,
     **kwargs: t.Any,
 ) -> Model:
@@ -1978,7 +1979,7 @@ def create_python_model(
         entrypoint=entrypoint,
         python_env=python_env,
         jinja_macros=jinja_macros,
-        physical_schema_override=physical_schema_override,
+        physical_schema_mapping=physical_schema_mapping,
         **kwargs,
     )
 
@@ -2024,7 +2025,7 @@ def _create_model(
     jinja_macro_references: t.Optional[t.Set[MacroReference]] = None,
     depends_on: t.Optional[t.Set[str]] = None,
     dialect: t.Optional[str] = None,
-    physical_schema_override: t.Optional[t.Dict[str, str]] = None,
+    physical_schema_mapping: t.Optional[t.Dict[re.Pattern, str]] = None,
     **kwargs: t.Any,
 ) -> Model:
     _validate_model_fields(klass, {"name", *kwargs} - {"grain", "table_properties"}, path)
@@ -2034,7 +2035,15 @@ def _create_model(
     )
 
     dialect = dialect or ""
-    physical_schema_override = physical_schema_override or {}
+
+    physical_schema_mapping = physical_schema_mapping or {}
+    model_schema_name = exp.to_table(name, dialect=dialect).db
+    physical_schema_override: t.Optional[str] = None
+
+    for re_pattern, override_schema in physical_schema_mapping.items():
+        if re.match(re_pattern, model_schema_name):
+            physical_schema_override = override_schema
+            break
 
     raw_kind = kwargs.pop("kind", None)
     if raw_kind:
@@ -2050,9 +2059,7 @@ def _create_model(
                 "jinja_macros": jinja_macros or JinjaMacroRegistry(),
                 "dialect": dialect,
                 "depends_on": depends_on,
-                "physical_schema_override": physical_schema_override.get(
-                    exp.to_table(name, dialect=dialect).db
-                ),
+                "physical_schema_override": physical_schema_override,
                 **kwargs,
             },
         )
diff --git a/sqlmesh/dbt/basemodel.py b/sqlmesh/dbt/basemodel.py
index a95b25b9..bf20aeb6 100644
--- a/sqlmesh/dbt/basemodel.py
+++ b/sqlmesh/dbt/basemodel.py
@@ -310,7 +310,7 @@ class BaseModelConfig(GeneralConfig):
             "pre_statements": [d.jinja_statement(hook.sql) for hook in self.pre_hook],
             "post_statements": [d.jinja_statement(hook.sql) for hook in self.post_hook],
             "tags": self.tags,
-            "physical_schema_override": context.sqlmesh_config.physical_schema_override,
+            "physical_schema_mapping": context.sqlmesh_config.physical_schema_mapping,
             "default_catalog": context.target.database,
             **self.sqlmesh_config_kwargs,
         }
diff --git a/sqlmesh/magics.py b/sqlmesh/magics.py
index d5fb095b..839288da 100644
--- a/sqlmesh/magics.py
+++ b/sqlmesh/magics.py
@@ -188,7 +188,7 @@ class SQLMeshMagics(Magics):
                 path=model._path,
                 dialect=config.dialect,
                 time_column_format=config.time_column_format,
-                physical_schema_override=context.config.physical_schema_override,
+                physical_schema_mapping=context.config.physical_schema_mapping,
                 default_catalog=context.default_catalog,
             )
 


Test patch:
diff --git a/tests/core/test_config.py b/tests/core/test_config.py
index b1577af4..ad4d25dc 100644
--- a/tests/core/test_config.py
+++ b/tests/core/test_config.py
@@ -486,6 +486,16 @@ environment_catalog_mapping:
         )
 
 
+def test_physical_schema_mapping_mutually_exclusive_with_physical_schema_override() -> None:
+    Config(physical_schema_override={"foo": "bar"})
+    Config(physical_schema_mapping={"^foo$": "bar"})
+
+    with pytest.raises(
+        ConfigError, match=r"Only one.*physical_schema_override.*physical_schema_mapping"
+    ):
+        Config(physical_schema_override={"foo": "bar"}, physical_schema_mapping={"^foo$": "bar"})
+
+
 def test_load_feature_flag(tmp_path_factory):
     config_path = tmp_path_factory.mktemp("yaml_config") / "config.yaml"
     with open(config_path, "w", encoding="utf-8") as fd:
@@ -635,7 +645,7 @@ def test_load_model_defaults_audits(tmp_path):
             """
 model_defaults:
     dialect: ''
-    audits: 
+    audits:
         - assert_positive_order_ids
         - does_not_exceed_threshold(column := id, threshold := 1000)
         """
diff --git a/tests/core/test_context.py b/tests/core/test_context.py
index f305c2fd..22a034a9 100644
--- a/tests/core/test_context.py
+++ b/tests/core/test_context.py
@@ -1,6 +1,7 @@
 import logging
 import pathlib
 import typing as t
+import re
 from datetime import date, timedelta
 from tempfile import TemporaryDirectory
 from unittest.mock import PropertyMock, call, patch
@@ -556,12 +557,12 @@ def test_physical_schema_override(copy_to_temp_path: t.Callable) -> None:
 
     project_path = copy_to_temp_path("examples/sushi")
     no_mapping_context = Context(paths=project_path)
-    assert no_mapping_context.config.physical_schema_override == {}
+    assert no_mapping_context.config.physical_schema_mapping == {}
     assert get_schemas(no_mapping_context) == {"sqlmesh__sushi", "sqlmesh__raw"}
     assert get_view_schemas(no_mapping_context) == {"sushi", "raw"}
     no_mapping_fingerprints = get_sushi_fingerprints(no_mapping_context)
     context = Context(paths=project_path, config="map_config")
-    assert context.config.physical_schema_override == {"sushi": "company_internal"}
+    assert context.config.physical_schema_mapping == {re.compile("^sushi$"): "company_internal"}
     assert get_schemas(context) == {"company_internal", "sqlmesh__raw"}
     assert get_view_schemas(context) == {"sushi", "raw"}
     sushi_fingerprints = get_sushi_fingerprints(context)
@@ -572,6 +573,52 @@ def test_physical_schema_override(copy_to_temp_path: t.Callable) -> None:
     )
 
 
+@pytest.mark.slow
+def test_physical_schema_mapping(tmp_path: pathlib.Path) -> None:
+    create_temp_file(
+        tmp_path,
+        pathlib.Path(pathlib.Path("models"), "a.sql"),
+        "MODEL(name foo_staging.model_a); SELECT 1;",
+    )
+
+    create_temp_file(
+        tmp_path,
+        pathlib.Path(pathlib.Path("models"), "b.sql"),
+        "MODEL(name testone.model_b); SELECT 1;",
+    )
+
+    create_temp_file(
+        tmp_path,
+        pathlib.Path(pathlib.Path("models"), "c.sql"),
+        "MODEL(name untouched.model_c); SELECT 1;",
+    )
+
+    ctx = Context(
+        config=Config(
+            model_defaults=ModelDefaultsConfig(dialect="duckdb"),
+            physical_schema_mapping={
+                # anything ending with 'staging' becomes 'overridden_staging'
+                "^.*staging$": "overridden_staging",
+                # anything starting with 'test' becomes 'testing'
+                "^test.*": "testing",
+            },
+        ),
+        paths=tmp_path,
+    )
+
+    ctx.load()
+
+    physical_schemas = [snapshot.physical_schema for snapshot in sorted(ctx.snapshots.values())]
+
+    view_schemas = [
+        snapshot.qualified_view_name.schema_name for snapshot in sorted(ctx.snapshots.values())
+    ]
+
+    assert len(physical_schemas) == len(view_schemas) == 3
+    assert physical_schemas == ["overridden_staging", "testing", "sqlmesh__untouched"]
+    assert view_schemas == ["foo_staging", "testone", "untouched"]
+
+
 @pytest.mark.slow
 def test_janitor(sushi_context, mocker: MockerFixture) -> None:
     adapter_mock = mocker.MagicMock()