Skip to content

Commit b833b17

Browse files
Add new-syle YAML metric
1 parent 11ada88 commit b833b17

File tree

2 files changed

+94
-13
lines changed

2 files changed

+94
-13
lines changed

core/dbt/contracts/graph/unparsed.py

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -610,12 +610,37 @@ class UnparsedMetricTypeParams(dbtClassMixin):
610610
cumulative_type_params: Optional[UnparsedCumulativeTypeParams] = None
611611

612612

613+
class UnparsedMetricBase(dbtClassMixin):
614+
@classmethod
615+
def validate(cls, data):
616+
super().validate(data)
617+
if "name" in data:
618+
errors = []
619+
if " " in data["name"]:
620+
errors.append("cannot contain spaces")
621+
# This handles failing queries due to too long metric names.
622+
# It only occurs in BigQuery and Snowflake (Postgres/Redshift truncate)
623+
if len(data["name"]) > 250:
624+
errors.append("cannot contain more than 250 characters")
625+
if not (re.match(r"^[A-Za-z]", data["name"])):
626+
errors.append("must begin with a letter")
627+
if not (re.match(r"[\w-]+$", data["name"])):
628+
errors.append("must contain only letters, numbers and underscores")
629+
630+
if errors:
631+
raise ParsingError(
632+
f"The metric name '{data['name']}' is invalid. It {', '.join(e for e in errors)}"
633+
)
634+
635+
613636
@dataclass
614-
class UnparsedMetric(dbtClassMixin):
637+
class UnparsedMetric(UnparsedMetricBase):
638+
"""Old-style YAML metric; prefer UnparsedMetricV2 instead as of late 2025."""
639+
615640
name: str
616641
label: str
617642
type: str
618-
type_params: UnparsedMetricTypeParams
643+
type_params: UnparsedMetricTypeParams # old-style YAML
619644
description: str = ""
620645
# Note: `Union` must be the outermost part of the type annotation for serialization to work properly.
621646
filter: Union[str, List[str], None] = None
@@ -625,22 +650,71 @@ class UnparsedMetric(dbtClassMixin):
625650
tags: List[str] = field(default_factory=list)
626651
config: Dict[str, Any] = field(default_factory=dict)
627652

653+
654+
@dataclass
655+
class UnparsedNonAdditiveDimensionV2(dbtClassMixin):
656+
name: str
657+
window_agg: str # AggregationType enum
658+
group_by: List[str] = field(default_factory=list)
659+
660+
661+
@dataclass
662+
class UnparsedMetricV2(UnparsedMetricBase):
663+
name: str
664+
label: Optional[str] = None
665+
hidden: bool = False
666+
description: Optional[str] = None
667+
type: Optional[str] = "simple"
668+
agg: Optional[str] = None
669+
670+
percentile: Optional[float] = None
671+
percentile_type: Optional[str] = None
672+
673+
join_to_timespine: Optional[bool] = None
674+
fill_nulls_with: Optional[int] = None
675+
expr: Optional[Union[str, int]] = None
676+
filter: Union[str, List[str], None] = None
677+
678+
tags: List[str] = field(default_factory=list)
679+
meta: Dict[str, Any] = field(default_factory=dict)
680+
config: Dict[str, Any] = field(default_factory=dict)
681+
682+
non_additive_dimension: Optional[UnparsedNonAdditiveDimensionV2] = None
683+
agg_time_dimension: Optional[str] = None
684+
685+
# For cumulative metrics
686+
window: Optional[str] = None
687+
grain_to_date: Optional[str] = None
688+
period_agg: Optional[str] = None
689+
input_metric: Optional[Union[str, Dict[str, Any]]] = None
690+
691+
# For ratio metrics
692+
numerator: Optional[Union[str, Dict[str, Any]]] = None
693+
denominator: Optional[Union[str, Dict[str, Any]]] = None
694+
695+
# For derived metrics
696+
input_metrics: Optional[List[Dict[str, Any]]] = None
697+
698+
# For conversion metrics
699+
entity: Optional[str] = None
700+
calculation: Optional[str] = None
701+
base_metric: Optional[Union[str, Dict[str, Any]]] = None
702+
conversion_metric: Optional[Union[str, Dict[str, Any]]] = None
703+
constant_properties: Optional[List[Dict[str, Any]]] = None
704+
628705
@classmethod
629706
def validate(cls, data):
630-
super(UnparsedMetric, cls).validate(data)
707+
super(UnparsedMetricV2, cls).validate(data)
631708
if "name" in data:
632709
errors = []
633710
if " " in data["name"]:
634711
errors.append("cannot contain spaces")
635-
# This handles failing queries due to too long metric names.
636-
# It only occurs in BigQuery and Snowflake (Postgres/Redshift truncate)
637712
if len(data["name"]) > 250:
638713
errors.append("cannot contain more than 250 characters")
639714
if not (re.match(r"^[A-Za-z]", data["name"])):
640715
errors.append("must begin with a letter")
641716
if not (re.match(r"[\w-]+$", data["name"])):
642717
errors.append("must contain only letters, numbers and underscores")
643-
644718
if errors:
645719
raise ParsingError(
646720
f"The metric name '{data['name']}' is invalid. It {', '.join(e for e in errors)}"

core/dbt/parser/schema_yaml_readers.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272

7373

7474
def parse_where_filter(
75-
where: Optional[Union[List[str], str]]
75+
where: Optional[Union[List[str], str]],
7676
) -> Optional[WhereFilterIntersection]:
7777
if where is None:
7878
return None
@@ -470,12 +470,19 @@ def _generate_metric_config(
470470

471471
def parse(self) -> None:
472472
for data in self.get_key_dicts():
473-
try:
474-
UnparsedMetric.validate(data)
475-
unparsed = UnparsedMetric.from_dict(data)
476-
477-
except (ValidationError, JSONValidationError) as exc:
478-
raise YamlParseDictError(self.yaml.path, self.key, data, exc)
473+
if "type_params" in data:
474+
try:
475+
UnparsedMetric.validate(data)
476+
unparsed = UnparsedMetric.from_dict(data)
477+
478+
except (ValidationError, JSONValidationError) as exc:
479+
raise YamlParseDictError(self.yaml.path, self.key, data, exc)
480+
else:
481+
try:
482+
UnparsedMetricV2.validate(data)
483+
unparsed = UnparsedMetricV2.from_dict(data)
484+
except (ValidationError, JSONValidationError) as exc:
485+
raise YamlParseDictError(self.yaml.path, self.key, data, exc)
479486
self.parse_metric(unparsed)
480487

481488

0 commit comments

Comments
 (0)