diff --git a/converters/dbt/src/ossie_dbt/msi_to_osi.py b/converters/dbt/src/ossie_dbt/msi_to_osi.py index db9e1315..30b7d06d 100644 --- a/converters/dbt/src/ossie_dbt/msi_to_osi.py +++ b/converters/dbt/src/ossie_dbt/msi_to_osi.py @@ -122,7 +122,6 @@ def convert( return ConverterResult( output=OSIDocument( version="0.2.0.dev0", - dialects=[self._dialect], semantic_model=[ OSISemanticModel( name=osi_model_name, diff --git a/converters/dbt/tests/__snapshots__/test_msi_to_osi.ambr b/converters/dbt/tests/__snapshots__/test_msi_to_osi.ambr index facd135e..d88444ad 100644 --- a/converters/dbt/tests/__snapshots__/test_msi_to_osi.ambr +++ b/converters/dbt/tests/__snapshots__/test_msi_to_osi.ambr @@ -1,25 +1,7 @@ # serializer version: 1 -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. # name: TestMetricConversion.test_derived_metric_nested ''' version: 0.2.0.dev0 - dialects: - - ANSI_SQL semantic_model: - name: semantic_model datasets: @@ -73,8 +55,6 @@ # name: TestMetricConversion.test_ratio_metric_inlines_sub_expressions ''' version: 0.2.0.dev0 - dialects: - - ANSI_SQL semantic_model: - name: semantic_model datasets: @@ -114,8 +94,6 @@ # name: TestMetricFilterFlattening.test_metric_and_measure_filters_combined_with_and ''' version: 0.2.0.dev0 - dialects: - - ANSI_SQL semantic_model: - name: semantic_model datasets: @@ -140,8 +118,6 @@ # name: TestRelationshipConversion.test_three_datasets_produce_all_pairs ''' version: 0.2.0.dev0 - dialects: - - ANSI_SQL semantic_model: - name: semantic_model datasets: diff --git a/converters/dbt/tests/__snapshots__/test_osi_to_msi.ambr b/converters/dbt/tests/__snapshots__/test_osi_to_msi.ambr index e886e0b4..08ac9e4a 100644 --- a/converters/dbt/tests/__snapshots__/test_osi_to_msi.ambr +++ b/converters/dbt/tests/__snapshots__/test_osi_to_msi.ambr @@ -1,25 +1,7 @@ # serializer version: 1 -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. # name: TestOSIToMSIRoundTrip.test_osi_to_msi_to_osi_preserves_structure ''' version: 0.2.0.dev0 - dialects: - - ANSI_SQL semantic_model: - name: semantic_model datasets: diff --git a/converters/dbt/tests/test_msi_to_osi.py b/converters/dbt/tests/test_msi_to_osi.py index a0aa9f8a..9284ec4d 100644 --- a/converters/dbt/tests/test_msi_to_osi.py +++ b/converters/dbt/tests/test_msi_to_osi.py @@ -19,6 +19,7 @@ from typing import List, Optional import pytest +import yaml from syrupy.assertion import SnapshotAssertion from ossie_dbt.converter_issues import ConverterIssueType @@ -126,6 +127,19 @@ def test_multiple_semantic_models_become_multiple_datasets(self) -> None: names = [ds.name for ds in result.semantic_model[0].datasets] assert names == ["orders", "users"] + def test_serialized_output_omits_schema_invalid_root_advertisement_fields(self) -> None: + sm = semantic_model_with_guaranteed_meta( + name="orders", + dimensions=[_dimension("status")], + ) + result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm])).output + + data = yaml.safe_load(result.to_osi_yaml()) + + assert "dialects" not in data + assert "vendors" not in data + assert _fields(result)[0].expression.dialects[0].dialect == OSIDialect.ANSI_SQL + class TestDimensionConversion: def test_categorical_dimension_has_is_time_false(self) -> None: @@ -302,7 +316,6 @@ def test_default_dialect_is_ansi_sql(self) -> None: ) result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm])).output - assert result.dialects == [OSIDialect.ANSI_SQL] assert _fields(result)[0].expression.dialects[0].dialect == OSIDialect.ANSI_SQL def test_configurable_dialect(self) -> None: @@ -312,7 +325,6 @@ def test_configurable_dialect(self) -> None: ) result = MSIToOSIConverter(dialect=OSIDialect.SNOWFLAKE).convert(_manifest(semantic_models=[sm])).output - assert result.dialects == [OSIDialect.SNOWFLAKE] assert _fields(result)[0].expression.dialects[0].dialect == OSIDialect.SNOWFLAKE diff --git a/converters/honeydew/src/honeydew_osi/converter.py b/converters/honeydew/src/honeydew_osi/converter.py index e3f78f4f..1e55cf6a 100644 --- a/converters/honeydew/src/honeydew_osi/converter.py +++ b/converters/honeydew/src/honeydew_osi/converter.py @@ -88,6 +88,13 @@ def convert_osi_to_honeydew(osi_yaml_str: str) -> dict[str, str]: if not isinstance(root, dict): raise HoneydewConversionError("Invalid OSI YAML: expected a mapping at the root") + extra_root_fields = sorted(set(root) - {"version", "semantic_model"}) + if extra_root_fields: + raise HoneydewConversionError( + "Invalid OSI YAML: unsupported root field(s): " + + ", ".join(extra_root_fields) + ) + version_str = str(root.get("version", "")) if version_str != SUPPORTED_OSI_VERSION: raise HoneydewConversionError( @@ -104,11 +111,10 @@ def convert_osi_to_honeydew(osi_yaml_str: str) -> dict[str, str]: "only the first will be converted" ) - vendors = [v for v in (root.get("vendors") or []) if v != HONEYDEW_VENDOR] - return _model_to_files(semantic_models[0], extra_vendors=vendors) + return _model_to_files(semantic_models[0]) -def _model_to_files(sm: dict[str, Any], *, extra_vendors: list[str] | None = None) -> dict[str, str]: +def _model_to_files(sm: dict[str, Any]) -> dict[str, str]: name = sm.get("name") if not name: raise HoneydewConversionError("Missing 'name' in semantic model") @@ -119,13 +125,12 @@ def _model_to_files(sm: dict[str, Any], *, extra_vendors: list[str] | None = Non if sm.get("description"): workspace["description"] = sm["description"] - # Preserve model-level ai_context, non-HONEYDEW custom_extensions, and extra vendors + # Preserve model-level ai_context and non-HONEYDEW custom_extensions. model_ai_ctx = sm.get("ai_context") model_ext = [e for e in (sm.get("custom_extensions") or []) if e.get("vendor_name") != HONEYDEW_VENDOR] ws_meta = _build_osi_metadata( ai_context=model_ai_ctx, custom_extensions=model_ext or None, - extra_vendors=extra_vendors or None, ) if ws_meta: workspace["metadata"] = [ws_meta] @@ -602,11 +607,8 @@ def convert_honeydew_to_osi(workspace_dir: str) -> str: if osi_metrics: sm["metrics"] = osi_metrics - extra_vendors = ws_osi_meta.get("vendors") or [] - vendors = [HONEYDEW_VENDOR] + [v for v in extra_vendors if v != HONEYDEW_VENDOR] root: dict[str, Any] = { "version": SUPPORTED_OSI_VERSION, - "vendors": vendors, "semantic_model": [sm], } return _dump(root) @@ -930,7 +932,6 @@ def _build_osi_metadata( label: str | None = None, unique_keys: Any = None, custom_extensions: list | None = None, - extra_vendors: list[str] | None = None, ) -> dict[str, Any] | None: """Build a Honeydew metadata entry that stores OSI-only fields for round-tripping.""" items: list[dict[str, Any]] = [] @@ -944,8 +945,6 @@ def _build_osi_metadata( items.append({"name": "unique_keys", "value": json.dumps(unique_keys)}) if custom_extensions: items.append({"name": "custom_extensions", "value": json.dumps(custom_extensions)}) - if extra_vendors: - items.append({"name": "vendors", "value": json.dumps(extra_vendors)}) if not items: return None @@ -968,7 +967,7 @@ def _read_osi_metadata(obj: dict[str, Any]) -> dict[str, Any]: result[key] = raw elif key == "label": result[key] = raw - elif key in ("unique_keys", "custom_extensions", "vendors"): + elif key in ("unique_keys", "custom_extensions"): try: result[key] = json.loads(raw) except (json.JSONDecodeError, TypeError): @@ -1065,4 +1064,4 @@ def main() -> None: if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/converters/honeydew/tests/test_honeydew_osi_converter.py b/converters/honeydew/tests/test_honeydew_osi_converter.py index c3aaa75c..f89a62ff 100644 --- a/converters/honeydew/tests/test_honeydew_osi_converter.py +++ b/converters/honeydew/tests/test_honeydew_osi_converter.py @@ -608,13 +608,29 @@ def test_osi_to_honeydew_multiple_models_warns(): # ───────────────────────────────────────────────────────────────────────────── def _hd_root(sm): - return {"version": OSI_VERSION, "vendors": ["HONEYDEW"], "semantic_model": [sm]} + return {"version": OSI_VERSION, "semantic_model": [sm]} def _ansi(expr): return {"dialects": [{"dialect": "ANSI_SQL", "expression": expr}]} +def test_honeydew_to_osi_omits_schema_invalid_root_advertisement_fields(tmp_path): + _write_workspace(str(tmp_path), "ws", [{ + "name": "orders", + "keys": ["id"], + "key_dataset": "orders", + "sql": "db.s.orders", + "dataset_attrs": [], + }]) + + result = yaml.safe_load(convert_honeydew_to_osi(str(tmp_path))) + + assert "vendors" not in result + assert "dialects" not in result + assert result["semantic_model"][0]["datasets"][0]["name"] == "orders" + + @pytest.mark.parametrize("ws_name,entities,expected_root", [ # ── basic entity with two dataset attributes ────────────────────────────── pytest.param( @@ -800,8 +816,7 @@ def test_honeydew_to_osi_missing_workspace_raises(tmp_path): def test_honeydew_to_osi_missing_schema_dir_empty_model(tmp_path): (tmp_path / "workspace.yml").write_text(yaml.dump({"type": "workspace", "name": "ws"})) result = yaml.safe_load(convert_honeydew_to_osi(str(tmp_path))) - assert result == {"version": OSI_VERSION, "vendors": ["HONEYDEW"], - "semantic_model": [{"name": "ws", "datasets": []}]} + assert result == {"version": OSI_VERSION, "semantic_model": [{"name": "ws", "datasets": []}]} def test_honeydew_to_osi_empty_metric_sql_skipped(tmp_path): @@ -1405,28 +1420,22 @@ def test_connectionless_relation_warns(): # ───────────────────────────────────────────────────────────────────────────── -# Vendors round-trip +# Root advertisement fields are schema-invalid # ───────────────────────────────────────────────────────────────────────────── -@pytest.mark.parametrize("input_vendors,expected_vendors", [ - (["SNOWFLAKE", "HONEYDEW"], ["HONEYDEW", "SNOWFLAKE"]), - (["SNOWFLAKE"], ["HONEYDEW", "SNOWFLAKE"]), - (["HONEYDEW"], ["HONEYDEW"]), +@pytest.mark.parametrize("field,value", [ + ("vendors", ["SNOWFLAKE", "HONEYDEW"]), + ("dialects", ["ANSI_SQL"]), ]) -def test_vendors_roundtrip(tmp_path, input_vendors, expected_vendors): +def test_osi_to_honeydew_rejects_root_advertisement_fields(field, value): doc = yaml.dump({ "version": OSI_VERSION, - "vendors": input_vendors, + field: value, "semantic_model": [{"name": "m", "datasets": []}], }) - files = convert_osi_to_honeydew(doc) - for rel_path, content in files.items(): - p = tmp_path / rel_path - p.parent.mkdir(parents=True, exist_ok=True) - p.write_text(content) - result = yaml.safe_load(convert_honeydew_to_osi(str(tmp_path))) - assert result == {"version": OSI_VERSION, "vendors": expected_vendors, - "semantic_model": [{"name": "m", "datasets": []}]} + + with pytest.raises(HoneydewConversionError, match=field): + convert_osi_to_honeydew(doc) # ───────────────────────────────────────────────────────────────────────────── @@ -1469,7 +1478,6 @@ def test_main_honeydew_to_osi(tmp_path): assert result.returncode == 0 assert yaml.safe_load(output_file.read_text()) == { "version": OSI_VERSION, - "vendors": ["HONEYDEW"], "semantic_model": [{"name": "ws", "datasets": [ {"name": "orders", "source": "DB.S.ORDERS", "primary_key": ["id"], "unique_keys": [["id"]]}, diff --git a/python/src/ossie/models.py b/python/src/ossie/models.py index 5406a743..87d56c34 100644 --- a/python/src/ossie/models.py +++ b/python/src/ossie/models.py @@ -206,11 +206,9 @@ class OSISemanticModel(BaseModel): class OSIDocument(BaseModel): """Root Ossie document.""" - model_config = ConfigDict(frozen=True) + model_config = ConfigDict(frozen=True, extra="forbid") version: str = "0.2.0.dev0" - dialects: Optional[list[OSIDialect]] = None - vendors: Optional[list[OSIVendor]] = None semantic_model: list[OSISemanticModel] def to_osi_yaml(self, **kwargs: Any) -> str: diff --git a/python/tests/test_models.py b/python/tests/test_models.py index 749a226c..9ac2d1e6 100644 --- a/python/tests/test_models.py +++ b/python/tests/test_models.py @@ -111,6 +111,14 @@ def test_invalid_datatype_is_rejected() -> None: OSIDocument.model_validate(document) +def test_root_dialects_field_is_rejected() -> None: + document = _document() + document["dialects"] = ["ANSI_SQL"] + + with pytest.raises(ValidationError): + OSIDocument.model_validate(document) + + @pytest.mark.parametrize( ("dimension", "datatype", "expected"), [