Skip to content

Commit 78b7514

Browse files
Add new-syle YAML (Parsed) Metric
1 parent 4acb1df commit 78b7514

File tree

8 files changed

+495
-104
lines changed

8 files changed

+495
-104
lines changed

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 old-style YAML (pre-November 2025)
114116
measure: Optional[MetricInputMeasure] = None
117+
# Only used in old-style YAML (pre-November 2025)
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
@@ -123,7 +127,12 @@ class MetricTypeParams(dbtClassMixin):
123127
metrics: Optional[List[MetricInput]] = None
124128
conversion_type_params: Optional[ConversionTypeParams] = None
125129
cumulative_type_params: Optional[CumulativeTypeParams] = None
130+
131+
# Only used in v2 YAML
126132
metric_aggregation_params: Optional[MetricAggregationParams] = None
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
127136

128137

129138
@dataclass
@@ -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/semantic_manifest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ def _get_pydantic_semantic_manifest(self) -> PydanticSemanticManifest:
206206
)
207207

208208
for metric in self.manifest.metrics.values():
209+
print(metric.to_dict())
209210
pydantic_semantic_manifest.metrics.append(PydanticMetric.parse_obj(metric.to_dict()))
210211

211212
for saved_query in self.manifest.saved_queries.values():

core/dbt/contracts/graph/unparsed.py

Lines changed: 50 additions & 8 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,15 +654,34 @@ class UnparsedMetricInput(dbtClassMixin):
651654

652655
@dataclass
653656
class UnparsedConversionTypeParams(dbtClassMixin):
654-
base_measure: Union[UnparsedMetricInputMeasure, str]
655-
conversion_measure: Union[UnparsedMetricInputMeasure, str]
657+
base_measure: Optional[Union[UnparsedMetricInputMeasure, str]]
658+
conversion_measure: Optional[Union[UnparsedMetricInputMeasure, str]]
656659
entity: str
660+
661+
# # *_measure params are for old-style YAML.
662+
# # *_metric params are for v2-style metrics.
663+
# base_metric: Optional[Union[UnparsedMetricInput, str]] = None
664+
# conversion_metric: Optional[Union[UnparsedMetricInput, str]] = None
665+
657666
calculation: str = (
658667
ConversionCalculationType.CONVERSION_RATE.value
659668
) # ConversionCalculationType Enum
660669
window: Optional[str] = None
661670
constant_properties: Optional[List[ConstantPropertyInput]] = None
662671

672+
# @override
673+
# @classmethod
674+
# def validate(cls, data):
675+
# super().validate(data)
676+
# if data.get("base_measure") is None and data.get("base_metric") is None:
677+
# raise ValidationError(
678+
# "Conversion metrics must define a base_measure or base_metric parameter."
679+
# )
680+
# if data.get("conversion_measure") is None and data.get("conversion_metric") is None:
681+
# raise ValidationError(
682+
# "Conversion metrics must define a conversion_measure or conversion_metric parameter."
683+
# )
684+
663685

664686
@dataclass
665687
class UnparsedCumulativeTypeParams(dbtClassMixin):
@@ -757,18 +779,25 @@ class UnparsedMetricV2(UnparsedMetricBase):
757779
input_metric: Optional[Union[str, Dict[str, Any]]] = None
758780

759781
# For ratio metrics
760-
numerator: Optional[Union[str, Dict[str, Any]]] = None
761-
denominator: Optional[Union[str, Dict[str, Any]]] = None
782+
numerator: Optional[Union[UnparsedMetricInput, str]] = None
783+
denominator: Optional[Union[UnparsedMetricInput, str]] = None
762784

763785
# For derived metrics
764-
input_metrics: Optional[List[Dict[str, Any]]] = None
786+
input_metrics: Optional[List[Union[UnparsedMetricInput, str]]] = None
765787

766788
# For conversion metrics
767789
entity: Optional[str] = None
768790
calculation: Optional[str] = None
769-
base_metric: Optional[Union[str, Dict[str, Any]]] = None
770-
conversion_metric: Optional[Union[str, Dict[str, Any]]] = None
771-
constant_properties: Optional[List[Dict[str, Any]]] = None
791+
base_metric: Optional[Union[UnparsedMetricInput, str]] = None
792+
conversion_metric: Optional[Union[UnparsedMetricInput, str]] = None
793+
constant_properties: Optional[List[ConstantPropertyInput]] = None
794+
795+
@classmethod
796+
@override
797+
def validate(cls, data):
798+
super().validate(data)
799+
if data["type"] == "simple" and data.get("agg") is None:
800+
raise ValidationError("Simple metrics must have an agg param.")
772801

773802

774803
@dataclass
@@ -783,6 +812,14 @@ def validate(cls, data):
783812
super(UnparsedGroup, cls).validate(data)
784813
if data["owner"].get("name") is None and data["owner"].get("email") is None:
785814
raise ValidationError("Group owner must have at least one of 'name' or 'email'.")
815+
# TODO DI-4413: the following are not strictly necessary (they will be handled
816+
# in dsi validation), but they would be a better user experience
817+
# if we did it at parse time.
818+
# TODO: validate that conversion metrics have base_metric, conversion_metric, and entity
819+
# TODO: validate that cumulative metrics have all required inputs here
820+
# TODO: validate that derived metrics have all required inputs here
821+
# TODO: validate that ratio metrics have all required inputs here
822+
# TODO: validate that simple metrics have all required inputs here
786823

787824

788825
@dataclass
@@ -808,6 +845,11 @@ class UnparsedNonAdditiveDimension(dbtClassMixin):
808845
window_groupings: List[str] = field(default_factory=list)
809846

810847

848+
class PercentileType(str, Enum):
849+
DISCRETE = "discrete"
850+
CONTINUOUS = "continuous"
851+
852+
811853
@dataclass
812854
class UnparsedMeasure(dbtClassMixin):
813855
name: str

core/dbt/parser/manifest.py

Lines changed: 82 additions & 38 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,14 @@ 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 len(metric.type_params.input_measures) > 0 or metric.type_params.metric_aggregation_params is not None, f"{metric} should have a measure or agg type defined, but it does not."
19411942
for input_measure in metric.type_params.input_measures:
19421943
target_semantic_model = manifest.resolve_semantic_model_for_measure(
19431944
target_measure_name=input_measure.name,
@@ -1958,6 +1959,39 @@ def _process_metric_depends_on(
19581959
metric.depends_on.add_node(target_semantic_model.unique_id)
19591960

19601961

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

19752009
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-
)
2010+
if (
2011+
metric.type_params.measure is None
2012+
and metric.type_params.metric_aggregation_params is None
2013+
):
2014+
# This should be caught earlier, but just in case, we assert here to avoid
2015+
# any unexpected behaviors.
2016+
raise dbt.exceptions.ParsingError(
2017+
f"Metric {metric} should have a measure or agg type defined, but it does not.",
2018+
node=metric,
2019+
)
2020+
if metric.type_params.measure is not None:
2021+
metric.add_input_measure(metric.type_params.measure)
2022+
_process_metric_depends_on_semantic_models_for_measures(
2023+
manifest=manifest, current_project=current_project, metric=metric
2024+
)
2025+
# TODO DI-4415: Once we can process simple metric merged into a model directly,
2026+
# we need to add a 'depends on' for the semantic model
19832027
elif metric.type is MetricType.CONVERSION:
19842028
conversion_type_params = metric.type_params.conversion_type_params
19852029
assert (
19862030
conversion_type_params
19872031
), 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
2032+
# Handle old-style YAML measure inputs
2033+
if conversion_type_params.base_measure is not None:
2034+
# TODO: add test for base_measure
2035+
metric.add_input_measure(conversion_type_params.base_measure)
2036+
if conversion_type_params.conversion_measure is not None:
2037+
# TODO: add test for conversion_measure
2038+
metric.add_input_measure(conversion_type_params.conversion_measure)
2039+
_process_metric_depends_on_semantic_models_for_measures(
2040+
manifest=manifest,
2041+
current_project=current_project,
2042+
metric=metric,
2043+
)
2044+
# Recursively process input metrics' input measures for blended v2 & old-style YAML inputs
2045+
metric_inputs = []
2046+
if conversion_type_params.base_metric is not None:
2047+
metric_inputs.append(conversion_type_params.base_metric)
2048+
if conversion_type_params.conversion_metric is not None:
2049+
metric_inputs.append(conversion_type_params.conversion_metric)
2050+
_process_multiple_metric_inputs(
2051+
manifest=manifest,
2052+
current_project=current_project,
2053+
metric=metric,
2054+
metric_inputs=metric_inputs,
19922055
)
19932056
elif metric.type is MetricType.DERIVED or metric.type is MetricType.RATIO:
19942057
input_metrics = metric.input_metrics
@@ -2000,31 +2063,12 @@ def _process_metric_node(
20002063
)
20012064
input_metrics = [metric.type_params.numerator, metric.type_params.denominator]
20022065

2003-
for input_metric in input_metrics:
2004-
target_metric = manifest.resolve_metric(
2005-
target_metric_name=input_metric.name,
2006-
target_metric_package=None,
2007-
current_project=current_project,
2008-
node_package=metric.package_name,
2009-
)
2010-
2011-
if target_metric is None:
2012-
raise dbt.exceptions.ParsingError(
2013-
f"The metric `{input_metric.name}` does not exist but was referenced by metric `{metric.name}`.",
2014-
node=metric,
2015-
)
2016-
elif isinstance(target_metric, Disabled):
2017-
raise dbt.exceptions.ParsingError(
2018-
f"The metric `{input_metric.name}` is disabled and thus cannot be referenced.",
2019-
node=metric,
2020-
)
2021-
2022-
_process_metric_node(
2023-
manifest=manifest, current_project=current_project, metric=target_metric
2024-
)
2025-
for input_measure in target_metric.type_params.input_measures:
2026-
metric.add_input_measure(input_measure)
2027-
metric.depends_on.add_node(target_metric.unique_id)
2066+
_process_multiple_metric_inputs(
2067+
manifest=manifest,
2068+
current_project=current_project,
2069+
metric=metric,
2070+
metric_inputs=input_metrics,
2071+
)
20282072
else:
20292073
assert_values_exhausted(metric.type)
20302074

0 commit comments

Comments
 (0)