From 4ada650a6df74ba279a97a6b0da908d775cc8c5a Mon Sep 17 00:00:00 2001 From: Jan Kadlec Date: Tue, 19 May 2026 17:24:42 +0200 Subject: [PATCH] feat(gooddata-sdk): add dimensionality support to MetricValueFilter Adds the optional `dimensionality` argument to `MetricValueFilter` and `CompoundMetricValueFilter`, matching the field already available on `RankingFilter`. The API client models for `ComparisonMeasureValueFilter`/`RangeMeasureValueFilter`/`CompoundMeasureValueFilter` already expose this field, so no client regeneration is required. The compute-to-SDK converter is updated to round-trip the field for all three measure value filter variants; a shared `_extract_dimensionality` helper replaces the inline cast previously used by `RankingFilter`. End-to-end verified against a local GoodData stack: with `dimensionality=[region]` the same `orders > 50` filter that returns 0 rows when applied per `(region, customer)` cell returns 884 rows when evaluated at the region-aggregation level, confirming the field is honored by the backend. JIRA: PSDK-231 risk: low --- .../compute/compute_to_sdk_converter.py | 21 +++--- .../src/gooddata_sdk/compute/model/filter.py | 18 ++++- .../compute/test_compute_to_sdk_converter.py | 72 +++++++++++++++++++ ...sionality_and_treat_nulls_as.snapshot.json | 15 ++++ ...mensionality_using_local_ids.snapshot.json | 17 +++++ ...ensionality_using_mix_of_ids.snapshot.json | 20 ++++++ ...e_filter_with_dimensionality.snapshot.json | 18 +++++ .../test_compound_metric_value_filter.py | 62 ++++++++++++++++ .../compute_model/test_metric_value_filter.py | 41 +++++++++++ 9 files changed, 274 insertions(+), 10 deletions(-) create mode 100644 packages/gooddata-sdk/tests/compute_model/metric_value_filter/comparison_filter_with_dimensionality_and_treat_nulls_as.snapshot.json create mode 100644 packages/gooddata-sdk/tests/compute_model/metric_value_filter/comparison_filter_with_dimensionality_using_local_ids.snapshot.json create mode 100644 packages/gooddata-sdk/tests/compute_model/metric_value_filter/comparison_filter_with_dimensionality_using_mix_of_ids.snapshot.json create mode 100644 packages/gooddata-sdk/tests/compute_model/metric_value_filter/range_filter_with_dimensionality.snapshot.json diff --git a/packages/gooddata-sdk/src/gooddata_sdk/compute/compute_to_sdk_converter.py b/packages/gooddata-sdk/src/gooddata_sdk/compute/compute_to_sdk_converter.py index c1e2a0345..3bc1be7f6 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/compute/compute_to_sdk_converter.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/compute/compute_to_sdk_converter.py @@ -31,6 +31,14 @@ from gooddata_sdk.utils import ref_extract, ref_extract_obj_id +def _extract_dimensionality(f: dict[str, Any]) -> list[Union[str, ObjId, Attribute, Metric]] | None: + # mypy is unable to automatically convert Union[str, ObjId] to Union[str, ObjId, Attribute, Metric] + # so use explicit cast here + if "dimensionality" not in f: + return None + return [cast(Union[str, ObjId, Attribute, Metric], ref_extract(a)) for a in f["dimensionality"]] + + class ComputeToSdkConverter: """ Provides functions to convert Compute API model objects represented as dictionaries to the SDK Compute model. @@ -142,6 +150,7 @@ def convert_filter(filter_dict: dict[str, Any]) -> Filter: operator=f["operator"], values=f["value"], treat_nulls_as=f.get("treatNullValuesAs"), + dimensionality=_extract_dimensionality(f), ) if "rangeMeasureValueFilter" in filter_dict: @@ -152,6 +161,7 @@ def convert_filter(filter_dict: dict[str, Any]) -> Filter: operator=f["operator"], values=(f["from"], f["to"]), treat_nulls_as=f.get("treatNullValuesAs"), + dimensionality=_extract_dimensionality(f), ) if "compoundMeasureValueFilter" in filter_dict: @@ -174,22 +184,15 @@ def convert_filter(filter_dict: dict[str, Any]) -> Filter: metric=ref_extract(f["measure"]), conditions=conditions, treat_nulls_as=f.get("treatNullValuesAs"), + dimensionality=_extract_dimensionality(f), ) if "rankingFilter" in filter_dict: f = filter_dict["rankingFilter"] - # mypy is unable to automatically convert Union[str, ObjId] to Union[str, ObjId, Attribute, Metric] - # so use explicit cast here - dimensionality = ( - [cast(Union[str, ObjId, Attribute, Metric], ref_extract(a)) for a in f["dimensionality"]] - if "dimensionality" in f - else None - ) - return RankingFilter( metrics=[ref_extract(m) for m in f["measures"]], - dimensionality=dimensionality, + dimensionality=_extract_dimensionality(f), operator=f["operator"], value=f["value"], ) diff --git a/packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py b/packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py index 94171f156..e63fee799 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py @@ -586,6 +586,7 @@ def __init__( operator: str, values: Union[float, int, tuple[float, float]], treat_nulls_as: Union[float, None] = None, + dimensionality: list[Union[str, ObjId, Attribute, Metric]] | None = None, ) -> None: super().__init__() @@ -611,6 +612,7 @@ def __init__( self._metric = _extract_id_or_local_id(metric) self._operator = operator self._treat_nulls_as = treat_nulls_as + self._dimensionality = [_extract_id_or_local_id(d) for d in dimensionality] if dimensionality else None @property def metric(self) -> Union[ObjId, str]: @@ -632,19 +634,25 @@ def values(self) -> Union[tuple[float], tuple[float, float]]: def treat_nulls_as(self) -> Union[float, None]: return self._treat_nulls_as + @property + def dimensionality(self) -> list[Union[ObjId, str]] | None: + return self._dimensionality + def is_noop(self) -> bool: return False def as_api_model(self) -> Union[afm_models.ComparisonMeasureValueFilter, afm_models.RangeMeasureValueFilter]: measure = _to_identifier(self._metric) - kwargs = dict( + kwargs: dict[str, Any] = dict( measure=measure, operator=self.operator, _check_type=False, ) if self.treat_nulls_as is not None: kwargs["treat_null_values_as"] = self.treat_nulls_as + if self._dimensionality is not None: + kwargs["dimensionality"] = [_to_identifier(d) for d in self._dimensionality] if _METRIC_VALUE_FILTER_OPERATORS[self.operator] == "comparison": kwargs["value"] = self.values[0] @@ -727,11 +735,13 @@ def __init__( metric: Union[ObjId, str, Metric], conditions: list[MetricValueCondition], treat_nulls_as: Union[float, None] = None, + dimensionality: list[Union[str, ObjId, Attribute, Metric]] | None = None, ) -> None: super().__init__() self._metric = _extract_id_or_local_id(metric) self._conditions = conditions self._treat_nulls_as = treat_nulls_as + self._dimensionality = [_extract_id_or_local_id(d) for d in dimensionality] if dimensionality else None @property def metric(self) -> Union[ObjId, str]: @@ -745,6 +755,10 @@ def conditions(self) -> list[MetricValueCondition]: def treat_nulls_as(self) -> Union[float, None]: return self._treat_nulls_as + @property + def dimensionality(self) -> list[Union[ObjId, str]] | None: + return self._dimensionality + def is_noop(self) -> bool: return len(self.conditions) == 0 @@ -758,6 +772,8 @@ def as_api_model(self) -> afm_models.CompoundMeasureValueFilter: ) if self.treat_nulls_as is not None: kwargs["treat_null_values_as"] = self.treat_nulls_as + if self._dimensionality is not None: + kwargs["dimensionality"] = [_to_identifier(d) for d in self._dimensionality] body = CompoundMeasureValueFilterBody(**kwargs) return afm_models.CompoundMeasureValueFilter(body, _check_type=False) diff --git a/packages/gooddata-sdk/tests/compute/test_compute_to_sdk_converter.py b/packages/gooddata-sdk/tests/compute/test_compute_to_sdk_converter.py index d8794285e..f7249095e 100644 --- a/packages/gooddata-sdk/tests/compute/test_compute_to_sdk_converter.py +++ b/packages/gooddata-sdk/tests/compute/test_compute_to_sdk_converter.py @@ -13,6 +13,7 @@ MetricValueFilter, MetricValueRangeCondition, NegativeAttributeFilter, + ObjId, PopDateMetric, PopDatesetMetric, PositiveAttributeFilter, @@ -270,6 +271,77 @@ def test_compound_measure_value_filter_conversion(): assert result.conditions[1].to_value == 20 +def test_comparison_measure_value_filter_with_dimensionality_conversion(): + filter_dict = json.loads( + """ + { + "comparisonMeasureValueFilter": { + "measure": { "localIdentifier": "measureLocalId" }, + "operator": "GREATER_THAN", + "value": 100, + "dimensionality": [ + { "localIdentifier": "attributeLocalId" }, + { "identifier": { "id": "label.id", "type": "label" } } + ] + } + } + """ + ) + + result = ComputeToSdkConverter.convert_filter(filter_dict) + + assert isinstance(result, MetricValueFilter) + assert result.dimensionality is not None + assert result.dimensionality[0] == "attributeLocalId" + assert result.dimensionality[1] == ObjId(type="label", id="label.id") + + +def test_range_measure_value_filter_with_dimensionality_conversion(): + filter_dict = json.loads( + """ + { + "rangeMeasureValueFilter": { + "measure": { "localIdentifier": "measureLocalId" }, + "operator": "BETWEEN", + "from": 100, + "to": 200, + "dimensionality": [ + { "localIdentifier": "attributeLocalId" } + ] + } + } + """ + ) + + result = ComputeToSdkConverter.convert_filter(filter_dict) + + assert isinstance(result, MetricValueFilter) + assert result.dimensionality == ["attributeLocalId"] + + +def test_compound_measure_value_filter_with_dimensionality_conversion(): + filter_dict = json.loads( + """ + { + "compoundMeasureValueFilter": { + "measure": { "localIdentifier": "measureLocalId" }, + "conditions": [ + { "comparison": { "operator": "GREATER_THAN", "value": 100 } } + ], + "dimensionality": [ + { "localIdentifier": "attributeLocalId" } + ] + } + } + """ + ) + + result = ComputeToSdkConverter.convert_filter(filter_dict) + + assert isinstance(result, CompoundMetricValueFilter) + assert result.dimensionality == ["attributeLocalId"] + + def test_ranking_filter_conversion(): filter_dict = json.loads( """ diff --git a/packages/gooddata-sdk/tests/compute_model/metric_value_filter/comparison_filter_with_dimensionality_and_treat_nulls_as.snapshot.json b/packages/gooddata-sdk/tests/compute_model/metric_value_filter/comparison_filter_with_dimensionality_and_treat_nulls_as.snapshot.json new file mode 100644 index 000000000..2a30b70da --- /dev/null +++ b/packages/gooddata-sdk/tests/compute_model/metric_value_filter/comparison_filter_with_dimensionality_and_treat_nulls_as.snapshot.json @@ -0,0 +1,15 @@ +{ + "comparison_measure_value_filter": { + "dimensionality": [ + { + "local_identifier": "local_id3" + } + ], + "measure": { + "local_identifier": "local_id1" + }, + "operator": "GREATER_THAN", + "treat_null_values_as": 0, + "value": 10.0 + } +} diff --git a/packages/gooddata-sdk/tests/compute_model/metric_value_filter/comparison_filter_with_dimensionality_using_local_ids.snapshot.json b/packages/gooddata-sdk/tests/compute_model/metric_value_filter/comparison_filter_with_dimensionality_using_local_ids.snapshot.json new file mode 100644 index 000000000..1f2d01f22 --- /dev/null +++ b/packages/gooddata-sdk/tests/compute_model/metric_value_filter/comparison_filter_with_dimensionality_using_local_ids.snapshot.json @@ -0,0 +1,17 @@ +{ + "comparison_measure_value_filter": { + "dimensionality": [ + { + "local_identifier": "local_id3" + }, + { + "local_identifier": "local_id4" + } + ], + "measure": { + "local_identifier": "local_id1" + }, + "operator": "GREATER_THAN", + "value": 10.0 + } +} diff --git a/packages/gooddata-sdk/tests/compute_model/metric_value_filter/comparison_filter_with_dimensionality_using_mix_of_ids.snapshot.json b/packages/gooddata-sdk/tests/compute_model/metric_value_filter/comparison_filter_with_dimensionality_using_mix_of_ids.snapshot.json new file mode 100644 index 000000000..c5a7d6a4a --- /dev/null +++ b/packages/gooddata-sdk/tests/compute_model/metric_value_filter/comparison_filter_with_dimensionality_using_mix_of_ids.snapshot.json @@ -0,0 +1,20 @@ +{ + "comparison_measure_value_filter": { + "dimensionality": [ + { + "local_identifier": "local_id3" + }, + { + "identifier": { + "id": "label.id", + "type": "label" + } + } + ], + "measure": { + "local_identifier": "local_id1" + }, + "operator": "GREATER_THAN", + "value": 10.0 + } +} diff --git a/packages/gooddata-sdk/tests/compute_model/metric_value_filter/range_filter_with_dimensionality.snapshot.json b/packages/gooddata-sdk/tests/compute_model/metric_value_filter/range_filter_with_dimensionality.snapshot.json new file mode 100644 index 000000000..ec04ffbad --- /dev/null +++ b/packages/gooddata-sdk/tests/compute_model/metric_value_filter/range_filter_with_dimensionality.snapshot.json @@ -0,0 +1,18 @@ +{ + "range_measure_value_filter": { + "_from": 2, + "dimensionality": [ + { + "local_identifier": "local_id3" + }, + { + "local_identifier": "local_id4" + } + ], + "measure": { + "local_identifier": "local_id1" + }, + "operator": "BETWEEN", + "to": 3 + } +} diff --git a/packages/gooddata-sdk/tests/compute_model/test_compound_metric_value_filter.py b/packages/gooddata-sdk/tests/compute_model/test_compound_metric_value_filter.py index 783157dbf..199449f91 100644 --- a/packages/gooddata-sdk/tests/compute_model/test_compound_metric_value_filter.py +++ b/packages/gooddata-sdk/tests/compute_model/test_compound_metric_value_filter.py @@ -2,6 +2,7 @@ from __future__ import annotations from gooddata_sdk import ( + Attribute, CompoundMetricValueFilter, MetricValueComparisonCondition, MetricValueRangeCondition, @@ -35,3 +36,64 @@ def test_compound_metric_value_filter_to_api_model(): def test_compound_metric_value_filter_noop_when_no_conditions(): f = CompoundMetricValueFilter(metric=ObjId(type="metric", id="metric.id"), conditions=[]) assert f.is_noop() is True + + +def test_compound_metric_value_filter_with_dimensionality_to_api_model(): + f = CompoundMetricValueFilter( + metric="local_id1", + conditions=[ + MetricValueComparisonCondition(operator="GREATER_THAN", value=10), + ], + dimensionality=["local_id3", Attribute(local_id="local_id4", label="label.id")], + ) + + assert f.dimensionality == ["local_id3", "local_id4"] + assert f.as_api_model().to_dict() == { + "compound_measure_value_filter": { + "conditions": [ + {"comparison": {"operator": "GREATER_THAN", "value": 10.0}}, + ], + "dimensionality": [ + {"local_identifier": "local_id3"}, + {"local_identifier": "local_id4"}, + ], + "measure": {"local_identifier": "local_id1"}, + } + } + + +def test_compound_metric_value_filter_with_dimensionality_mixed_ids(): + f = CompoundMetricValueFilter( + metric="local_id1", + conditions=[ + MetricValueComparisonCondition(operator="GREATER_THAN", value=10), + ], + dimensionality=["local_id3", ObjId(type="label", id="label.id")], + treat_nulls_as=0, + ) + + assert f.as_api_model().to_dict() == { + "compound_measure_value_filter": { + "conditions": [ + {"comparison": {"operator": "GREATER_THAN", "value": 10.0}}, + ], + "dimensionality": [ + {"local_identifier": "local_id3"}, + {"identifier": {"id": "label.id", "type": "label"}}, + ], + "measure": {"local_identifier": "local_id1"}, + "treat_null_values_as": 0, + } + } + + +def test_compound_metric_value_filter_without_dimensionality_omits_field(): + f = CompoundMetricValueFilter( + metric="local_id1", + conditions=[ + MetricValueComparisonCondition(operator="GREATER_THAN", value=10), + ], + ) + + assert f.dimensionality is None + assert "dimensionality" not in f.as_api_model().to_dict()["compound_measure_value_filter"] diff --git a/packages/gooddata-sdk/tests/compute_model/test_metric_value_filter.py b/packages/gooddata-sdk/tests/compute_model/test_metric_value_filter.py index 24e10afab..32653a5d0 100644 --- a/packages/gooddata-sdk/tests/compute_model/test_metric_value_filter.py +++ b/packages/gooddata-sdk/tests/compute_model/test_metric_value_filter.py @@ -56,6 +56,47 @@ def _scenario_to_snapshot_name(scenario: str): MetricValueFilter(metric="local_id1", operator="BETWEEN", values=(2, 3), treat_nulls_as=1), "Local ID: between 2 - 3", ], + [ + "comparison filter with dimensionality using local ids", + MetricValueFilter( + metric="local_id1", + operator="GREATER_THAN", + values=10, + dimensionality=["local_id3", _attribute], + ), + "Local ID: > 10.0", + ], + [ + "comparison filter with dimensionality using mix of ids", + MetricValueFilter( + metric="local_id1", + operator="GREATER_THAN", + values=10, + dimensionality=["local_id3", ObjId(type="label", id="label.id")], + ), + "Local ID: > 10.0", + ], + [ + "range filter with dimensionality", + MetricValueFilter( + metric="local_id1", + operator="BETWEEN", + values=(2, 3), + dimensionality=["local_id3", _attribute], + ), + "Local ID: between 2 - 3", + ], + [ + "comparison filter with dimensionality and treat nulls as", + MetricValueFilter( + metric="local_id1", + operator="GREATER_THAN", + values=10, + treat_nulls_as=0, + dimensionality=["local_id3"], + ), + "Local ID: > 10.0", + ], ]