fix(context): harden description checks for bad MDL rows (#2616)

This commit is contained in:
Bartok
2026-08-16 22:18:47 -07:00
committed by GitHub
parent 7f7370e4e9
commit b56d4da16b
2 changed files with 97 additions and 1 deletions
+37 -1
View File
@@ -1117,6 +1117,29 @@ def validate_project(project_path: Path) -> list[ValidationError]:
)
)
props = model.get("properties")
if props is not None and not isinstance(props, dict):
errors.append(
ValidationError(
"error",
f"{src_path} > {name}",
"properties must be a mapping",
)
)
for j, col in enumerate(columns):
if not isinstance(col, dict):
continue
col_props = col.get("properties")
if col_props is not None and not isinstance(col_props, dict):
col_name = col.get("name") or f"columns[{j}]"
errors.append(
ValidationError(
"error",
f"{src_path} > {name} > {col_name}",
"properties must be a mapping",
)
)
# v1 legacy views.yml may contain non-mapping entries (e.g. `- null`).
# load_views() silently drops those (matching the other loaders), but
# validate_project's job is to tell the user about hand-edited mistakes
@@ -1232,6 +1255,16 @@ def validate_project(project_path: Path) -> list[ValidationError]:
)
)
view_props = view.get("properties")
if view_props is not None and not isinstance(view_props, dict):
errors.append(
ValidationError(
"error",
f"views/{src_dir}",
"properties must be a mapping",
)
)
# Check relationships — walk the raw list when available so indices match
# the file (filtered loader positions would renumber past dropped junk).
all_entity_names = model_names | view_names
@@ -1644,7 +1677,10 @@ _VALID_LEVELS = frozenset({"error", "warning", "strict"})
def _prop_description(item: dict) -> str | None:
return (item.get("properties") or {}).get("description")
props = item.get("properties") or {}
if not isinstance(props, dict):
return None
return props.get("description")
def _check_descriptions(manifest: dict, *, strict: bool = False) -> list[str]:
@@ -0,0 +1,60 @@
"""Guards for non-mapping properties and safe description lookup."""
from __future__ import annotations
import textwrap
from pathlib import Path
from wren.context import _prop_description, validate_project
def test_prop_description_non_mapping_returns_none():
assert _prop_description({"properties": "oops"}) is None
assert _prop_description({"properties": {"description": "ok"}}) == "ok"
assert _prop_description({}) is None
def test_validate_project_reports_non_mapping_properties(tmp_path: Path):
(tmp_path / "wren_project.yml").write_text(
"name: demo\ndata_source: postgres\nschema_version: 2\n",
encoding="utf-8",
)
model_dir = tmp_path / "models" / "orders"
model_dir.mkdir(parents=True)
(model_dir / "metadata.yml").write_text(
textwrap.dedent(
"""\
name: orders
table_reference:
table: orders
properties: oops
columns:
- name: id
type: INTEGER
properties: not-a-map
"""
),
encoding="utf-8",
)
view_dir = tmp_path / "views" / "v1"
view_dir.mkdir(parents=True)
(view_dir / "metadata.yml").write_text(
textwrap.dedent(
"""\
name: v1
properties: []
"""
),
encoding="utf-8",
)
(view_dir / "sql.yml").write_text(
"statement: SELECT 1\n",
encoding="utf-8",
)
errors = validate_project(tmp_path)
messages = [f"{e.path}: {e.message}" for e in errors]
joined = "\n".join(messages)
assert "properties must be a mapping" in joined
# model + column + view
assert sum("properties must be a mapping" in m for m in messages) == 3