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

Validate granularity names in saved query where filters #359

Merged
merged 7 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 6 additions & 0 deletions .changes/unreleased/Under the Hood-20241023-180425.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: Under the Hood
body: Added validation warnings for invalid time spines in where filters of saved queries.
theyostalservice marked this conversation as resolved.
Show resolved Hide resolved
time: 2024-10-23T18:04:25.235887-07:00
custom:
Author: theyostalservice
Issue: "360"
46 changes: 44 additions & 2 deletions dbt_semantic_interfaces/validations/saved_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@
)
from dbt_semantic_interfaces.protocols import SemanticManifestT
from dbt_semantic_interfaces.protocols.saved_query import SavedQuery
from dbt_semantic_interfaces.type_enums.time_granularity import TimeGranularity
from dbt_semantic_interfaces.validations.validator_helpers import (
FileContext,
SavedQueryContext,
SavedQueryElementType,
SemanticManifestValidationRule,
ValidationError,
ValidationIssue,
ValidationWarning,
generate_exception_issue,
validate_safely,
)
Expand Down Expand Up @@ -114,7 +116,7 @@ def _check_metrics(valid_metric_names: Set[str], saved_query: SavedQuery) -> Seq

@staticmethod
@validate_safely("Validate the where field in a saved query.")
def _check_where(saved_query: SavedQuery) -> Sequence[ValidationIssue]:
def _check_where(saved_query: SavedQuery, custom_granularity_names: list[str]) -> Sequence[ValidationIssue]:
issues: List[ValidationIssue] = []
if saved_query.query_params.where is None:
return issues
Expand All @@ -136,9 +138,44 @@ def _check_where(saved_query: SavedQuery) -> Sequence[ValidationIssue]:
},
)
)
else:
issues += SavedQueryRule._check_where_timespine(saved_query, custom_granularity_names)

return issues

def _check_where_timespine(
theyostalservice marked this conversation as resolved.
Show resolved Hide resolved
theyostalservice marked this conversation as resolved.
Show resolved Hide resolved
theyostalservice marked this conversation as resolved.
Show resolved Hide resolved
saved_query: SavedQuery, custom_granularity_names: list[str]
) -> Sequence[ValidationIssue]:
where_param = saved_query.query_params.where
if where_param is None:
return []

issues: List[ValidationIssue] = []

valid_granularity_names = [
standard_granularity.name for standard_granularity in TimeGranularity
] + custom_granularity_names

for where_filter in where_param.where_filters:
for time_dim_call_parameter_set in where_filter.call_parameter_sets.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(
ValidationWarning(
context=SavedQueryContext(
file_context=FileContext.from_metadata(metadata=saved_query.metadata),
element_type=SavedQueryElementType.WHERE,
element_value=where_filter.where_sql_template,
),
# message=f"Filter for metric `{context.metric.metric_name}` is not valid. "
theyostalservice marked this conversation as resolved.
Show resolved Hide resolved
message=f"Filter for saved query `{saved_query.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
def _parse_query_item(
saved_query: SavedQuery,
Expand Down Expand Up @@ -280,6 +317,11 @@ def validate_manifest(semantic_manifest: SemanticManifestT) -> Sequence[Validati
for entity in semantic_model.entities:
valid_group_by_element_names.add(entity.name)

custom_granularity_names = [
granularity.name
for time_spine in semantic_manifest.project_configuration.time_spines
for granularity in time_spine.custom_granularities
]
theyostalservice marked this conversation as resolved.
Show resolved Hide resolved
for saved_query in semantic_manifest.saved_queries:
issues += SavedQueryRule._check_metrics(
valid_metric_names=valid_metric_names,
Expand All @@ -289,7 +331,7 @@ def validate_manifest(semantic_manifest: SemanticManifestT) -> Sequence[Validati
valid_group_by_element_names=valid_group_by_element_names,
saved_query=saved_query,
)
issues += SavedQueryRule._check_where(saved_query)
issues += SavedQueryRule._check_where(saved_query, custom_granularity_names)
issues += SavedQueryRule._check_order_by(saved_query)
issues += SavedQueryRule._check_limit(saved_query)
return issues
Expand Down
43 changes: 43 additions & 0 deletions tests/validations/test_saved_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@ def check_only_one_error_with_message( # noqa: D
} and found_match


def check_only_one_warning_with_message( # noqa: D
theyostalservice marked this conversation as resolved.
Show resolved Hide resolved
results: SemanticManifestValidationResults, target_message: str
) -> None:
assert len(results.warnings) == 1
assert len(results.errors) == 0
assert len(results.future_errors) == 0

found_match = results.warnings[0].message.find(target_message) != -1
# Adding this dict to the assert so that when it does not match, pytest prints the expected and actual values.
assert {
"expected": target_message,
"actual": results.warnings[0].message,
} and found_match


def test_invalid_metric_in_saved_query( # noqa: D
simple_semantic_manifest__with_primary_transforms: PydanticSemanticManifest,
) -> None:
Expand Down Expand Up @@ -87,6 +102,34 @@ def test_invalid_where_in_saved_query( # noqa: D
)


def test_where_filter_validations_invalid_granularity( # noqa: D
theyostalservice marked this conversation as resolved.
Show resolved Hide resolved
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.",
query_params=PydanticSavedQueryQueryParams(
metrics=["bookings"],
group_by=["Dimension('booking__is_instant')"],
where=PydanticWhereFilterIntersection(
where_filters=[
PydanticWhereFilter(where_sql_template="{{ TimeDimension('metric_time', 'cool') }}"),
theyostalservice marked this conversation as resolved.
Show resolved Hide resolved
]
),
),
),
]

manifest_validator = SemanticManifestValidator[PydanticSemanticManifest]([SavedQueryRule()])
check_only_one_warning_with_message(
manifest_validator.validate_semantic_manifest(manifest),
"is not a valid granularity name",
)


def test_invalid_group_by_element_in_saved_query( # noqa: D
simple_semantic_manifest__with_primary_transforms: PydanticSemanticManifest,
) -> None:
Expand Down
Loading