diff --git a/converters/gooddata/src/ossie_gooddata/osi_to_gooddata.py b/converters/gooddata/src/ossie_gooddata/osi_to_gooddata.py index a8dbac1c..ee1bd232 100644 --- a/converters/gooddata/src/ossie_gooddata/osi_to_gooddata.py +++ b/converters/gooddata/src/ossie_gooddata/osi_to_gooddata.py @@ -329,6 +329,12 @@ def _convert_relationship( to_ds = rel["to"] from_columns: list[str] = rel.get("from_columns", []) to_columns: list[str] = rel.get("to_columns", from_columns) + if len(from_columns) != len(to_columns): + raise ValueError( + f"Relationship '{rel.get('name', '')}': from_columns and " + f"to_columns must have the same length (got {len(from_columns)} and " + f"{len(to_columns)})" + ) target_meta = target_info.get(to_ds, {"is_date": False, "col_to_attr": {}}) sources: list[GdReferenceSource] = [] diff --git a/converters/gooddata/tests/test_osi_to_gooddata.py b/converters/gooddata/tests/test_osi_to_gooddata.py index 7848eec4..e45adccd 100644 --- a/converters/gooddata/tests/test_osi_to_gooddata.py +++ b/converters/gooddata/tests/test_osi_to_gooddata.py @@ -352,6 +352,44 @@ def test_relationships_become_references(osi_tpcds_dict: dict): assert "store" in ref_targets +def test_relationship_column_count_mismatch_raises(): + """Verify incomplete Ossie relationship pairs do not get silently truncated.""" + model = { + "semantic_model": [ + { + "name": "orders", + "datasets": [ + { + "name": "orders", + "source": "analytics.orders", + "fields": [ + _direct_field("customer_id", dimension={}), + _direct_field("region_id", dimension={}), + ], + }, + { + "name": "customers", + "source": "analytics.customers", + "fields": [_direct_field("id", dimension={})], + }, + ], + "relationships": [ + { + "name": "orders_to_customers", + "from": "orders", + "to": "customers", + "from_columns": ["customer_id", "region_id"], + "to_columns": ["id"], + } + ], + } + ] + } + + with pytest.raises(ValueError, match="from_columns.*to_columns.*same length"): + osi_to_gooddata(model) + + def test_source_column_from_ansi_sql(osi_tpcds_dict: dict): """Verify source columns are extracted from ANSI_SQL expressions.""" result = osi_to_gooddata(osi_tpcds_dict) diff --git a/converters/wisdom/src/ossie_wisdom/osi_to_wisdom.py b/converters/wisdom/src/ossie_wisdom/osi_to_wisdom.py index 0ae111fa..dc441271 100644 --- a/converters/wisdom/src/ossie_wisdom/osi_to_wisdom.py +++ b/converters/wisdom/src/ossie_wisdom/osi_to_wisdom.py @@ -366,6 +366,13 @@ def _convert_relationships( ConverterIssue(issue_type=ConverterIssueType.AI_CONTEXT_DROPPED, element_name=relationship.name) ) + if len(relationship.from_columns) != len(relationship.to_columns): + raise ValueError( + f"Relationship '{relationship.name}': from_columns and to_columns " + f"must have the same length (got {len(relationship.from_columns)} " + f"and {len(relationship.to_columns)})" + ) + left_ref = {"uuid": zsheet_refs[left]["uuid"], "name": left} right_ref = {"uuid": zsheet_refs[right]["uuid"], "name": right} conditions = [ diff --git a/converters/wisdom/tests/test_osi_to_wisdom.py b/converters/wisdom/tests/test_osi_to_wisdom.py index 1ee17173..233ab2d9 100644 --- a/converters/wisdom/tests/test_osi_to_wisdom.py +++ b/converters/wisdom/tests/test_osi_to_wisdom.py @@ -209,6 +209,32 @@ def test_one_to_one_note_restores_relationship_type(): assert edges[0]["properties"]["relationshipType"] == "ONE_TO_ONE" +def test_relationship_column_count_mismatch_raises(): + document = OSIDocument( + semantic_model=[ + OSISemanticModel( + name="m", + datasets=[ + OSIDataset(name="orders", source="db.s.orders"), + OSIDataset(name="customers", source="db.s.customers"), + ], + relationships=[ + OSIRelationship( + name="orders_to_customers", + from_dataset="orders", + to="customers", + from_columns=["customer_id", "region_id"], + to_columns=["id"], + ) + ], + ) + ] + ) + + with pytest.raises(ValueError, match="from_columns.*to_columns.*same length"): + OSIToWisdomConverter().convert(document, exported_at="2026-07-10T00:00:00+00:00") + + def test_unresolved_metric_attaches_to_first_dataset(): from ossie import OSIMetric