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
18 changes: 18 additions & 0 deletions core/wren/src/wren/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -965,6 +965,15 @@ def validate_project(project_path: Path) -> list[ValidationError]:
src = model.get("_source_dir", f"models[{i}]")
src_path = f"models/{src}/metadata.yml"
name = model.get("name")
if isinstance(name, (list, dict)):
errors.append(
ValidationError(
"error",
src_path,
f"model 'name' must be a scalar value, got {type(name).__name__}",
)
)
continue
if not name:
errors.append(ValidationError("error", src_path, "model missing 'name'"))
continue
Expand Down Expand Up @@ -1213,6 +1222,15 @@ def validate_project(project_path: Path) -> list[ValidationError]:
for i, view in enumerate(views):
src_dir = view.get("_source_dir", f"views[{i}]")
name = view.get("name")
if isinstance(name, (list, dict)):
errors.append(
ValidationError(
"error",
f"views/{src_dir}/metadata.yml",
f"view 'name' must be a scalar value, got {type(name).__name__}",
)
)
continue
if not name:
errors.append(
ValidationError(
Expand Down
117 changes: 117 additions & 0 deletions core/wren/tests/unit/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,75 @@ def test_validate_duplicate_model(tmp_path):
assert any("duplicate model name" in e.message for e in errors)


def test_validate_model_name_list_reports_error(tmp_path):
_make_v2_project(tmp_path)
d = tmp_path / "models" / "orders"
d.mkdir(parents=True)
(d / "metadata.yml").write_text(
"name: [a, b]\ntable_reference:\n table: orders\ncolumns: []\n"
)
errors = validate_project(tmp_path)
assert any(
"model 'name' must be a scalar value, got list" in e.message for e in errors
)


def test_validate_model_name_dict_reports_error(tmp_path):
_make_v2_project(tmp_path)
d = tmp_path / "models" / "orders"
d.mkdir(parents=True)
(d / "metadata.yml").write_text(
"name: {x: 1}\ntable_reference:\n table: orders\ncolumns: []\n"
)
errors = validate_project(tmp_path)
assert any(
"model 'name' must be a scalar value, got dict" in e.message for e in errors
)


def test_validate_model_name_int_is_not_rejected(tmp_path):
# An int name is unusual but hashable, so it never hit the crash this guard
# exists for; the guard must not turn it into a new validation error.
_make_v2_project(tmp_path)
d = tmp_path / "models" / "orders"
d.mkdir(parents=True)
(d / "metadata.yml").write_text(
"name: 3\ntable_reference:\n table: orders\ncolumns: []\n"
)
errors = validate_project(tmp_path)
assert not any("must be a scalar value" in e.message for e in errors)


def test_validate_model_name_empty_list_reports_scalar_error_not_missing(tmp_path):
# An empty list is falsy, so the type guard must run before the missing-name
# check or this reports "missing 'name'" instead of the malformed type.
_make_v2_project(tmp_path)
d = tmp_path / "models" / "orders"
d.mkdir(parents=True)
(d / "metadata.yml").write_text(
"name: []\ntable_reference:\n table: orders\ncolumns: []\n"
)
errors = validate_project(tmp_path)
assert any(
"model 'name' must be a scalar value, got list" in e.message for e in errors
)
assert not any("model missing 'name'" in e.message for e in errors)


def test_validate_model_name_empty_dict_reports_scalar_error_not_missing(tmp_path):
_make_v2_project(tmp_path)
d = tmp_path / "models" / "orders"
d.mkdir(parents=True)
(d / "metadata.yml").write_text(
"name: {}\ntable_reference:\n table: orders\ncolumns: []\n"
)
errors = validate_project(tmp_path)
assert any(
"model 'name' must be a scalar value, got dict" in e.message for e in errors
)
assert not any("model missing 'name'" in e.message for e in errors)


def test_validate_both_tref_and_ref_sql(tmp_path):
_make_v2_project(tmp_path)
d = tmp_path / "models" / "conflict"
Expand Down Expand Up @@ -682,6 +751,54 @@ def test_validate_view_no_statement(tmp_path):
assert any("missing 'statement'" in e.message for e in errors)


def test_validate_view_name_list_reports_error(tmp_path):
_make_v2_project(tmp_path)
d = tmp_path / "views" / "monthly"
d.mkdir(parents=True)
(d / "metadata.yml").write_text("name: [a, b]\nstatement: SELECT 1\n")
errors = validate_project(tmp_path)
assert any(
"view 'name' must be a scalar value, got list" in e.message for e in errors
)


def test_validate_view_name_dict_reports_error(tmp_path):
_make_v2_project(tmp_path)
d = tmp_path / "views" / "monthly"
d.mkdir(parents=True)
(d / "metadata.yml").write_text("name: {x: 1}\nstatement: SELECT 1\n")
errors = validate_project(tmp_path)
assert any(
"view 'name' must be a scalar value, got dict" in e.message for e in errors
)


def test_validate_view_name_empty_list_reports_scalar_error_not_missing(tmp_path):
# An empty list is falsy, so the type guard must run before the missing-name
# check or this reports "missing 'name'" instead of the malformed type.
_make_v2_project(tmp_path)
d = tmp_path / "views" / "monthly"
d.mkdir(parents=True)
(d / "metadata.yml").write_text("name: []\nstatement: SELECT 1\n")
errors = validate_project(tmp_path)
assert any(
"view 'name' must be a scalar value, got list" in e.message for e in errors
)
assert not any("view missing 'name'" in e.message for e in errors)


def test_validate_view_name_empty_dict_reports_scalar_error_not_missing(tmp_path):
_make_v2_project(tmp_path)
d = tmp_path / "views" / "monthly"
d.mkdir(parents=True)
(d / "metadata.yml").write_text("name: {}\nstatement: SELECT 1\n")
errors = validate_project(tmp_path)
assert any(
"view 'name' must be a scalar value, got dict" in e.message for e in errors
)
assert not any("view missing 'name'" in e.message for e in errors)


def test_validate_missing_join_type(tmp_path):
_make_valid_project(tmp_path)
(tmp_path / "relationships.yml").write_text(
Expand Down
Loading