Skip to content

Commit abb58d7

Browse files
Add Parsed class for new-style YAML Metric (#12161)
* Add new-syle YAML (Parsed) Metric * Change changie issue number
1 parent db5a9e0 commit abb58d7

File tree

8 files changed

+392
-92
lines changed

8 files changed

+392
-92
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Features
2+
body: Process semantic metrics in v2 YAML if they are not merged into a model.
3+
time: 2026-01-27T11:41:49.912438-08:00
4+
custom:
5+
Author: theyostalservice
6+
Issue: "12161"

core/dbt/artifacts/resources/v1/metric.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,11 @@ class ConstantPropertyInput(dbtClassMixin):
8383

8484
@dataclass
8585
class ConversionTypeParams(dbtClassMixin):
86-
base_measure: MetricInputMeasure
87-
conversion_measure: MetricInputMeasure
8886
entity: str
87+
base_measure: Optional[MetricInputMeasure] = None
88+
conversion_measure: Optional[MetricInputMeasure] = None
89+
base_metric: Optional[MetricInput] = None
90+
conversion_metric: Optional[MetricInput] = None
8991
calculation: ConversionCalculationType = ConversionCalculationType.CONVERSION_RATE
9092
window: Optional[MetricTimeWindow] = None
9193
constant_properties: Optional[List[ConstantPropertyInput]] = None
@@ -106,13 +108,15 @@ class MetricAggregationParams(dbtClassMixin):
106108
agg_params: Optional[MeasureAggregationParameters] = None
107109
agg_time_dimension: Optional[str] = None
108110
non_additive_dimension: Optional[NonAdditiveDimension] = None
109-
expr: Optional[str] = None
110111

111112

112113
@dataclass
113114
class MetricTypeParams(dbtClassMixin):
115+
# Only used in v1 Semantic YAML
114116
measure: Optional[MetricInputMeasure] = None
117+
# Only used in v1 Semantic YAML
115118
input_measures: List[MetricInputMeasure] = field(default_factory=list)
119+
116120
numerator: Optional[MetricInput] = None
117121
denominator: Optional[MetricInput] = None
118122
expr: Optional[str] = None
@@ -125,6 +129,11 @@ class MetricTypeParams(dbtClassMixin):
125129
cumulative_type_params: Optional[CumulativeTypeParams] = None
126130
metric_aggregation_params: Optional[MetricAggregationParams] = None
127131

132+
# Below this point, all fields are only used in v2 Semantic YAML
133+
fill_nulls_with: Optional[int] = None
134+
join_to_timespine: bool = False
135+
is_private: Optional[bool] = None # populated by "hidden" field in YAML
136+
128137

129138
@dataclass
130139
class MetricConfig(BaseConfig):
@@ -148,8 +157,6 @@ class Metric(GraphResource):
148157
metadata: Optional[SourceFileMetadata] = None
149158
time_granularity: Optional[str] = None
150159
resource_type: Literal[NodeType.Metric]
151-
meta: Dict[str, Any] = field(default_factory=dict, metadata=MergeBehavior.Update.meta())
152-
tags: List[str] = field(default_factory=list)
153160
config: MetricConfig = field(default_factory=MetricConfig)
154161
unrendered_config: Dict[str, Any] = field(default_factory=dict)
155162
sources: List[List[str]] = field(default_factory=list)
@@ -159,14 +166,18 @@ class Metric(GraphResource):
159166
created_at: float = field(default_factory=lambda: time.time())
160167
group: Optional[str] = None
161168

169+
# These fields are only used in v1 metrics.
170+
meta: Dict[str, Any] = field(default_factory=dict, metadata=MergeBehavior.Update.meta())
171+
tags: List[str] = field(default_factory=list)
172+
173+
@property
174+
def input_metrics(self) -> List[MetricInput]:
175+
return self.type_params.metrics or []
176+
162177
@property
163178
def input_measures(self) -> List[MetricInputMeasure]:
164179
return self.type_params.input_measures
165180

166181
@property
167182
def measure_references(self) -> List[MeasureReference]:
168183
return [x.measure_reference() for x in self.input_measures]
169-
170-
@property
171-
def input_metrics(self) -> List[MetricInput]:
172-
return self.type_params.metrics or []

core/dbt/contracts/graph/unparsed.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import datetime
22
import re
33
from dataclasses import dataclass, field
4+
from enum import Enum
45
from pathlib import Path
56
from typing import Any, Dict, List, Literal, Optional, Sequence, Union
67

8+
from typing_extensions import override
9+
710
# trigger the PathEncoder
811
import dbt_common.helper_types # noqa:F401
912
from dbt import deprecations
@@ -651,6 +654,8 @@ class UnparsedMetricInput(dbtClassMixin):
651654

652655
@dataclass
653656
class UnparsedConversionTypeParams(dbtClassMixin):
657+
"""Only used in v1 Semantic YAML"""
658+
654659
base_measure: Union[UnparsedMetricInputMeasure, str]
655660
conversion_measure: Union[UnparsedMetricInputMeasure, str]
656661
entity: str
@@ -663,6 +668,8 @@ class UnparsedConversionTypeParams(dbtClassMixin):
663668

664669
@dataclass
665670
class UnparsedCumulativeTypeParams(dbtClassMixin):
671+
"""Only used in v1 Semantic YAML"""
672+
666673
window: Optional[str] = None
667674
grain_to_date: Optional[str] = None
668675
period_agg: str = PeriodAggregation.FIRST.value
@@ -759,18 +766,25 @@ class UnparsedMetricV2(UnparsedMetricBase):
759766
input_metric: Optional[Union[str, Dict[str, Any]]] = None
760767

761768
# For ratio metrics
762-
numerator: Optional[Union[str, Dict[str, Any]]] = None
763-
denominator: Optional[Union[str, Dict[str, Any]]] = None
769+
numerator: Optional[Union[UnparsedMetricInput, str]] = None
770+
denominator: Optional[Union[UnparsedMetricInput, str]] = None
764771

765772
# For derived metrics
766-
input_metrics: Optional[List[Dict[str, Any]]] = None
773+
input_metrics: Optional[List[Union[UnparsedMetricInput, str]]] = None
767774

768775
# For conversion metrics
769776
entity: Optional[str] = None
770777
calculation: Optional[str] = None
771-
base_metric: Optional[Union[str, Dict[str, Any]]] = None
772-
conversion_metric: Optional[Union[str, Dict[str, Any]]] = None
773-
constant_properties: Optional[List[Dict[str, Any]]] = None
778+
base_metric: Optional[Union[UnparsedMetricInput, str]] = None
779+
conversion_metric: Optional[Union[UnparsedMetricInput, str]] = None
780+
constant_properties: Optional[List[ConstantPropertyInput]] = None
781+
782+
@classmethod
783+
@override
784+
def validate(cls, data):
785+
super().validate(data)
786+
if data["type"] == "simple" and data.get("agg") is None:
787+
raise ValidationError("Simple metrics must have an agg param.")
774788

775789

776790
@dataclass
@@ -785,6 +799,14 @@ def validate(cls, data):
785799
super(UnparsedGroup, cls).validate(data)
786800
if data["owner"].get("name") is None and data["owner"].get("email") is None:
787801
raise ValidationError("Group owner must have at least one of 'name' or 'email'.")
802+
# TODO DI-4413: the following are not strictly necessary (they will be handled
803+
# in dsi validation), but they would be a better user experience
804+
# if we did it at parse time.
805+
# TODO: validate that conversion metrics have base_metric, conversion_metric, and entity
806+
# TODO: validate that cumulative metrics have all required inputs here
807+
# TODO: validate that derived metrics have all required inputs here
808+
# TODO: validate that ratio metrics have all required inputs here
809+
# TODO: validate that simple metrics have all required inputs here
788810

789811

790812
@dataclass
@@ -810,6 +832,11 @@ class UnparsedNonAdditiveDimension(dbtClassMixin):
810832
window_groupings: List[str] = field(default_factory=list)
811833

812834

835+
class PercentileType(str, Enum):
836+
DISCRETE = "discrete"
837+
CONTINUOUS = "continuous"
838+
839+
813840
@dataclass
814841
class UnparsedMeasure(dbtClassMixin):
815842
name: str

core/dbt/parser/manifest.py

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from dbt.artifacts.resources import (
2929
CatalogWriteIntegrationConfig,
3030
FileHash,
31+
MetricInput,
3132
NodeRelation,
3233
NodeVersion,
3334
)
@@ -1930,14 +1931,17 @@ def _process_refs(
19301931
node.depends_on.add_node(target_model_id)
19311932

19321933

1933-
def _process_metric_depends_on(
1934+
def _process_metric_depends_on_semantic_models_for_measures(
19341935
manifest: Manifest,
19351936
current_project: str,
19361937
metric: Metric,
19371938
) -> None:
19381939
"""For a given metric, set the `depends_on` property"""
19391940

1940-
assert len(metric.type_params.input_measures) > 0
1941+
assert (
1942+
len(metric.type_params.input_measures) > 0
1943+
or metric.type_params.metric_aggregation_params is not None
1944+
), f"{metric} should have a measure or agg type defined, but it does not."
19411945
for input_measure in metric.type_params.input_measures:
19421946
target_semantic_model = manifest.resolve_semantic_model_for_measure(
19431947
target_measure_name=input_measure.name,
@@ -1958,6 +1962,39 @@ def _process_metric_depends_on(
19581962
metric.depends_on.add_node(target_semantic_model.unique_id)
19591963

19601964

1965+
def _process_multiple_metric_inputs(
1966+
manifest: Manifest,
1967+
current_project: str,
1968+
metric: Metric,
1969+
metric_inputs: List[MetricInput],
1970+
) -> None:
1971+
for input_metric in metric_inputs:
1972+
target_metric = manifest.resolve_metric(
1973+
target_metric_name=input_metric.name,
1974+
target_metric_package=None,
1975+
current_project=current_project,
1976+
node_package=metric.package_name,
1977+
)
1978+
1979+
if target_metric is None:
1980+
raise dbt.exceptions.ParsingError(
1981+
f"The metric `{input_metric.name}` does not exist but was referenced.",
1982+
node=metric,
1983+
)
1984+
elif isinstance(target_metric, Disabled):
1985+
raise dbt.exceptions.ParsingError(
1986+
f"The metric `{input_metric.name}` is disabled and thus cannot be referenced.",
1987+
node=metric,
1988+
)
1989+
1990+
_process_metric_node(
1991+
manifest=manifest, current_project=current_project, metric=target_metric
1992+
)
1993+
for input_measure in target_metric.type_params.input_measures:
1994+
metric.add_input_measure(input_measure)
1995+
metric.depends_on.add_node(target_metric.unique_id)
1996+
1997+
19611998
def _process_metric_node(
19621999
manifest: Manifest,
19632000
current_project: str,
@@ -1973,23 +2010,40 @@ def _process_metric_node(
19732010
return
19742011

19752012
if metric.type is MetricType.SIMPLE or metric.type is MetricType.CUMULATIVE:
1976-
assert (
1977-
metric.type_params.measure is not None
1978-
), f"{metric} should have a measure defined, but it does not."
1979-
metric.add_input_measure(metric.type_params.measure)
1980-
_process_metric_depends_on(
1981-
manifest=manifest, current_project=current_project, metric=metric
1982-
)
2013+
if (
2014+
metric.type_params.measure is None
2015+
and metric.type_params.metric_aggregation_params is None
2016+
):
2017+
# This should be caught earlier, but just in case, we assert here to avoid
2018+
# any unexpected behaviors.
2019+
raise dbt.exceptions.ParsingError(
2020+
f"Metric {metric} should have a measure or agg type defined, but it does not.",
2021+
node=metric,
2022+
)
2023+
if metric.type_params.measure is not None:
2024+
metric.add_input_measure(metric.type_params.measure)
2025+
_process_metric_depends_on_semantic_models_for_measures(
2026+
manifest=manifest, current_project=current_project, metric=metric
2027+
)
2028+
# TODO DI-4415: Once we can process simple metric merged into a model directly,
2029+
# we need to add a 'depends on' for the semantic model
19832030
elif metric.type is MetricType.CONVERSION:
19842031
conversion_type_params = metric.type_params.conversion_type_params
19852032
assert (
19862033
conversion_type_params
19872034
), f"{metric.name} is a conversion metric and must have conversion_type_params defined."
1988-
metric.add_input_measure(conversion_type_params.base_measure)
1989-
metric.add_input_measure(conversion_type_params.conversion_measure)
1990-
_process_metric_depends_on(
1991-
manifest=manifest, current_project=current_project, metric=metric
2035+
# Handle old-style YAML measure inputs
2036+
if conversion_type_params.base_measure is not None:
2037+
metric.add_input_measure(conversion_type_params.base_measure)
2038+
if conversion_type_params.conversion_measure is not None:
2039+
metric.add_input_measure(conversion_type_params.conversion_measure)
2040+
_process_metric_depends_on_semantic_models_for_measures(
2041+
manifest=manifest,
2042+
current_project=current_project,
2043+
metric=metric,
19922044
)
2045+
# If we ever want to enable blended v1 and v2 manifests, we'll need to recurse through
2046+
# metric inputs here to find their measure inputs.
19932047
elif metric.type is MetricType.DERIVED or metric.type is MetricType.RATIO:
19942048
input_metrics = metric.input_metrics
19952049
if metric.type is MetricType.RATIO:
@@ -2007,7 +2061,6 @@ def _process_metric_node(
20072061
current_project=current_project,
20082062
node_package=metric.package_name,
20092063
)
2010-
20112064
if target_metric is None:
20122065
raise dbt.exceptions.ParsingError(
20132066
f"The metric `{input_metric.name}` does not exist but was referenced by metric `{metric.name}`.",
@@ -2018,7 +2071,6 @@ def _process_metric_node(
20182071
f"The metric `{input_metric.name}` is disabled and thus cannot be referenced.",
20192072
node=metric,
20202073
)
2021-
20222074
_process_metric_node(
20232075
manifest=manifest, current_project=current_project, metric=target_metric
20242076
)

0 commit comments

Comments
 (0)