Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion converters/dbt/src/ossie_dbt/msi_to_osi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
24 changes: 0 additions & 24 deletions converters/dbt/tests/__snapshots__/test_msi_to_osi.ambr
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
18 changes: 0 additions & 18 deletions converters/dbt/tests/__snapshots__/test_osi_to_msi.ambr
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
16 changes: 14 additions & 2 deletions converters/dbt/tests/test_msi_to_osi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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


Expand Down
25 changes: 12 additions & 13 deletions converters/honeydew/src/honeydew_osi/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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")
Expand All @@ -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]
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]] = []
Expand All @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -1065,4 +1064,4 @@ def main() -> None:


if __name__ == "__main__":
main()
main()
46 changes: 27 additions & 19 deletions converters/honeydew/tests/test_honeydew_osi_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)


# ─────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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"]]},
Expand Down
4 changes: 1 addition & 3 deletions python/src/ossie/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions python/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
[
Expand Down