Skip to content

Commit 7af3b00

Browse files
Add new-syle YAML (Parsed) Metric
1 parent e839e7a commit 7af3b00

File tree

9 files changed

+550
-103
lines changed

9 files changed

+550
-103
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
@@ -622,15 +625,34 @@ class UnparsedMetricInput(dbtClassMixin):
622625

623626
@dataclass
624627
class UnparsedConversionTypeParams(dbtClassMixin):
625-
base_measure: Union[UnparsedMetricInputMeasure, str]
626-
conversion_measure: Union[UnparsedMetricInputMeasure, str]
628+
base_measure: Optional[Union[UnparsedMetricInputMeasure, str]]
629+
conversion_measure: Optional[Union[UnparsedMetricInputMeasure, str]]
627630
entity: str
631+
632+
# # *_measure params are for old-style YAML.
633+
# # *_metric params are for v2-style metrics.
634+
# base_metric: Optional[Union[UnparsedMetricInput, str]] = None
635+
# conversion_metric: Optional[Union[UnparsedMetricInput, str]] = None
636+
628637
calculation: str = (
629638
ConversionCalculationType.CONVERSION_RATE.value
630639
) # ConversionCalculationType Enum
631640
window: Optional[str] = None
632641
constant_properties: Optional[List[ConstantPropertyInput]] = None
633642

643+
# @override
644+
# @classmethod
645+
# def validate(cls, data):
646+
# super().validate(data)
647+
# if data.get("base_measure") is None and data.get("base_metric") is None:
648+
# raise ValidationError(
649+
# "Conversion metrics must define a base_measure or base_metric parameter."
650+
# )
651+
# if data.get("conversion_measure") is None and data.get("conversion_metric") is None:
652+
# raise ValidationError(
653+
# "Conversion metrics must define a conversion_measure or conversion_metric parameter."
654+
# )
655+
634656

635657
@dataclass
636658
class UnparsedCumulativeTypeParams(dbtClassMixin):
@@ -728,18 +750,25 @@ class UnparsedMetricV2(UnparsedMetricBase):
728750
input_metric: Optional[Union[str, Dict[str, Any]]] = None
729751

730752
# For ratio metrics
731-
numerator: Optional[Union[str, Dict[str, Any]]] = None
732-
denominator: Optional[Union[str, Dict[str, Any]]] = None
753+
numerator: Optional[Union[UnparsedMetricInput, str]] = None
754+
denominator: Optional[Union[UnparsedMetricInput, str]] = None
733755

734756
# For derived metrics
735-
input_metrics: Optional[List[Dict[str, Any]]] = None
757+
input_metrics: Optional[List[Union[UnparsedMetricInput, str]]] = None
736758

737759
# For conversion metrics
738760
entity: Optional[str] = None
739761
calculation: Optional[str] = None
740-
base_metric: Optional[Union[str, Dict[str, Any]]] = None
741-
conversion_metric: Optional[Union[str, Dict[str, Any]]] = None
742-
constant_properties: Optional[List[Dict[str, Any]]] = None
762+
base_metric: Optional[Union[UnparsedMetricInput, str]] = None
763+
conversion_metric: Optional[Union[UnparsedMetricInput, str]] = None
764+
constant_properties: Optional[List[ConstantPropertyInput]] = None
765+
766+
@classmethod
767+
@override
768+
def validate(cls, data):
769+
super().validate(data)
770+
if data["type"] == "simple" and data.get("agg") is None:
771+
raise ValidationError("Simple metrics must have an agg param.")
743772

744773

745774
@dataclass
@@ -754,6 +783,14 @@ def validate(cls, data):
754783
super(UnparsedGroup, cls).validate(data)
755784
if data["owner"].get("name") is None and data["owner"].get("email") is None:
756785
raise ValidationError("Group owner must have at least one of 'name' or 'email'.")
786+
# TODO DI-4413: the following are not strictly necessary (they will be handled
787+
# in dsi validation), but they would be a better user experience
788+
# if we did it at parse time.
789+
# TODO: validate that conversion metrics have base_metric, conversion_metric, and entity
790+
# TODO: validate that cumulative metrics have all required inputs here
791+
# TODO: validate that derived metrics have all required inputs here
792+
# TODO: validate that ratio metrics have all required inputs here
793+
# TODO: validate that simple metrics have all required inputs here
757794

758795

759796
@dataclass
@@ -790,6 +827,11 @@ class UnparsedNonAdditiveDimension(dbtClassMixin):
790827
window_groupings: List[str] = field(default_factory=list)
791828

792829

830+
class PercentileType(str, Enum):
831+
DISCRETE = "discrete"
832+
CONTINUOUS = "continuous"
833+
834+
793835
@dataclass
794836
class UnparsedMeasure(dbtClassMixin):
795837
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)