From 519a3fd9e7cdd2273aad07addcbaa3a4423be62b Mon Sep 17 00:00:00 2001 From: Quigley Malcolm Date: Mon, 25 Sep 2023 11:02:47 -0700 Subject: [PATCH] Support null coalescing properties for metric nodes (#8700) * Include 'join_to_timespine` and `fill_nulls_with` in metric fixture * Support `join_to_timespine` and `fill_nulls_with` properties on measure inputs to metrics * Assert new `fill_nulls_with` and `join_to_timespine` properties don't break associated DSI protocol * Add doc for metric null coalescing improvements * Fix unit test for unparsed metric objects The `assert_symmetric` function asserts that dictionaries are mostly equivalent. I say mostly equivalent because it drops keys that are `None`. The issue is that that `join_to_timespine` gets defaulted to `False`, so we have to specify it in the `get_ok_dict` so that they match. --- .changes/unreleased/Features-20230922-150754.yaml | 6 ++++++ core/dbt/contracts/graph/nodes.py | 2 ++ core/dbt/contracts/graph/unparsed.py | 2 ++ core/dbt/parser/schema_yaml_readers.py | 2 ++ tests/functional/metrics/fixtures.py | 2 ++ tests/unit/test_contracts_graph_unparsed.py | 1 + tests/unit/test_semantic_layer_nodes_satisfy_protocols.py | 6 +++++- 7 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 .changes/unreleased/Features-20230922-150754.yaml diff --git a/.changes/unreleased/Features-20230922-150754.yaml b/.changes/unreleased/Features-20230922-150754.yaml new file mode 100644 index 00000000000..6492c3d934a --- /dev/null +++ b/.changes/unreleased/Features-20230922-150754.yaml @@ -0,0 +1,6 @@ +kind: Features +body: Support `fill_nulls_with` and `join_to_timespine` for metric nodes +time: 2023-09-22T15:07:54.981752-07:00 +custom: + Author: QMalcolm + Issue: "8593" diff --git a/core/dbt/contracts/graph/nodes.py b/core/dbt/contracts/graph/nodes.py index 02e2c4c9dca..3aad00901eb 100644 --- a/core/dbt/contracts/graph/nodes.py +++ b/core/dbt/contracts/graph/nodes.py @@ -1411,6 +1411,8 @@ class MetricInputMeasure(dbtClassMixin): name: str filter: Optional[WhereFilter] = None alias: Optional[str] = None + join_to_timespine: bool = False + fill_nulls_with: Optional[int] = None def measure_reference(self) -> MeasureReference: return MeasureReference(element_name=self.name) diff --git a/core/dbt/contracts/graph/unparsed.py b/core/dbt/contracts/graph/unparsed.py index 887359329d7..d06b13faa03 100644 --- a/core/dbt/contracts/graph/unparsed.py +++ b/core/dbt/contracts/graph/unparsed.py @@ -584,6 +584,8 @@ class UnparsedMetricInputMeasure(dbtClassMixin): name: str filter: Optional[str] = None alias: Optional[str] = None + join_to_timespine: bool = False + fill_nulls_with: Optional[int] = None @dataclass diff --git a/core/dbt/parser/schema_yaml_readers.py b/core/dbt/parser/schema_yaml_readers.py index 45f3cfe115b..dddad84c6db 100644 --- a/core/dbt/parser/schema_yaml_readers.py +++ b/core/dbt/parser/schema_yaml_readers.py @@ -178,6 +178,8 @@ def _get_input_measure( name=unparsed_input_measure.name, filter=filter, alias=unparsed_input_measure.alias, + join_to_timespine=unparsed_input_measure.join_to_timespine, + fill_nulls_with=unparsed_input_measure.fill_nulls_with, ) def _get_optional_input_measure( diff --git a/tests/functional/metrics/fixtures.py b/tests/functional/metrics/fixtures.py index 65d61ad74ad..89eeb930043 100644 --- a/tests/functional/metrics/fixtures.py +++ b/tests/functional/metrics/fixtures.py @@ -116,6 +116,8 @@ measure: name: years_tenure filter: "{{ Dimension('id__loves_dbt') }} is true" + join_to_timespine: true + fill_nulls_with: 0 - name: collective_window label: "Collective window" diff --git a/tests/unit/test_contracts_graph_unparsed.py b/tests/unit/test_contracts_graph_unparsed.py index 7f29937a007..fbf23a082c7 100644 --- a/tests/unit/test_contracts_graph_unparsed.py +++ b/tests/unit/test_contracts_graph_unparsed.py @@ -858,6 +858,7 @@ def get_ok_dict(self): "measure": { "name": "customers", "filter": "is_new = true", + "join_to_timespine": False, }, }, "config": {}, diff --git a/tests/unit/test_semantic_layer_nodes_satisfy_protocols.py b/tests/unit/test_semantic_layer_nodes_satisfy_protocols.py index 7325a41da19..85d24797ef1 100644 --- a/tests/unit/test_semantic_layer_nodes_satisfy_protocols.py +++ b/tests/unit/test_semantic_layer_nodes_satisfy_protocols.py @@ -216,7 +216,11 @@ def simple_metric_input_measure() -> MetricInputMeasure: @pytest.fixture(scope="session") def complex_metric_input_measure(where_filter) -> MetricInputMeasure: return MetricInputMeasure( - name="test_complex_metric_input_measure", filter=where_filter, alias="complex_alias" + name="test_complex_metric_input_measure", + filter=where_filter, + alias="complex_alias", + join_to_timespine=True, + fill_nulls_with=0, )