Skip to content

Commit

Permalink
Validate granularities in where filters
Browse files Browse the repository at this point in the history
  • Loading branch information
courtneyholcomb committed Sep 26, 2024
1 parent 71754db commit 9a83ff5
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 4 deletions.
73 changes: 69 additions & 4 deletions dbt_semantic_interfaces/validations/metrics.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import traceback
from typing import Dict, Generic, List, Optional, Sequence, Set
from typing import Dict, Generic, List, Optional, Sequence, Set, Tuple

from dbt_semantic_interfaces.call_parameter_sets import FilterCallParameterSets
from dbt_semantic_interfaces.errors import ParsingException
from dbt_semantic_interfaces.implementations.metric import (
PydanticMetric,
Expand Down Expand Up @@ -262,11 +263,37 @@ def validate_manifest(semantic_manifest: SemanticManifestT) -> Sequence[Validati
class WhereFiltersAreParseable(SemanticManifestValidationRule[SemanticManifestT], Generic[SemanticManifestT]):
"""Validates that all Metric WhereFilters are parseable."""

@staticmethod
def _validate_time_granularity_names(
context: MetricContext,
filter_expression_parameter_sets: Sequence[Tuple[str, FilterCallParameterSets]],
custom_granularity_names: List[str],
) -> Sequence[ValidationIssue]:
issues: List[ValidationIssue] = []

valid_granularity_names = [
standard_granularity.name for standard_granularity in TimeGranularity
] + custom_granularity_names
for _, parameter_set in filter_expression_parameter_sets:
for time_dim_call_parameter_set in parameter_set.time_dimension_call_parameter_sets:
if not time_dim_call_parameter_set.time_granularity_name:
continue
if time_dim_call_parameter_set.time_granularity_name not in valid_granularity_names:
issues.append(
ValidationError(
context=context,
message=f"Filter for metric `{context.metric.metric_name}` is not valid. "
f"`{time_dim_call_parameter_set.time_granularity_name}` is not a valid granularity name. "
f"Valid granularity options: {valid_granularity_names}",
)
)
return issues

@staticmethod
@validate_safely(
whats_being_done="running model validation ensuring a metric's filter properties are configured properly"
)
def _validate_metric(metric: Metric) -> Sequence[ValidationIssue]: # noqa: D
def _validate_metric(metric: Metric, custom_granularity_names: List[str]) -> Sequence[ValidationIssue]: # noqa: D
issues: List[ValidationIssue] = []
context = MetricContext(
file_context=FileContext.from_metadata(metadata=metric.metadata),
Expand All @@ -287,6 +314,12 @@ def _validate_metric(metric: Metric) -> Sequence[ValidationIssue]: # noqa: D
},
)
)
else:
issues += WhereFiltersAreParseable._validate_time_granularity_names(
context=context,
filter_expression_parameter_sets=metric.filter.filter_expression_parameter_sets,
custom_granularity_names=custom_granularity_names,
)

if metric.type_params:
measure = metric.type_params.measure
Expand All @@ -305,6 +338,12 @@ def _validate_metric(metric: Metric) -> Sequence[ValidationIssue]: # noqa: D
},
)
)
else:
issues += WhereFiltersAreParseable._validate_time_granularity_names(
context=context,
filter_expression_parameter_sets=measure.filter.filter_expression_parameter_sets,
custom_granularity_names=custom_granularity_names,
)

numerator = metric.type_params.numerator
if numerator is not None and numerator.filter is not None:
Expand All @@ -321,6 +360,12 @@ def _validate_metric(metric: Metric) -> Sequence[ValidationIssue]: # noqa: D
},
)
)
else:
issues += WhereFiltersAreParseable._validate_time_granularity_names(
context=context,
filter_expression_parameter_sets=numerator.filter.filter_expression_parameter_sets,
custom_granularity_names=custom_granularity_names,
)

denominator = metric.type_params.denominator
if denominator is not None and denominator.filter is not None:
Expand All @@ -337,6 +382,12 @@ def _validate_metric(metric: Metric) -> Sequence[ValidationIssue]: # noqa: D
},
)
)
else:
issues += WhereFiltersAreParseable._validate_time_granularity_names(
context=context,
filter_expression_parameter_sets=denominator.filter.filter_expression_parameter_sets,
custom_granularity_names=custom_granularity_names,
)

for input_metric in metric.type_params.metrics or []:
if input_metric.filter is not None:
Expand All @@ -354,15 +405,29 @@ def _validate_metric(metric: Metric) -> Sequence[ValidationIssue]: # noqa: D
},
)
)
else:
issues += WhereFiltersAreParseable._validate_time_granularity_names(
context=context,
filter_expression_parameter_sets=input_metric.filter.filter_expression_parameter_sets,
custom_granularity_names=custom_granularity_names,
)

# TODO: Are saved query filters being validated? Task: SL-2932
return issues

@staticmethod
@validate_safely(whats_being_done="running manifest validation ensuring all metric where filters are parseable")
def validate_manifest(semantic_manifest: SemanticManifestT) -> Sequence[ValidationIssue]: # noqa: D
issues: List[ValidationIssue] = []

custom_granularity_names = [
granularity.name
for time_spine in semantic_manifest.project_configuration.time_spines
for granularity in time_spine.custom_granularities
]
for metric in semantic_manifest.metrics or []:
issues += WhereFiltersAreParseable._validate_metric(metric)
issues += WhereFiltersAreParseable._validate_metric(
metric=metric, custom_granularity_names=custom_granularity_names
)
return issues


Expand Down
21 changes: 21 additions & 0 deletions tests/validations/test_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,27 @@ def test_where_filter_validations_bad_input_metric_filter( # noqa: D
validator.checked_validations(manifest)


def test_where_filter_validations_invalid_granularity( # noqa: D
simple_semantic_manifest__with_primary_transforms: PydanticSemanticManifest,
) -> None:
manifest = deepcopy(simple_semantic_manifest__with_primary_transforms)

metric, _ = find_metric_with(
manifest,
lambda metric: metric.type_params is not None
and metric.type_params.metrics is not None
and len(metric.type_params.metrics) > 0,
)
assert metric.type_params.metrics is not None
input_metric = metric.type_params.metrics[0]
input_metric.filter = PydanticWhereFilterIntersection(
where_filters=[PydanticWhereFilter(where_sql_template="{{ TimeDimension('metric_time', 'cool') }}")]
)
validator = SemanticManifestValidator[PydanticSemanticManifest]([WhereFiltersAreParseable()])
with pytest.raises(SemanticManifestValidationException, match="`cool` is not a valid granularity name"):
validator.checked_validations(manifest)


def test_conversion_metrics() -> None: # noqa: D
base_measure_name = "base_measure"
conversion_measure_name = "conversion_measure"
Expand Down

0 comments on commit 9a83ff5

Please sign in to comment.