Skip to content

Commit

Permalink
Merge branch 'Add_meta_to_measures,_entities,_and_dimensions' into co…
Browse files Browse the repository at this point in the history
…nsolidate_meta_config
  • Loading branch information
DevonFulcher committed Nov 11, 2024
2 parents 83ff542 + 45a0b52 commit 6275d37
Show file tree
Hide file tree
Showing 23 changed files with 846 additions and 622 deletions.
6 changes: 6 additions & 0 deletions .changes/unreleased/Breaking Changes-20241105-180727.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: Breaking Changes
body: Update PydanticWhereFilter.call_parameter_sets and PydanticWhereFilterIntersection.filter_expression_parameter_sets from property to a method
time: 2024-11-05T18:07:27.325103-05:00
custom:
Author: WilliamDee
Issue: None
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 granularity names in where filters of saved queries.
time: 2024-10-23T18:04:25.235887-07:00
custom:
Author: theyostalservice
Issue: "360"
21 changes: 14 additions & 7 deletions dbt_semantic_interfaces/implementations/filters/where_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import textwrap
import traceback
from typing import Callable, Generator, List, Tuple
from typing import Callable, Generator, List, Sequence, Tuple

from typing_extensions import Self

Expand Down Expand Up @@ -49,9 +49,10 @@ def _from_yaml_value(
else:
raise ValueError(f"Expected input to be of type string, but got type {type(input)} with value: {input}")

@property
def call_parameter_sets(self) -> FilterCallParameterSets: # noqa: D
return WhereFilterParser.parse_call_parameter_sets(self.where_sql_template)
def call_parameter_sets(self, custom_granularity_names: Sequence[str]) -> FilterCallParameterSets: # noqa: D
return WhereFilterParser.parse_call_parameter_sets(
where_sql_template=self.where_sql_template, custom_granularity_names=custom_granularity_names
)


class PydanticWhereFilterIntersection(HashableBaseModel):
Expand Down Expand Up @@ -115,14 +116,20 @@ def _convert_legacy_and_yaml_input(cls, input: PydanticParseableValueType) -> Se
f"or dict but got {type(input)} with value {input}"
)

@property
def filter_expression_parameter_sets(self) -> List[Tuple[str, FilterCallParameterSets]]:
def filter_expression_parameter_sets(
self, custom_granularity_names: Sequence[str]
) -> List[Tuple[str, FilterCallParameterSets]]:
"""Gets the call parameter sets for each filter expression."""
filter_parameter_sets: List[Tuple[str, FilterCallParameterSets]] = []
invalid_filter_expressions: List[Tuple[str, Exception]] = []
for where_filter in self.where_filters:
try:
filter_parameter_sets.append((where_filter.where_sql_template, where_filter.call_parameter_sets))
filter_parameter_sets.append(
(
where_filter.where_sql_template,
where_filter.call_parameter_sets(custom_granularity_names=custom_granularity_names),
)
)
except Exception as e:
invalid_filter_expressions.append((where_filter.where_sql_template, e))

Expand Down
77 changes: 14 additions & 63 deletions dbt_semantic_interfaces/naming/dundered.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import logging
from dataclasses import dataclass
from typing import Optional, Tuple
from typing import Optional, Sequence, Tuple

from dbt_semantic_interfaces.naming.keywords import DUNDER
from dbt_semantic_interfaces.references import EntityReference
Expand All @@ -19,32 +19,32 @@ class StructuredDunderedName:
entity_links: ["listing"]
element_name: "ds"
granularity: TimeGranularity.WEEK
The time granularity is part of legacy query syntax and there are plans to migrate away from this format. As such,
this will not be updated to allow for custom granularity values. This implies that any query paths that push named
parameters through this class will not support a custom grain reference of the form `metric_time__martian_year`,
and users wishing to use their martian year grain will have to explicitly reference it via a separate parameter
instead of gluing it onto the end of the name.
"""

entity_links: Tuple[EntityReference, ...]
element_name: str
time_granularity: Optional[TimeGranularity] = None
time_granularity: Optional[str] = None

@staticmethod
def parse_name(name: str) -> StructuredDunderedName:
def parse_name(name: str, custom_granularity_names: Sequence[str] = ()) -> StructuredDunderedName:
"""Construct from a string like 'listing__ds__month'."""
name_parts = name.split(DUNDER)

# No dunder, e.g. "ds"
if len(name_parts) == 1:
return StructuredDunderedName((), name_parts[0])

associated_granularity = None
granularity: TimeGranularity
associated_granularity: Optional[str] = None
for granularity in TimeGranularity:
if name_parts[-1] == granularity.value:
associated_granularity = granularity
associated_granularity = granularity.value
break

if associated_granularity is None:
for custom_grain in custom_granularity_names:
if name_parts[-1] == custom_grain:
associated_granularity = custom_grain
break

# Has a time granularity
if associated_granularity:
Expand All @@ -69,7 +69,7 @@ def dundered_name(self) -> str:
"""Return the full name form. e.g. ds or listing__ds__month."""
items = [entity_reference.element_name for entity_reference in self.entity_links] + [self.element_name]
if self.time_granularity:
items.append(self.time_granularity.value)
items.append(self.time_granularity)
return DUNDER.join(items)

@property
Expand All @@ -82,7 +82,7 @@ def dundered_name_without_granularity(self) -> str:
@property
def dundered_name_without_entity(self) -> str:
"""Return the name without the entity. e.g. listing__ds__month -> ds__month."""
return DUNDER.join((self.element_name,) + ((self.time_granularity.value,) if self.time_granularity else ()))
return DUNDER.join((self.element_name,) + ((self.time_granularity,) if self.time_granularity else ()))

@property
def entity_prefix(self) -> Optional[str]:
Expand All @@ -91,52 +91,3 @@ def entity_prefix(self) -> Optional[str]:
return DUNDER.join(tuple(entity_reference.element_name for entity_reference in self.entity_links))

return None


class DunderedNameFormatter:
"""Helps to parse names into StructuredDunderedName and vice versa."""

@staticmethod
def parse_name(name: str) -> StructuredDunderedName:
"""Construct from a string like 'listing__ds__month'."""
name_parts = name.split(DUNDER)

# No dunder, e.g. "ds"
if len(name_parts) == 1:
return StructuredDunderedName((), name_parts[0])

associated_granularity = None
granularity: TimeGranularity
for granularity in TimeGranularity:
if name_parts[-1] == granularity.value:
associated_granularity = granularity

# Has a time granularity
if associated_granularity:
# e.g. "ds__month"
if len(name_parts) == 2:
return StructuredDunderedName((), name_parts[0], associated_granularity)
# e.g. "messages__ds__month"
return StructuredDunderedName(
entity_links=tuple(EntityReference(element_name=entity_name) for entity_name in name_parts[:-2]),
element_name=name_parts[-2],
time_granularity=associated_granularity,
)
# e.g. "messages__ds"
else:
return StructuredDunderedName(
entity_links=tuple(EntityReference(element_name=entity_name) for entity_name in name_parts[:-1]),
element_name=name_parts[-1],
)

@staticmethod
def create_structured_name( # noqa: D
element_name: str,
entity_links: Tuple[EntityReference, ...] = (),
time_granularity: Optional[TimeGranularity] = None,
) -> StructuredDunderedName:
return StructuredDunderedName(
entity_links=entity_links,
element_name=element_name,
time_granularity=time_granularity,
)
7 changes: 4 additions & 3 deletions dbt_semantic_interfaces/parsing/text_input/ti_description.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,14 @@ def __post_init__(self) -> None: # noqa: D105
else:
assert_values_exhausted(item_type)

structured_item_name = StructuredDunderedName.parse_name(self.item_name)

# Check that metrics do not have an entity prefix or entity path.
if item_type is QueryItemType.METRIC:
if len(self.entity_path) > 0:
raise InvalidQuerySyntax("The entity path should not be specified for a metric.")
if len(structured_item_name.entity_links) > 0:
if (
len(StructuredDunderedName.parse_name(name=self.item_name, custom_granularity_names=()).entity_links)
> 0
):
raise InvalidQuerySyntax("The name of the metric should not have entity links.")
# Check that dimensions / time dimensions have a valid date part.
elif item_type is QueryItemType.DIMENSION or item_type is QueryItemType.TIME_DIMENSION:
Expand Down
46 changes: 0 additions & 46 deletions dbt_semantic_interfaces/parsing/text_input/ti_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@
from typing_extensions import override

from dbt_semantic_interfaces.errors import InvalidQuerySyntax
from dbt_semantic_interfaces.parsing.text_input.description_renderer import (
QueryItemDescriptionRenderer,
)
from dbt_semantic_interfaces.parsing.text_input.rendering_helper import (
ObjectBuilderJinjaRenderHelper,
)
Expand Down Expand Up @@ -77,34 +74,6 @@ def collect_descriptions_from_template(
)
return description_collector.collected_descriptions()

def render_template(
self,
jinja_template: str,
renderer: QueryItemDescriptionRenderer,
valid_method_mapping: ValidMethodMapping,
) -> str:
"""Renders the Jinja template using the specified renderer.
Args:
jinja_template: A Jinja template string like `{{ Dimension('listing__country') }} = 'US'`.
renderer: The renderer to use for rendering each item.
valid_method_mapping: Mapping from the builder object to the valid methods. See
`ConfiguredValidMethodMapping`.
Returns:
The rendered Jinja template.
Raises:
QueryItemJinjaException: See definition.
InvalidBuilderMethodException: See definition.
"""
render_processor = _RendererProcessor(renderer)
return self._process_template(
jinja_template=jinja_template,
valid_method_mapping=valid_method_mapping,
description_processor=render_processor,
)

def _process_template(
self,
jinja_template: str,
Expand Down Expand Up @@ -161,18 +130,3 @@ def process_description(self, item_description: ObjectBuilderItemDescription) ->
self._items.append(item_description)

return ""


class _RendererProcessor(ObjectBuilderItemDescriptionProcessor):
"""Processor that renders the descriptions in a Jinja template using the given renderer.
This is just a pass-through, but it allows `QueryItemDescriptionRenderer` to be a facade that has more appropriate
method names.
"""

def __init__(self, renderer: QueryItemDescriptionRenderer) -> None: # noqa: D107
self._renderer = renderer

@override
def process_description(self, item_description: ObjectBuilderItemDescription) -> str:
return self._renderer.render_description(item_description)
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
ParseWhereFilterException,
TimeDimensionCallParameterSet,
)
from dbt_semantic_interfaces.naming.dundered import DunderedNameFormatter
from dbt_semantic_interfaces.naming.dundered import StructuredDunderedName
from dbt_semantic_interfaces.naming.keywords import is_metric_time_name
from dbt_semantic_interfaces.references import (
DimensionReference,
Expand Down Expand Up @@ -46,6 +46,7 @@ def _exception_message_for_incorrect_format(element_name: str) -> str:
@staticmethod
def create_time_dimension(
time_dimension_name: str,
custom_granularity_names: Sequence[str],
time_granularity_name: Optional[str] = None,
entity_path: Sequence[str] = (),
date_part_name: Optional[str] = None,
Expand All @@ -65,14 +66,14 @@ def create_time_dimension(
for parsing where filters. When we solve the problems with our current where filter spec this will
persist as a backwards compatibility model, but nothing more.
"""
group_by_item_name = DunderedNameFormatter.parse_name(time_dimension_name)
group_by_item_name = StructuredDunderedName.parse_name(
name=time_dimension_name, custom_granularity_names=custom_granularity_names
)
if len(group_by_item_name.entity_links) != 1 and not is_metric_time_name(group_by_item_name.element_name):
raise ParseWhereFilterException(
ParameterSetFactory._exception_message_for_incorrect_format(time_dimension_name)
)
grain_parsed_from_name = (
group_by_item_name.time_granularity.value if group_by_item_name.time_granularity else None
)
grain_parsed_from_name = group_by_item_name.time_granularity
inputs_are_mismatched = (
grain_parsed_from_name is not None
and time_granularity_name is not None
Expand Down Expand Up @@ -101,7 +102,7 @@ def create_time_dimension(
@staticmethod
def create_dimension(dimension_name: str, entity_path: Sequence[str] = ()) -> DimensionCallParameterSet:
"""Gets called by Jinja when rendering {{ Dimension(...) }}."""
group_by_item_name = DunderedNameFormatter.parse_name(dimension_name)
group_by_item_name = StructuredDunderedName.parse_name(name=dimension_name, custom_granularity_names=())

if len(group_by_item_name.entity_links) != 1 and not is_metric_time_name(group_by_item_name.element_name):
raise ParseWhereFilterException(ParameterSetFactory._exception_message_for_incorrect_format(dimension_name))
Expand All @@ -116,7 +117,7 @@ def create_dimension(dimension_name: str, entity_path: Sequence[str] = ()) -> Di
@staticmethod
def create_entity(entity_name: str, entity_path: Sequence[str] = ()) -> EntityCallParameterSet:
"""Gets called by Jinja when rendering {{ Entity(...) }}."""
structured_dundered_name = DunderedNameFormatter.parse_name(entity_name)
structured_dundered_name = StructuredDunderedName.parse_name(name=entity_name, custom_granularity_names=())
if structured_dundered_name.time_granularity is not None:
raise ParseWhereFilterException(
f"Name is in an incorrect format: {repr(entity_name)}. " f"It should not contain a time grain suffix."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ def parse_item_descriptions(where_sql_template: str) -> Sequence[ObjectBuilderIt
raise ParseWhereFilterException(f"Error while parsing Jinja template:\n{where_sql_template}") from e

@staticmethod
def parse_call_parameter_sets(where_sql_template: str) -> FilterCallParameterSets:
def parse_call_parameter_sets(
where_sql_template: str, custom_granularity_names: Sequence[str]
) -> FilterCallParameterSets:
"""Return the result of extracting the semantic objects referenced in the where SQL template string."""
descriptions = WhereFilterParser.parse_item_descriptions(where_sql_template)

Expand All @@ -63,6 +65,7 @@ def parse_call_parameter_sets(where_sql_template: str) -> FilterCallParameterSet
time_granularity_name=description.time_granularity_name,
entity_path=description.entity_path,
date_part_name=description.date_part_name,
custom_granularity_names=custom_granularity_names,
)
)
else:
Expand All @@ -79,6 +82,7 @@ def parse_call_parameter_sets(where_sql_template: str) -> FilterCallParameterSet
time_granularity_name=description.time_granularity_name,
entity_path=description.entity_path,
date_part_name=description.date_part_name,
custom_granularity_names=custom_granularity_names,
)
)
elif item_type is QueryItemType.ENTITY:
Expand Down
Loading

0 comments on commit 6275d37

Please sign in to comment.