From 75446fbc2926aa359251bc2eeaa126a725ca846e Mon Sep 17 00:00:00 2001 From: Amir Fathi Date: Tue, 15 Sep 2026 20:42:13 +0000 Subject: [PATCH 1/3] 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 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..7832c2c3 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, + 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..46381eaa 100644 --- a/converters/sigma/tests/test_ossie_to_sigma.py +++ b/converters/sigma/tests/test_ossie_to_sigma.py @@ -140,6 +140,45 @@ 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( + 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 c0905a36a86913da6b7d2d72e4e42153d50dc819 Mon Sep 17 00:00:00 2001 From: Amir Fathi Date: Wed, 16 Sep 2026 16:15:44 +0000 Subject: [PATCH 2/3] 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 7832c2c3..453cd648 100644 --- a/converters/sigma/src/ossie_sigma/ossie_to_sigma.py +++ b/converters/sigma/src/ossie_sigma/ossie_to_sigma.py @@ -373,7 +373,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 46381eaa..d4887754 100644 --- a/converters/sigma/tests/test_ossie_to_sigma.py +++ b/converters/sigma/tests/test_ossie_to_sigma.py @@ -179,6 +179,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( From db87056737d4d178511cd155ea29fa0cdada76a3 Mon Sep 17 00:00:00 2001 From: Amir Fathi Date: Sun, 27 Sep 2026 08:23:16 +0000 Subject: [PATCH 3/3] fix(sigma): update arity-mismatch tests for the flat OssieDocument shape Rebased onto main after #396 (flat semantic model documents). The two new tests from the previous commits still built OssieDocument via the retired semantic_model=[OssieSemanticModel(...)] wrapper, which now raises a ValidationError, so both were failing with NameError: name 'OssieSemanticModel' is not defined. Rewritten to the flat OssieDocument(name=..., datasets=..., relationships=...) shape used elsewhere in this file. --- converters/sigma/tests/test_ossie_to_sigma.py | 82 +++++++++---------- 1 file changed, 37 insertions(+), 45 deletions(-) diff --git a/converters/sigma/tests/test_ossie_to_sigma.py b/converters/sigma/tests/test_ossie_to_sigma.py index d4887754..3e1495f0 100644 --- a/converters/sigma/tests/test_ossie_to_sigma.py +++ b/converters/sigma/tests/test_ossie_to_sigma.py @@ -146,24 +146,20 @@ def test_relationship_column_arity_mismatch_is_recorded_not_silently_truncated() 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"], - ), - ], - ) - ] + 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) @@ -185,33 +181,29 @@ def test_relationship_arity_mismatch_element_names_are_scoped_by_owning_dataset( 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"], - ), - ], - ) - ] + 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)