From fdc74e8705685866c5cdfc9d20b5a64196112875 Mon Sep 17 00:00:00 2001 From: sophiascaglioni-whoop Date: Fri, 9 Oct 2026 11:12:28 -0400 Subject: [PATCH] [SST-258] Add per-view target database/schema override Parse optional database and schema fields from semantic view YAML definitions. When present, these override the global --db/--schema for that specific view's CREATE SEMANTIC VIEW statement. The override flows through all three code paths that build view config dicts: the YAML parser, the manifest-based generation path, and the Snowflake-based generation path. Both sequential and parallel generation apply the per-view override. Views without these fields use the global default (backward compatible). Note: sst drop --prune only checks the default location; multi-location prune support is tracked as a follow-up. Closes #258 --- .../core/parsing/parsers/semantic_parser.py | 2 ++ .../services/generate_semantic_views.py | 17 +++++++++-- tests/unit/core/test_view_scope.py | 28 +++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/snowflake_semantic_tools/core/parsing/parsers/semantic_parser.py b/snowflake_semantic_tools/core/parsing/parsers/semantic_parser.py index bcf1a22c..fee673d7 100644 --- a/snowflake_semantic_tools/core/parsing/parsers/semantic_parser.py +++ b/snowflake_semantic_tools/core/parsing/parsers/semantic_parser.py @@ -578,6 +578,8 @@ def parse_semantic_views( "tables": tables_json, "custom_instructions": custom_instructions_json, "source_file": str(file_path), + "database": view_def.get("database"), + "schema": view_def.get("schema"), **scope_fields, } diff --git a/snowflake_semantic_tools/services/generate_semantic_views.py b/snowflake_semantic_tools/services/generate_semantic_views.py index ec9ba2ec..42668c29 100644 --- a/snowflake_semantic_tools/services/generate_semantic_views.py +++ b/snowflake_semantic_tools/services/generate_semantic_views.py @@ -363,6 +363,12 @@ def execute( description = view_config.get("description", "") custom_instruction_names = view_config.get("custom_instructions", []) + # Per-view target database/schema override + view_db = view_config.get("database") + view_schema = view_config.get("schema") + self.builder.target_database = view_db.upper() if view_db else generate_config.target_database + self.builder.target_schema = view_schema.upper() if view_schema else generate_config.target_schema + # Construct view scope from include/exclude lists view_scope = {} for scope_key in ( @@ -603,8 +609,11 @@ def _generate_one(view_config: Dict[str, Any], idx: int) -> Dict[str, Any]: cm = pool.checkout() try: builder = SemanticViewBuilder(config=self.config, snowflake_loader=cm) - builder.target_database = generate_config.target_database - builder.target_schema = generate_config.target_schema + # Per-view target database/schema override + view_db = view_config.get("database") + view_schema = view_config.get("schema") + builder.target_database = view_db.upper() if view_db else generate_config.target_database + builder.target_schema = view_schema.upper() if view_schema else generate_config.target_schema builder.metadata_database = generate_config.metadata_database builder.metadata_schema = generate_config.metadata_schema builder.store = self.builder.store @@ -811,6 +820,8 @@ def _query_available_views(self) -> List[Dict[str, Any]]: "tables": tables, "description": row_dict.get("DESCRIPTION") or "", "custom_instructions": custom_instruction_names, + "database": row_dict.get("DATABASE"), + "schema": row_dict.get("SCHEMA"), } # Extract scope fields if present in the table @@ -988,6 +999,8 @@ def _parse_view_configs(self, available_views: List[Dict]) -> List[Dict]: "tables": tables if isinstance(tables, list) else [], "description": view.get("DESCRIPTION", ""), "custom_instructions": custom_instructions, + "database": view.get("DATABASE"), + "schema": view.get("SCHEMA"), } # Pass through view scope fields if present diff --git a/tests/unit/core/test_view_scope.py b/tests/unit/core/test_view_scope.py index ef060f8e..65b3b40d 100644 --- a/tests/unit/core/test_view_scope.py +++ b/tests/unit/core/test_view_scope.py @@ -161,6 +161,34 @@ def test_no_scope_fields_not_in_record(self): assert "columns" not in result[0] assert "relationships" not in result[0] + def test_database_and_schema_parsed(self): + """Per-view database and schema override are included in the record.""" + views = [ + { + "name": "security_view", + "tables": ["incidents"], + "database": "SECURITY_DB", + "schema": "SEMANTIC_VIEWS", + } + ] + result = parse_semantic_views(views, Path("test.yml")) + assert result[0]["database"] == "SECURITY_DB" + assert result[0]["schema"] == "SEMANTIC_VIEWS" + + def test_database_only_override(self): + """Only database can be overridden; schema defaults to None.""" + views = [{"name": "test_view", "tables": ["orders"], "database": "OTHER_DB"}] + result = parse_semantic_views(views, Path("test.yml")) + assert result[0]["database"] == "OTHER_DB" + assert result[0]["schema"] is None + + def test_no_database_schema_defaults_to_none(self): + """When no database/schema specified, both default to None.""" + views = [{"name": "simple_view", "tables": ["orders"]}] + result = parse_semantic_views(views, Path("test.yml")) + assert result[0]["database"] is None + assert result[0]["schema"] is None + # ===== Builder scope tests =====