You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ossie.apache.org by kb...@apache.org on 2026/09/15 15:52:32 UTC

(ossie) branch main updated: fix(validation): enforce equal arity for relationship column arrays (#375)

This is an automated email from the ASF dual-hosted git repository.

khush-bhatia pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ossie.git


The following commit(s) were added to refs/heads/main by this push:
     new 8170f667 fix(validation): enforce equal arity for relationship column arrays (#375)
8170f667 is described below

commit 8170f66716a175b9fd40ade92da5b06e498d40c1
Author: Chris Eubank <10...@users.noreply.github.com>
AuthorDate: Tue Sep 15 08:52:25 2026 -0700

    fix(validation): enforce equal arity for relationship column arrays (#375)
    
    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 <10...@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 @@ _VALIDATE = module_from_spec(_SPEC)
 _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 @@ Validates Ossie YAML files against:
 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 <yaml_file>
@@ -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", "<unnamed>")
+
+        for rel in model.get("relationships", []):
+            rel_name = rel.get("name", "<unnamed>")
+            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