diff --git a/dbt_semantic_interfaces/implementations/saved_query.py b/dbt_semantic_interfaces/implementations/saved_query.py index b988c314..b84da1f3 100644 --- a/dbt_semantic_interfaces/implementations/saved_query.py +++ b/dbt_semantic_interfaces/implementations/saved_query.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import List, Optional, Sequence +from typing import List, Optional from typing_extensions import override @@ -30,11 +30,3 @@ def _implements_protocol(self) -> SavedQuery: description: Optional[str] = None metadata: Optional[PydanticMetadata] = None - - @property - def metric_names(self) -> Sequence[str]: # noqa: D - return self.metrics - - @property - def group_by_item_names(self) -> Sequence[str]: # noqa: D - return self.group_bys diff --git a/dbt_semantic_interfaces/protocols/saved_query.py b/dbt_semantic_interfaces/protocols/saved_query.py index 1f350f3a..0547ba74 100644 --- a/dbt_semantic_interfaces/protocols/saved_query.py +++ b/dbt_semantic_interfaces/protocols/saved_query.py @@ -25,12 +25,12 @@ def description(self) -> Optional[str]: # noqa: D @property @abstractmethod - def metric_names(self) -> Sequence[str]: # noqa: D + def metrics(self) -> Sequence[str]: # noqa: D pass @property @abstractmethod - def group_by_item_names(self) -> Sequence[str]: # noqa: D + def group_bys(self) -> Sequence[str]: # noqa: D pass @property diff --git a/dbt_semantic_interfaces/validations/saved_query.py b/dbt_semantic_interfaces/validations/saved_query.py index 492e3f74..f66bba43 100644 --- a/dbt_semantic_interfaces/validations/saved_query.py +++ b/dbt_semantic_interfaces/validations/saved_query.py @@ -2,7 +2,9 @@ import traceback from typing import Generic, List, Sequence, Set -from dbt_semantic_interfaces.protocols import SemanticManifestT, SemanticModel +from dbt_semantic_interfaces.naming.dundered import DunderedNameFormatter +from dbt_semantic_interfaces.naming.keywords import METRIC_TIME_ELEMENT_NAME +from dbt_semantic_interfaces.protocols import SemanticManifestT from dbt_semantic_interfaces.protocols.saved_query import SavedQuery from dbt_semantic_interfaces.validations.validator_helpers import ( FileContext, @@ -29,14 +31,30 @@ class SavedQueryRule(SemanticManifestValidationRule[SemanticManifestT], Generic[ """ @staticmethod - def _model_requires_primary_entity(semantic_model: SemanticModel) -> bool: - return len(semantic_model.dimensions) > 0 + @validate_safely("Validate the group-by field in a saved query.") + def _check_group_bys(valid_group_by_element_names: Set[str], saved_query: SavedQuery) -> Sequence[ValidationIssue]: + issues: List[ValidationIssue] = [] + + for group_by_item_name in saved_query.group_bys: + structured_name = DunderedNameFormatter.parse_name(group_by_item_name) + if structured_name.element_name not in valid_group_by_element_names: + issues.append( + ValidationError( + message=f"`{group_by_item_name}` is not a valid group-by name.", + context=SavedQueryContext( + file_context=FileContext.from_metadata(metadata=saved_query.metadata), + element_type=SavedQueryElementType.GROUP_BY, + element_value=group_by_item_name, + ), + ) + ) + return issues @staticmethod - @validate_safely("Validate a saved query.") - def _check_saved_query(valid_metric_names: Set[str], saved_query: SavedQuery) -> Sequence[ValidationIssue]: + @validate_safely("Validate the metrics field in a saved query.") + def _check_metrics(valid_metric_names: Set[str], saved_query: SavedQuery) -> Sequence[ValidationIssue]: issues: List[ValidationIssue] = [] - for metric_name in saved_query.metric_names: + for metric_name in saved_query.metrics: if metric_name not in valid_metric_names: issues.append( ValidationError( @@ -48,7 +66,12 @@ def _check_saved_query(valid_metric_names: Set[str], saved_query: SavedQuery) -> ), ) ) + return issues + @staticmethod + @validate_safely("Validate the where field in a saved query.") + def _check_where(saved_query: SavedQuery) -> Sequence[ValidationIssue]: + issues: List[ValidationIssue] = [] for where_filter in saved_query.where: try: where_filter.call_parameter_sets @@ -75,10 +98,22 @@ def _check_saved_query(valid_metric_names: Set[str], saved_query: SavedQuery) -> def validate_manifest(semantic_manifest: SemanticManifestT) -> Sequence[ValidationIssue]: # noqa: D issues: List[ValidationIssue] = [] valid_metric_names = {metric.name for metric in semantic_manifest.metrics} + valid_group_by_element_names = {METRIC_TIME_ELEMENT_NAME} + for semantic_model in semantic_manifest.semantic_models: + for dimension in semantic_model.dimensions: + valid_group_by_element_names.add(dimension.name) + for entity in semantic_model.entities: + valid_group_by_element_names.add(entity.name) + for saved_query in semantic_manifest.saved_queries: - issues += SavedQueryRule._check_saved_query( + issues += SavedQueryRule._check_metrics( valid_metric_names=valid_metric_names, saved_query=saved_query, ) + issues += SavedQueryRule._check_group_bys( + valid_group_by_element_names=valid_group_by_element_names, + saved_query=saved_query, + ) + issues += SavedQueryRule._check_where(saved_query=saved_query) return issues diff --git a/tests/validations/test_saved_query.py b/tests/validations/test_saved_query.py index 7c4c5345..0f704ede 100644 --- a/tests/validations/test_saved_query.py +++ b/tests/validations/test_saved_query.py @@ -44,6 +44,7 @@ def test_invalid_metric_in_saved_query( # noqa: D description="Example description.", metrics=["invalid_metric"], group_bys=["booking__is_instant"], + where=[PydanticWhereFilter(where_sql_template="{{ Dimension('booking__is_instant') }}")], ), ] @@ -72,3 +73,24 @@ def test_invalid_where_in_saved_query( # noqa: D manifest_validator.validate_semantic_manifest(manifest), "trying to parse a filter in saved query", ) + + +def test_invalid_group_by_in_saved_query( # noqa: D + simple_semantic_manifest__with_primary_transforms: PydanticSemanticManifest, +) -> None: + manifest = copy.deepcopy(simple_semantic_manifest__with_primary_transforms) + manifest.saved_queries = [ + PydanticSavedQuery( + name="Example Saved Query", + description="Example description.", + metrics=["bookings"], + group_bys=["invalid_dimension"], + where=[PydanticWhereFilter(where_sql_template="{{ Dimension('booking__is_instant') }}")], + ), + ] + + manifest_validator = SemanticManifestValidator[PydanticSemanticManifest]([SavedQueryRule()]) + check_only_one_error_with_message( + manifest_validator.validate_semantic_manifest(manifest), + "is not a valid group-by name.", + )