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
11 changes: 9 additions & 2 deletions converters/databricks/src/ossie_databricks/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,9 +261,16 @@ def validate_source(source, dataset_name):
Accepts a 3-part `catalog.schema.table` identifier or a `SELECT`/`WITH`
subquery. Raises ConversionError otherwise.
"""
if not source or not str(source).strip():
if not source:
raise ConversionError(f"Dataset '{dataset_name}': missing/empty 'source'")
s = str(source).strip()
if not isinstance(source, str):
kind = source.get("kind") if isinstance(source, dict) else type(source).__name__
raise ConversionError(
f"Dataset '{dataset_name}': structured source kind {kind!r} is not supported by the Databricks converter"
)
if not source.strip():
raise ConversionError(f"Dataset '{dataset_name}': missing/empty 'source'")
s = source.strip()
# A SELECT/WITH subquery source. `\b` after the keyword matches `WITH(...)` (no
# space) too, but not an identifier like `WITHHELD`.
if re.match(r"(?i)(select|with)\b", s):
Expand Down
7 changes: 7 additions & 0 deletions converters/databricks/tests/test_ossie_to_metric_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@
from _util import canon, load_fixture, parse


def test_structured_dataset_source_is_rejected():
source = {"kind": "file", "format": "parquet", "locations": ["s3://bucket/orders.parquet"]}

with pytest.raises(ConversionError, match="structured source kind.*file.*not supported"):
exporter.validate_source(source, "orders")


def test_fixtureA_export_matches_expected():
out = exporter.convert_ossie_to_metric_view(load_fixture("fixtureA_ossie.yaml"))
assert parse(out) == parse(load_fixture("fixtureA_metric_view.yaml"))
Expand Down
10 changes: 8 additions & 2 deletions converters/dbt/src/ossie_dbt/osi_to_msi.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
OSIExpression,
OSIField,
OSISemanticModel,
OSISource,
)
from ossie_dbt.converter_issues import ConverterResult
from ossie_dbt.expression_utils import (
Expand Down Expand Up @@ -395,8 +396,13 @@ def _get_expression(self, osi_expr: OSIExpression) -> str:
return osi_expr.dialects[0].expression if osi_expr.dialects else ""

@staticmethod
def _parse_source(source: str) -> PydanticNodeRelation:
"""Parse `schema.table` or `db.schema.table` into a PydanticNodeRelation."""
def _parse_source(source: OSISource) -> PydanticNodeRelation:
"""Parse a legacy string source into a PydanticNodeRelation."""
if not isinstance(source, str):
kind = getattr(source, "kind", type(source).__name__)
raise TypeError(
f"Structured dataset source kind {kind!r} is not supported by the dbt converter"
)
parts = source.split(".")
if len(parts) >= 3:
database, schema, alias = parts[0], parts[1], ".".join(parts[2:])
Expand Down
9 changes: 8 additions & 1 deletion converters/dbt/tests/test_osi_to_msi.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import pytest
from syrupy.assertion import SnapshotAssertion

from ossie import OSIDataType, OSIDimension
from ossie import OSIDataType, OSIDimension, OSIFileSource
from ossie_dbt.msi_to_osi import MSIToOSIConverter
from ossie_dbt.osi_to_msi import OSIToMSIConverter
from metricflow_semantic_interfaces.type_enums import (
Expand All @@ -37,6 +37,13 @@
)


def test_structured_dataset_source_is_rejected() -> None:
source = OSIFileSource(kind="file", format="parquet", locations=["s3://bucket/orders.parquet"])

with pytest.raises(TypeError, match="Structured dataset source kind.*file.*not supported"):
OSIToMSIConverter._parse_source(source)


class TestOSIToMSIBasicConversion:
def test_empty_document_produces_empty_manifest(self) -> None:
result = OSIToMSIConverter().convert(_osi_doc()).output
Expand Down
9 changes: 7 additions & 2 deletions converters/gooddata/src/ossie_gooddata/osi_to_gooddata.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,13 @@ def _is_multivalue(rel: dict[str, Any]) -> bool:
return bool(gd_ext and gd_ext.get("multivalue"))


def _parse_source_to_table_id(source: str, data_source_id: str) -> GdDataSourceTableId:
"""Parse an Ossie source string into a GoodData DataSourceTableId."""
def _parse_source_to_table_id(source: object, data_source_id: str) -> GdDataSourceTableId:
"""Parse a legacy Ossie string source into a GoodData DataSourceTableId."""
if not isinstance(source, str):
kind = source.get("kind") if isinstance(source, dict) else type(source).__name__
raise TypeError(
f"Structured dataset source kind {kind!r} is not supported by the GoodData converter"
)
parts = source.split(".")
if len(parts) >= 3:
# source_id.schema.table or more
Expand Down
8 changes: 8 additions & 0 deletions converters/gooddata/tests/test_osi_to_gooddata.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,18 @@
from ossie_gooddata.osi_to_gooddata import (
_convert_to_attribute,
_convert_to_fact,
_parse_source_to_table_id,
osi_to_gooddata,
)


def test_structured_dataset_source_is_rejected() -> None:
source = {"kind": "file", "format": "parquet", "locations": ["s3://bucket/orders.parquet"]}

with pytest.raises(TypeError, match="Structured dataset source kind.*file.*not supported"):
_parse_source_to_table_id(source, "default")


def _field_with_extension(datatype: str | None, extension_type: str) -> dict:
field = {
"name": "value",
Expand Down
4 changes: 4 additions & 0 deletions converters/gsf/src/ossie_gsf/native_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1923,6 +1923,10 @@ def _parse_source(
default_database: str | None,
) -> dict[str, str | None]:
if isinstance(source, dict):
if "kind" in source:
raise GSFConversionError(
f"Structured dataset source kind {source.get('kind')!r} is not supported by the NVIDIA GSF converter"
)
database = source.get("database") or default_database
schema = source.get("schema")
table = source.get("table")
Expand Down
7 changes: 7 additions & 0 deletions converters/gsf/tests/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@
SCHEMA = Path(__file__).resolve().parents[3] / "core-spec" / "osi-schema.json"


def test_structured_dataset_source_is_rejected() -> None:
source = {"kind": "file", "format": "parquet", "locations": ["s3://bucket/orders.parquet"]}

with pytest.raises(GSFConversionError, match="Structured dataset source kind.*file.*not supported"):
_parse_source(source, "analytics")


def _ossie_yaml() -> str:
return (FIXTURES / "sales.ossie.yaml").read_text(encoding="utf-8")

Expand Down
7 changes: 6 additions & 1 deletion converters/honeydew/src/honeydew_osi/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,12 @@ def _is_simple_identifier(expr: str) -> bool:
return bool(re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", expr.strip()))


def _parse_osi_source(source: str) -> tuple[str, str]:
def _parse_osi_source(source: object) -> tuple[str, str]:
if not isinstance(source, str):
kind = source.get("kind") if isinstance(source, dict) else type(source).__name__
raise HoneydewConversionError(
f"Structured dataset source kind {kind!r} is not supported by the Honeydew converter"
)
source = (source or "").strip()
if not source:
return ("", "table")
Expand Down
7 changes: 7 additions & 0 deletions converters/honeydew/tests/test_honeydew_osi_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@
OSI_VERSION = "0.2.0.dev0"


def test_structured_dataset_source_is_rejected() -> None:
source = {"kind": "file", "format": "parquet", "locations": ["s3://bucket/orders.parquet"]}

with pytest.raises(HoneydewConversionError, match="Structured dataset source kind.*file.*not supported"):
_parse_osi_source(source)


def _osi(model_dict):
return yaml.dump(
{"version": OSI_VERSION, "semantic_model": [model_dict]},
Expand Down
11 changes: 9 additions & 2 deletions converters/omni/src/osi_omni/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,9 +328,16 @@ def parse_source(source, dataset_name):
(parts may be double-quoted: `"Omni Views".channel_info`).
Omni views require a `schema`, so a bare 1-part table name is rejected.
"""
if not source or not str(source).strip():
if not source:
raise ConversionError(f"Dataset '{dataset_name}': missing/empty 'source'")
s = str(source).strip()
if not isinstance(source, str):
kind = source.get("kind") if isinstance(source, dict) else type(source).__name__
raise ConversionError(
f"Dataset '{dataset_name}': structured source kind {kind!r} is not supported by the Omni converter"
)
if not source.strip():
raise ConversionError(f"Dataset '{dataset_name}': missing/empty 'source'")
s = source.strip()
if re.match(r"(?i)(select|with)\b", s):
return ("sql", s)
if not _SOURCE_PARTS_RE.match(s):
Expand Down
9 changes: 8 additions & 1 deletion converters/omni/tests/test_osi_to_omni.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,17 @@
import pytest

from osi_omni import ConversionError, convert_osi_to_omni
from osi_omni._common import dump_yaml
from osi_omni._common import dump_yaml, parse_source
from _util import REPO_ROOT, load_fixture, load_fixture_dir, parse, parse_files


def test_structured_dataset_source_is_rejected():
source = {"kind": "file", "format": "parquet", "locations": ["s3://bucket/orders.parquet"]}

with pytest.raises(ConversionError, match="structured source kind.*file.*not supported"):
parse_source(source, "orders")


def export(osi_yaml, **kwargs):
with warnings.catch_warnings():
warnings.simplefilter("ignore")
Expand Down
10 changes: 8 additions & 2 deletions converters/orionbelt/src/ossie_orionbelt/osi_to_obml.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,14 @@ def _carry_foreign_extensions(osi_exts: list[dict] | None, obml_target: dict[str
{"vendor": vendor, "data": ext.get("data", "")}
)

def _parse_source(self, source: str) -> tuple[str, str, str]:
"""Parse 'database.schema.table' into parts."""
def _parse_source(self, source: object) -> tuple[str, str, str]:
"""Parse a legacy string source into database/schema/table parts."""
if not isinstance(source, str):
kind = source.get("kind") if isinstance(source, dict) else type(source).__name__
raise TypeError(
f"Structured dataset source kind {kind!r} is not supported "
"by the OrionBelt converter"
)
parts = source.split(".")
if len(parts) == 3:
return parts[0], parts[1], parts[2]
Expand Down
8 changes: 8 additions & 0 deletions converters/orionbelt/tests/test_osi_v02_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@
# ---------------------------------------------------------------------------


def test_structured_dataset_source_is_rejected() -> None:
converter = conv.OSItoOBML({"version": "0.2.0.dev0", "semantic_model": []})
source = {"kind": "file", "format": "parquet", "locations": ["s3://bucket/orders.parquet"]}

with pytest.raises(TypeError, match="Structured dataset source kind.*file.*not supported"):
converter._parse_source(source)


@pytest.fixture(scope="module")
def schema_validator() -> Any:
"""Draft 2020-12 validator pinned to the resolved OSI v0.2 core schema
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,13 @@ private SemanticModel parseSemanticModel(Map<String, Object> map) {
private Dataset parseDataset(Map<String, Object> map) {
Dataset ds = new Dataset();
ds.setName((String) map.get("name"));
ds.setSource((String) map.get("source"));
Object source = map.get("source");
if (source != null && !(source instanceof String)) {
Object kind = source instanceof Map<?, ?> sourceMap ? sourceMap.get("kind") : source.getClass().getSimpleName();
throw new IllegalArgumentException(
"Structured dataset source kind '" + kind + "' is not supported by the Polaris converter");
}
ds.setSource((String) source);
ds.setDescription((String) map.get("description"));

List<String> pk = (List<String>) map.get("primary_key");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,27 @@ void testParseMinimalModel() {
assertEquals(1, sm.getMetrics().size());
}

@Test
void testStructuredDatasetSourceIsRejected() {
String structuredSourceModel =
"version: \"0.2.0.dev0\"\n"
+ "semantic_model:\n"
+ " - name: test_model\n"
+ " datasets:\n"
+ " - name: orders\n"
+ " source:\n"
+ " kind: file\n"
+ " format: parquet\n"
+ " locations: [s3://bucket/orders.parquet]\n";

IllegalArgumentException error = assertThrows(
IllegalArgumentException.class,
() -> new OsiModelParser().parse(
new ByteArrayInputStream(structuredSourceModel.getBytes(StandardCharsets.UTF_8))));

assertTrue(error.getMessage().contains("Structured dataset source kind 'file' is not supported"));
}

@Test
void testParseDatasetFields() {
OsiModelParser parser = new OsiModelParser();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public enum Level {
public static final String API_NAME = "apiName";
public static final String LABEL = "label";
public static final String DESCRIPTION = "description";
public static final String SOURCE = "source";
public static final String DATA_TYPE = "dataType";
public static final String OSI_DATATYPE = "datatype";
public static final String AI_CONTEXT = "ai_context";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,21 @@ public void execute(Map<String, Object> sourceData, Map<String, Object> outputDa
private void mapOsiToSalesforce(
Map<String, Object> sourceData, Map<String, Object> outputData, Map<String, String> mappings) {

List<Object> osiDatasets = getList(sourceData, DATASETS);
if (osiDatasets != null) {
streamMaps(osiDatasets).forEach(dataset -> {
Object source = dataset.get(SOURCE);
if (source != null && !(source instanceof String)) {
Object kind = source instanceof Map<?, ?> sourceMap
? sourceMap.get("kind")
: source.getClass().getSimpleName();
throw new org.apache.ossie.exception.ConversionException(
"Structured dataset source kind '" + kind
+ "' is not supported by the Salesforce converter");
}
});
}

Map<String, String> datasetMappings = MappingUtils.filterMappingsByPrefix(mappings, DATASETS);

var mappedData = GenericMappingEngine.applyMappings(sourceData, datasetMappings);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.apache.ossie.converter.ConverterFactory;
import org.apache.ossie.converter.ConversionDirection;
import org.apache.ossie.converter.CustomExtensionHandler;
import org.apache.ossie.exception.ConversionException;
import org.apache.ossie.validator.SchemaValidator;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -94,6 +95,23 @@ void setUp() throws IOException {
osiYaml = osiYamlAnsiSql;
}

@Test
void testStructuredDatasetSourceIsRejected() {
String structuredSourceModel =
"version: \"0.2.0.dev0\"\n"
+ "semantic_model:\n"
+ " - name: test_model\n"
+ " datasets:\n"
+ " - name: orders\n"
+ " source:\n"
+ " kind: file\n"
+ " format: parquet\n"
+ " locations: [s3://bucket/orders.parquet]\n";

ConversionException error = assertThrows(ConversionException.class, () -> converter.convert(structuredSourceModel));
assertTrue(error.getMessage().contains("Structured dataset source kind 'file' is not supported"));
}

@Test
void testCompleteConversion() throws Exception {
List<String> results = converter.convert(osiYaml);
Expand Down
7 changes: 6 additions & 1 deletion converters/snowflake/src/ossie_snowflake/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,8 +446,13 @@ def _parse_source(source):
"""
if not source:
return None
if not isinstance(source, str):
kind = source.get("kind") if isinstance(source, dict) else type(source).__name__
raise OsiConversionError(
f"Structured dataset source kind {kind!r} is not supported by the Snowflake converter"
)

source_stripped = str(source).strip()
source_stripped = source.strip()
if not source_stripped:
return None

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@
)


def test_structured_dataset_source_is_rejected():
source = {"kind": "file", "format": "parquet", "locations": ["s3://bucket/orders.parquet"]}

with pytest.raises(OsiConversionError, match="Structured dataset source kind.*file.*not supported"):
_parse_source(source)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
Expand Down
8 changes: 7 additions & 1 deletion converters/wisdom/src/ossie_wisdom/osi_to_wisdom.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
OSIDocument,
OSIExpression,
OSISemanticModel,
OSISource,
)
from ossie_wisdom.converter_issues import ConverterIssue, ConverterIssueType, ConverterResult

Expand Down Expand Up @@ -270,7 +271,12 @@ def _ai_context_text(self, ai_context, element_name: str, issues: List[Converter
return ai_context.instructions or ""
return ai_context

def _split_source(self, source: str) -> Tuple[str, str, str]:
def _split_source(self, source: OSISource) -> tuple[str, str, str]:
if not isinstance(source, str):
kind = getattr(source, "kind", type(source).__name__)
raise TypeError(
f"Structured dataset source kind {kind!r} is not supported by the Wisdom converter"
)
parts = source.split(".")
if len(parts) >= 3:
return parts[0], parts[1], ".".join(parts[2:])
Expand Down
Loading