diff --git a/.changes/unreleased/Fixes-20251107-160515.yaml b/.changes/unreleased/Fixes-20251107-160515.yaml new file mode 100644 index 00000000000..66ec9588ded --- /dev/null +++ b/.changes/unreleased/Fixes-20251107-160515.yaml @@ -0,0 +1,6 @@ +kind: Fixes +body: For metric names, fix bug allowing hyphens (not allowed in metricflow already), make validation throw ValidationErrors (not ParsingErrors), and add tests. +time: 2025-11-07T16:05:15.946331-08:00 +custom: + Author: theyostalservice + Issue: n/a diff --git a/core/dbt/contracts/graph/unparsed.py b/core/dbt/contracts/graph/unparsed.py index 9171a56b2ef..6c1a13c43c9 100644 --- a/core/dbt/contracts/graph/unparsed.py +++ b/core/dbt/contracts/graph/unparsed.py @@ -638,11 +638,11 @@ def validate(cls, data): errors.append("cannot contain more than 250 characters") if not (re.match(r"^[A-Za-z]", data["name"])): errors.append("must begin with a letter") - if not (re.match(r"[\w-]+$", data["name"])): + if not (re.match(r"[\w]+$", data["name"])): errors.append("must contain only letters, numbers and underscores") if errors: - raise ParsingError( + raise ValidationError( f"The metric name '{data['name']}' is invalid. It {', '.join(e for e in errors)}" ) diff --git a/tests/unit/contracts/graph/test_unparsed.py b/tests/unit/contracts/graph/test_unparsed.py index 336a72431d9..401a09a0ca1 100644 --- a/tests/unit/contracts/graph/test_unparsed.py +++ b/tests/unit/contracts/graph/test_unparsed.py @@ -933,6 +933,41 @@ def test_bad_tags(self): tst["tags"] = [123] self.assert_fails_validation(tst) + def test_bad_metric_name_with_spaces(self): + tst = self.get_ok_dict() + tst["name"] = "metric name with spaces" + self.assert_fails_validation(tst) + + def test_bad_metric_name_too_long(self): + tst = self.get_ok_dict() + tst["name"] = "a" * 251 + self.assert_fails_validation(tst) + + def test_bad_metric_name_does_not_start_with_letter(self): + tst = self.get_ok_dict() + tst["name"] = "123metric" + self.assert_fails_validation(tst) + + tst["name"] = "_metric" + self.assert_fails_validation(tst) + + def test_bad_metric_name_contains_special_characters(self): + tst = self.get_ok_dict() + tst["name"] = "metric!name" + self.assert_fails_validation(tst) + + tst["name"] = "metric@name" + self.assert_fails_validation(tst) + + tst["name"] = "metric#name" + self.assert_fails_validation(tst) + + tst["name"] = "metric$name" + self.assert_fails_validation(tst) + + tst["name"] = "metric-name" + self.assert_fails_validation(tst) + class TestUnparsedVersion(ContractTestCase): ContractType = UnparsedVersion