Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions validation/tests/test_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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])) == []
34 changes: 33 additions & 1 deletion validation/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <yaml_file>
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down