From 9d5fec3ffdebd8da43920a46add247895ae9b249 Mon Sep 17 00:00:00 2001 From: Chris Eubank <108756251+christianeu-db@users.noreply.github.com> Date: Thu, 10 Sep 2026 01:56:35 +0000 Subject: [PATCH] fix(validation): enforce equal arity for relationship column arrays The spec (core-spec/spec.md, "Relationships") requires from_columns and to_columns to correspond positionally and to have the same number of columns, but nothing enforced it. JSON Schema cannot express a cross-array equality constraint, so a document with mismatched arities (for example from_columns: [a, b] with to_columns: [c]) validated successfully despite describing an incomplete tuple join. Add validate_relationship_column_arity to validation/validate.py as a new endpoint-validation rule, alongside validate_references. It emits an [Arity] error per mismatched relationship and skips documents that already fail schema validation. Covered by tests in validation/tests/test_validate.py. Addresses Problem 6 (Relationship Column Array Arity) from https://github.com/apache/ossie/discussions/374 Signed-off-by: Chris Eubank <108756251+christianeu-db@users.noreply.github.com> --- validation/tests/test_validate.py | 55 +++++++++++++++++++++++++++++++ validation/validate.py | 34 ++++++++++++++++++- 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/validation/tests/test_validate.py b/validation/tests/test_validate.py index 30806f18..bc41135b 100644 --- a/validation/tests/test_validate.py +++ b/validation/tests/test_validate.py @@ -32,6 +32,7 @@ _SPEC.loader.exec_module(_VALIDATE) validate_references = _VALIDATE.validate_references +validate_relationship_column_arity = _VALIDATE.validate_relationship_column_arity def _document(datasets: list[dict], relationships: list[dict]) -> dict: @@ -161,3 +162,57 @@ def test_skips_malformed_flat_unique_keys() -> None: ) assert errors == [] + + +def _arity_relationship(from_columns: list[str], to_columns: list[str]) -> dict: + return { + "name": "orders_to_customers", + "from": "orders", + "to": "customers", + "from_columns": from_columns, + "to_columns": to_columns, + } + + +@pytest.mark.parametrize( + ("from_columns", "to_columns"), + [ + (["customer_id"], ["id"]), + (["product_id", "variant_id"], ["id", "variant_id"]), + ], +) +def test_arity_accepts_equal_length_columns( + from_columns: list[str], to_columns: list[str] +) -> None: + rel = _arity_relationship(from_columns, to_columns) + + assert validate_relationship_column_arity(_document([_ORDERS, _CUSTOMERS], [rel])) == [] + + +@pytest.mark.parametrize( + ("from_columns", "to_columns"), + [ + (["product_id", "variant_id"], ["id"]), + (["customer_id"], ["id", "variant_id"]), + ], +) +def test_arity_rejects_mismatched_length_columns( + from_columns: list[str], to_columns: list[str] +) -> None: + rel = _arity_relationship(from_columns, to_columns) + + errors = validate_relationship_column_arity(_document([_ORDERS, _CUSTOMERS], [rel])) + + assert errors == [ + f"[Arity] Relationship 'orders_to_customers' in model 'm': " + f"from_columns ({len(from_columns)}) and to_columns ({len(to_columns)}) " + f"must have the same number of columns" + ] + + +def test_arity_skips_non_list_columns() -> None: + # Schema validation reports the shape error; the arity check must not crash. + rel = _arity_relationship(["customer_id"], ["id"]) + rel["to_columns"] = "id" + + assert validate_relationship_column_arity(_document([_ORDERS, _CUSTOMERS], [rel])) == [] diff --git a/validation/validate.py b/validation/validate.py index e11d49f4..04932138 100644 --- a/validation/validate.py +++ b/validation/validate.py @@ -33,7 +33,8 @@ 1. JSON Schema (structure, types, enums) 2. Unique names (datasets, fields, metrics, relationships) 3. Valid relationship references -4. SQL syntax (using sqlglot) +4. Relationship column arity (from_columns and to_columns lengths match) +5. SQL syntax (using sqlglot) Usage: python validation/validate.py @@ -231,6 +232,36 @@ def validate_references(data: dict) -> list[str]: return errors +def validate_relationship_column_arity(data: dict) -> list[str]: + """Validate that from_columns and to_columns have the same length. + + The spec requires the two arrays to correspond positionally, so their + lengths must match. JSON Schema cannot express this, so it is checked here. + """ + errors = [] + + for model in data.get("semantic_model", []): + model_name = model.get("name", "") + + for rel in model.get("relationships", []): + rel_name = rel.get("name", "") + from_columns = rel.get("from_columns") + to_columns = rel.get("to_columns") + + # Skip anything that already failed schema validation. + if not isinstance(from_columns, list) or not isinstance(to_columns, list): + continue + + if len(from_columns) != len(to_columns): + errors.append( + f"[Arity] Relationship '{rel_name}' in model '{model_name}': " + f"from_columns ({len(from_columns)}) and " + f"to_columns ({len(to_columns)}) must have the same number of columns" + ) + + return errors + + def validate_sql_expression(expr: str, dialect: str, context: str) -> str | None: """Validate a single SQL expression. Returns error message or None if valid.""" if not SQLGLOT_AVAILABLE: @@ -344,6 +375,7 @@ def main(): if data.get("semantic_model"): errors.extend(validate_unique_names(data)) errors.extend(validate_references(data)) + errors.extend(validate_relationship_column_arity(data)) errors.extend(validate_sql(data)) # Report results