From aeb3be739fbb0b3cf8ea6d2b146f7753a31e0b34 Mon Sep 17 00:00:00 2001 From: Amir Fathi Date: Tue, 15 Sep 2026 20:42:13 +0000 Subject: [PATCH 1/2] fix(sigma): record dropped keys when a relationship's column arrays have unequal length from_columns and to_columns are only constrained independently in the OSI JSON schema (each just needs at least one entry), so a compound-key relationship with unequal-length arrays is legal input. _build_relationship paired them with a bare zip(), which stops at the shorter array and drops the extra key column(s) with no warning at all. Thread the issues list into _build_relationship and record a RELATIONSHIP_COLUMN_ARITY_MISMATCH issue when the lengths differ, mirroring the RELATIONSHIP_COLUMN_UNRESOLVED pattern already used for the reverse direction. The converter still degrades to the shorter pairing (Sigma's own keys array has no way to represent an unequal-length join), but the loss is now visible in the returned ConverterResult.issues instead of silent. --- .../sigma/src/ossie_sigma/converter_issues.py | 1 + .../sigma/src/ossie_sigma/ossie_to_sigma.py | 15 ++++++- converters/sigma/tests/test_ossie_to_sigma.py | 39 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/converters/sigma/src/ossie_sigma/converter_issues.py b/converters/sigma/src/ossie_sigma/converter_issues.py index 2a04005e..0e6c1d04 100644 --- a/converters/sigma/src/ossie_sigma/converter_issues.py +++ b/converters/sigma/src/ossie_sigma/converter_issues.py @@ -9,6 +9,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 f3532617..530092c1 100644 --- a/converters/sigma/src/ossie_sigma/ossie_to_sigma.py +++ b/converters/sigma/src/ossie_sigma/ossie_to_sigma.py @@ -279,7 +279,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 @@ -346,6 +346,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) @@ -365,6 +366,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, + 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 b9204045..bb068898 100644 --- a/converters/sigma/tests/test_ossie_to_sigma.py +++ b/converters/sigma/tests/test_ossie_to_sigma.py @@ -128,6 +128,45 @@ def test_empty_semantic_model_raises_a_clear_error(): OssieToSigmaConverter().convert(document) +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( + semantic_model=[ + OssieSemanticModel( + 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_model_level_metadata_round_trips_through_ossie_and_back(): spec = load_fixture("fixtureA_sigma.json") spec.update( From 633ca26ecdeceac2771ef18657fdc8211b2170ea Mon Sep 17 00:00:00 2001 From: Amir Fathi Date: Wed, 16 Sep 2026 16:15:44 +0000 Subject: [PATCH 2/2] fix(sigma): scope arity-mismatch element_name by owning dataset Relationship identity is already scoped by (dataset_name, rel.name); the arity-mismatch ConverterIssue used the bare relationship name, so two same-named relationships on different table pairs became indistinguishable when both hit the mismatch. --- .../sigma/src/ossie_sigma/ossie_to_sigma.py | 2 +- converters/sigma/tests/test_ossie_to_sigma.py | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/converters/sigma/src/ossie_sigma/ossie_to_sigma.py b/converters/sigma/src/ossie_sigma/ossie_to_sigma.py index 530092c1..2a7fed6e 100644 --- a/converters/sigma/src/ossie_sigma/ossie_to_sigma.py +++ b/converters/sigma/src/ossie_sigma/ossie_to_sigma.py @@ -371,7 +371,7 @@ def _build_relationship( issues.append( ConverterIssue( ConverterIssueType.RELATIONSHIP_COLUMN_ARITY_MISMATCH, - rel.name, + 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 " diff --git a/converters/sigma/tests/test_ossie_to_sigma.py b/converters/sigma/tests/test_ossie_to_sigma.py index bb068898..6502aa5e 100644 --- a/converters/sigma/tests/test_ossie_to_sigma.py +++ b/converters/sigma/tests/test_ossie_to_sigma.py @@ -167,6 +167,51 @@ def test_relationship_column_arity_mismatch_is_recorded_not_silently_truncated() 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( + semantic_model=[ + OssieSemanticModel( + 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(