Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add validations for time spines #344

Merged
merged 7 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ class SemanticManifestNodeType(ExtendedEnum):
METRIC = "metric"
SAVED_QUERY = "saved_query"
SEMANTIC_MODEL = "semantic_model"
TIME_SPINE = "time_spine"
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
86 changes: 65 additions & 21 deletions dbt_semantic_interfaces/validations/unique_valid_name.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import enum
import re
from typing import Dict, Generic, List, Optional, Sequence, Tuple, Union
from typing import Dict, Generic, List, Optional, Sequence, Set, Tuple, Union

from dbt_semantic_interfaces.enum_extension import assert_values_exhausted
from dbt_semantic_interfaces.protocols import (
Expand Down Expand Up @@ -102,12 +102,14 @@ def check_valid_name(name: str, context: Optional[ValidationContext] = None) ->

@staticmethod
@validate_safely(whats_being_done="checking semantic model sub element names are unique")
def _validate_semantic_model_elements(semantic_model: SemanticModel) -> List[ValidationIssue]:
def _validate_semantic_model_elements_and_time_spines(semantic_manifest: SemanticManifest) -> List[ValidationIssue]:
issues: List[ValidationIssue] = []
element_info_tuples: List[Tuple[ElementReference, str, ValidationContext]] = []
custom_granularity_restricted_names_and_types: Dict[str, str] = {}

if semantic_model.measures:
for semantic_model in semantic_manifest.semantic_models:
element_info_tuples: List[Tuple[ElementReference, str, ValidationContext]] = []
for measure in semantic_model.measures:
custom_granularity_restricted_names_and_types[measure.name] = SemanticModelElementType.MEASURE.value
element_info_tuples.append(
(
measure.reference,
Expand All @@ -121,8 +123,8 @@ def _validate_semantic_model_elements(semantic_model: SemanticModel) -> List[Val
),
)
)
if semantic_model.entities:
for entity in semantic_model.entities:
custom_granularity_restricted_names_and_types[entity.name] = SemanticModelElementType.ENTITY.value
element_info_tuples.append(
(
entity.reference,
Expand All @@ -136,8 +138,8 @@ def _validate_semantic_model_elements(semantic_model: SemanticModel) -> List[Val
),
)
)
if semantic_model.dimensions:
for dimension in semantic_model.dimensions:
custom_granularity_restricted_names_and_types[dimension.name] = SemanticModelElementType.DIMENSION.value
element_info_tuples.append(
(
dimension.reference,
Expand All @@ -151,22 +153,66 @@ def _validate_semantic_model_elements(semantic_model: SemanticModel) -> List[Val
),
)
)
name_to_type: Dict[ElementReference, str] = {}

for name, _type, context in element_info_tuples:
if name in name_to_type:
issues.append(
ValidationError(
context=context,
message=f"In semantic model `{semantic_model.name}`, can't use name `{name.element_name}` for "
f"a {_type} when it was already used for a {name_to_type[name]}",
# Verify uniqueness for this type within each semantic model
semantic_model_element_reference_to_type: Dict[ElementReference, str] = {}
for reference, _type, context in element_info_tuples:
if reference in semantic_model_element_reference_to_type:
issues.append(
ValidationError(
context=context,
message=f"In semantic model `{semantic_model.name}`, can't use name "
f"`{reference.element_name}` for a {_type} when it was already used for a "
f"{semantic_model_element_reference_to_type[reference]}",
)
)
else:
semantic_model_element_reference_to_type[reference] = _type

for name, _, context in element_info_tuples:
issues += UniqueAndValidNameRule.check_valid_name(name=name.element_name, context=context)

for metric in semantic_manifest.metrics:
custom_granularity_restricted_names_and_types[metric.name] = SemanticManifestNodeType.METRIC.value
for semantic_model in semantic_manifest.semantic_models:
custom_granularity_restricted_names_and_types[
semantic_model.name
] = SemanticManifestNodeType.SEMANTIC_MODEL.value

# Verify custom granularity names are unique across relevant elements
seen_custom_granularity_names: Set[str] = set()
duplicate_custom_granularity_names: Set[str] = set()
for time_spine in semantic_manifest.project_configuration.time_spines:
time_spine_context = ValidationIssueContext(
file_context=FileContext(),
object_name=time_spine.node_relation.alias,
object_type=SemanticManifestNodeType.TIME_SPINE.value,
)
for custom_granularity in time_spine.custom_granularities:
issues += UniqueAndValidNameRule.check_valid_name(
name=custom_granularity.name, context=time_spine_context
)
else:
name_to_type[name] = _type
if custom_granularity.name in custom_granularity_restricted_names_and_types:
issues.append(
ValidationError(
context=time_spine_context,
message=f"Can't use name `{custom_granularity.name}` for a custom granularity when it was "
"already used for a "
f"{custom_granularity_restricted_names_and_types[custom_granularity.name]}.",
)
)
if custom_granularity.name in seen_custom_granularity_names:
duplicate_custom_granularity_names.add(custom_granularity.name)
seen_custom_granularity_names.add(custom_granularity.name)

for name, _, context in element_info_tuples:
issues += UniqueAndValidNameRule.check_valid_name(name=name.element_name, context=context)
if duplicate_custom_granularity_names:
issues.append(
ValidationError(
context=time_spine_context,
message=f"Custom granularity names must be unique, but found duplicate custom granularities with "
f"the names {duplicate_custom_granularity_names}.",
)
)

return issues

Expand Down Expand Up @@ -230,9 +276,7 @@ def _validate_top_level_objects(semantic_manifest: SemanticManifest) -> List[Val
def validate_manifest(semantic_manifest: SemanticManifestT) -> Sequence[ValidationIssue]: # noqa: D
issues = []
issues += UniqueAndValidNameRule._validate_top_level_objects(semantic_manifest=semantic_manifest)

for semantic_model in semantic_manifest.semantic_models:
issues += UniqueAndValidNameRule._validate_semantic_model_elements(semantic_model=semantic_model)
issues += UniqueAndValidNameRule._validate_semantic_model_elements_and_time_spines(semantic_manifest)

return issues

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "dbt-semantic-interfaces"
version = "0.7.2.dev0"
version = "0.7.2"
description = 'The shared semantic layer definitions that dbt-core and MetricFlow use'
readme = "README.md"
requires-python = ">=3.8"
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
Loading
Loading