diff --git a/converters/sigma/src/ossie_sigma/converter_issues.py b/converters/sigma/src/ossie_sigma/converter_issues.py index 7ac34d49..46fdfb2c 100644 --- a/converters/sigma/src/ossie_sigma/converter_issues.py +++ b/converters/sigma/src/ossie_sigma/converter_issues.py @@ -26,6 +26,7 @@ class ConverterIssueType(Enum): UNSUPPORTED_ELEMENT_KIND = "UNSUPPORTED_ELEMENT_KIND" EXPRESSION_NOT_TRANSLATABLE = "EXPRESSION_NOT_TRANSLATABLE" RELATIONSHIP_COLUMN_UNRESOLVED = "RELATIONSHIP_COLUMN_UNRESOLVED" + RELATIONSHIP_COLUMN_ARITY_MISMATCH = "RELATIONSHIP_COLUMN_ARITY_MISMATCH" UNIQUE_KEY_COLUMN_UNRESOLVED = "UNIQUE_KEY_COLUMN_UNRESOLVED" DERIVED_ELEMENT_NOT_MODELED = "DERIVED_ELEMENT_NOT_MODELED" FILTER_NOT_MODELED = "FILTER_NOT_MODELED" diff --git a/converters/sigma/src/ossie_sigma/ossie_to_sigma.py b/converters/sigma/src/ossie_sigma/ossie_to_sigma.py index 3753aa70..453cd648 100644 --- a/converters/sigma/src/ossie_sigma/ossie_to_sigma.py +++ b/converters/sigma/src/ossie_sigma/ossie_to_sigma.py @@ -281,7 +281,7 @@ def _build_element( relationships = relationships_by_element.get(element_id, []) if relationships: element["relationships"] = [ - self._build_relationship(r, dataset.name, dataset_element_id, field_ids) for r in relationships + self._build_relationship(r, dataset.name, dataset_element_id, field_ids, issues) for r in relationships ] return element @@ -348,6 +348,7 @@ def _build_relationship( dataset_name: str, dataset_element_id: dict[str, str], field_ids: dict[str, str], + issues: list[ConverterIssue], ) -> dict[str, Any]: ext = _sigma_ext(rel) or {} target_element_id = dataset_element_id.get(rel.to, rel.to) @@ -367,6 +368,18 @@ def _build_relationship( if raw_keys is not None: result["keys"] = raw_keys else: + if len(rel.from_columns) != len(rel.to_columns): + # zip() below stops at the shorter array; record what it drops. + issues.append( + ConverterIssue( + ConverterIssueType.RELATIONSHIP_COLUMN_ARITY_MISMATCH, + f"{dataset_name}.{rel.name}", + f"from_columns ({len(rel.from_columns)}) and to_columns " + f"({len(rel.to_columns)}) have different lengths; the " + f"{abs(len(rel.from_columns) - len(rel.to_columns))} extra " + "key column(s) were dropped from the Sigma relationship.", + ) + ) result["keys"] = [ { "sourceColumnId": field_ids.get(from_col, from_col), diff --git a/converters/sigma/tests/test_ossie_to_sigma.py b/converters/sigma/tests/test_ossie_to_sigma.py index da6ff383..3e1495f0 100644 --- a/converters/sigma/tests/test_ossie_to_sigma.py +++ b/converters/sigma/tests/test_ossie_to_sigma.py @@ -140,6 +140,82 @@ def test_legacy_semantic_model_wrapper_is_rejected(): OssieDocument(semantic_model=[]) +def test_relationship_column_arity_mismatch_is_recorded_not_silently_truncated(): + """from_columns/to_columns are independently constrained in the OSI schema (each + only needs to be non-empty), so a compound-key relationship with unequal lengths + is legal input. zip() truncates to the shorter array; that must be a recorded + issue, not a silent drop of the extra key column(s).""" + document = OssieDocument( + name="m", + datasets=[ + OssieDataset(name="orders", source="db.public.orders"), + OssieDataset(name="regions", source="db.public.regions"), + ], + relationships=[ + OssieRelationship( + name="OrderRegion", + **{"from": "orders"}, + to="regions", + from_columns=["region_id", "sub_id"], + to_columns=["region_id"], + ), + ], + ) + + result = OssieToSigmaConverter().convert(document) + + issue_types = {i.issue_type for i in result.issues} + assert ConverterIssueType.RELATIONSHIP_COLUMN_ARITY_MISMATCH in issue_types + + rel = next( + r for p in result.output["pages"] for e in p["elements"] for r in e.get("relationships", []) + ) + # The mismatch is still recorded rather than crashing the conversion, but only + # one key pair can be formed from a 2-vs-1 mismatch. + assert len(rel["keys"]) == 1 + + +def test_relationship_arity_mismatch_element_names_are_scoped_by_owning_dataset(): + """Relationship identity is already scoped by (dataset_name, rel.name) (see + test_relationship_ids_are_scoped_by_owning_dataset); an arity-mismatch issue's + element_name must be scoped the same way, or two unrelated relationships sharing a + name on different table pairs become indistinguishable in the issue list.""" + document = OssieDocument( + name="m", + datasets=[ + OssieDataset(name="orders", source="db.public.orders"), + OssieDataset(name="customers", source="db.public.customers"), + OssieDataset(name="shipments", source="db.public.shipments"), + OssieDataset(name="carriers", source="db.public.carriers"), + ], + relationships=[ + OssieRelationship( + name="Parent", + **{"from": "orders"}, + to="customers", + from_columns=["region_id", "sub_id"], + to_columns=["region_id"], + ), + OssieRelationship( + name="Parent", + **{"from": "shipments"}, + to="carriers", + from_columns=["region_id", "sub_id"], + to_columns=["region_id"], + ), + ], + ) + + result = OssieToSigmaConverter().convert(document) + + arity_issues = [ + i for i in result.issues if i.issue_type == ConverterIssueType.RELATIONSHIP_COLUMN_ARITY_MISMATCH + ] + assert len(arity_issues) == 2 + element_names = {i.element_name for i in arity_issues} + assert len(element_names) == 2, "arity-mismatch issues for same-named relationships must not collide" + + def test_model_level_metadata_round_trips_through_ossie_and_back(): spec = load_fixture("fixtureA_sigma.json") spec.update(