Skip to content

Commit

Permalink
Support for date_part for Dimension & TimeDimension (#183)
Browse files Browse the repository at this point in the history
This PR adds support for the date_part for Dimension & TimeDimension. This is helpful for our Tableau integration. A time_dimension_call_parameter_set is now created if Dimension(...).grain(...) or Dimension(...).date_part(...) is used or both.
  • Loading branch information
DevonFulcher authored Oct 24, 2023
1 parent a00ee84 commit 4c98d80
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 11 deletions.
6 changes: 6 additions & 0 deletions .changes/unreleased/Features-20231024-171020.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: Features
body: Support for date_part for Dimension & TimeDimension
time: 2023-10-24T17:10:20.59653-05:00
custom:
Author: DevonFulcher
Issue: "188"
2 changes: 2 additions & 0 deletions dbt_semantic_interfaces/call_parameter_sets.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
TimeDimensionReference,
)
from dbt_semantic_interfaces.type_enums import TimeGranularity
from dbt_semantic_interfaces.type_enums.date_part import DatePart


@dataclass(frozen=True)
Expand All @@ -26,6 +27,7 @@ class TimeDimensionCallParameterSet:
entity_path: Tuple[EntityReference, ...]
time_dimension_reference: TimeDimensionReference
time_granularity: Optional[TimeGranularity] = None
date_part: Optional[DatePart] = None


@dataclass(frozen=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
TimeDimensionReference,
)
from dbt_semantic_interfaces.type_enums import TimeGranularity
from dbt_semantic_interfaces.type_enums.date_part import DatePart


class ParameterSetFactory:
Expand All @@ -31,7 +32,10 @@ def _exception_message_for_incorrect_format(element_name: str) -> str:

@staticmethod
def create_time_dimension(
time_dimension_name: str, time_granularity_name: Optional[str] = None, entity_path: Sequence[str] = ()
time_dimension_name: str,
time_granularity_name: Optional[str] = None,
entity_path: Sequence[str] = (),
date_part_name: Optional[str] = None,
) -> TimeDimensionCallParameterSet:
"""Gets called by Jinja when rendering {{ TimeDimension(...) }}."""
group_by_item_name = DunderedNameFormatter.parse_name(time_dimension_name)
Expand All @@ -57,6 +61,7 @@ def create_time_dimension(
tuple(EntityReference(element_name=arg) for arg in entity_path) + group_by_item_name.entity_links
),
time_granularity=TimeGranularity(time_granularity_name) if time_granularity_name is not None else None,
date_part=DatePart(date_part_name.lower()) if date_part_name else None,
)

@staticmethod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def __init__( # noqa
self.name = name
self.entity_path = entity_path
self.time_granularity_name: Optional[str] = None
self.date_part_name: Optional[str] = None

def grain(self, time_granularity: str) -> QueryInterfaceDimension:
"""The time granularity."""
Expand All @@ -37,9 +38,10 @@ def descending(self, _is_descending: bool) -> QueryInterfaceDimension:
"""Set the sort order for order-by."""
raise InvalidQuerySyntax("descending is invalid in the where parameter and filter spec")

def date_part(self, _date_part: str) -> QueryInterfaceDimension:
def date_part(self, date_part_name: str) -> QueryInterfaceDimension:
"""Date part to extract from the dimension."""
raise InvalidQuerySyntax("date_part isn't currently supported in the where parameter and filter spec")
self.date_part_name = date_part_name
return self


class WhereFilterDimensionFactory(ProtocolHint[QueryInterfaceDimensionFactory]):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,15 @@ def parse_call_parameter_sets(where_sql_template: str) -> FilterCallParameterSet
raise ParseWhereFilterException(f"Error while parsing Jinja template:\n{where_sql_template}") from e

"""
Dimensions that are created with a grain parameter, Dimension(...).grain(...), are
Dimensions that are created with a grain or date_part parameter, for instance Dimension(...).grain(...), are
added to time_dimension_call_parameter_sets otherwise they are add to dimension_call_parameter_sets
"""
dimension_call_parameter_sets = []
for dimension in dimension_factory.created:
if dimension.time_granularity_name:
if dimension.time_granularity_name or dimension.date_part_name:
time_dimension_factory.time_dimension_call_parameter_sets.append(
ParameterSetFactory.create_time_dimension(
dimension.name,
dimension.time_granularity_name,
dimension.entity_path,
dimension.name, dimension.time_granularity_name, dimension.entity_path, dimension.date_part_name
)
)
else:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ def create(
"""Gets called by Jinja when rendering {{ TimeDimension(...) }}."""
if descending is not None:
raise InvalidQuerySyntax("descending is invalid in the where parameter and filter spec")
if date_part_name is not None:
raise InvalidQuerySyntax("date_part isn't currently supported in the where parameter and filter spec")
self.time_dimension_call_parameter_sets.append(
ParameterSetFactory.create_time_dimension(time_dimension_name, time_granularity_name, entity_path)
ParameterSetFactory.create_time_dimension(
time_dimension_name, time_granularity_name, entity_path, date_part_name
)
)
return TimeDimensionStub()
43 changes: 43 additions & 0 deletions dbt_semantic_interfaces/type_enums/date_part.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from enum import Enum
from typing import List

from dbt_semantic_interfaces.enum_extension import assert_values_exhausted
from dbt_semantic_interfaces.type_enums.time_granularity import TimeGranularity


class DatePart(Enum):
"""Date parts able to be extracted from a time dimension.
Week is not an option due to divergent results across engine contexts see: https://github.com/dbt-labs/metricflow/pull/812
TODO: add support for hour, minute, second once those granularities are available
"""

YEAR = "year"
QUARTER = "quarter"
MONTH = "month"
DAY = "day"
DOW = "dow"
DOY = "doy"

def to_int(self) -> int:
"""Convert to an int so that the size of the granularity can be easily compared."""
if self is DatePart.DAY:
return TimeGranularity.DAY.to_int()
elif self is DatePart.DOW:
return TimeGranularity.DAY.to_int()
elif self is DatePart.DOY:
return TimeGranularity.DAY.to_int()
elif self is DatePart.MONTH:
return TimeGranularity.MONTH.to_int()
elif self is DatePart.QUARTER:
return TimeGranularity.QUARTER.to_int()
elif self is DatePart.YEAR:
return TimeGranularity.YEAR.to_int()
else:
assert_values_exhausted(self)

@property
def compatible_granularities(self) -> List[TimeGranularity]:
"""Granularities that can be queried with this date part."""
return [granularity for granularity in TimeGranularity if granularity.to_int() >= self.to_int()]
27 changes: 27 additions & 0 deletions tests/parsing/test_where_filter_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@
"""


import pytest

from dbt_semantic_interfaces.implementations.base import HashableBaseModel
from dbt_semantic_interfaces.implementations.filters.where_filter import (
PydanticWhereFilter,
PydanticWhereFilterIntersection,
)
from dbt_semantic_interfaces.parsing.where_filter.where_filter_parser import (
WhereFilterParser,
)
from dbt_semantic_interfaces.type_enums.date_part import DatePart

__BOOLEAN_EXPRESSION__ = "1 > 0"

Expand Down Expand Up @@ -135,3 +141,24 @@ def test_where_filter_intersection_from_partially_deserialized_list_of_strings()
parsed_model = ModelWithWhereFilterIntersection.parse_obj(obj)

assert parsed_model.where_filter == expected_parsed_output


@pytest.mark.parametrize(
"where",
[
"{{ TimeDimension('metric_time', 'YEAR', [], None, 'YEAR') }} > '2023-01-01'",
"{{ TimeDimension(time_dimension_name='metric_time', time_granularity_name='YEAR', date_part_name='YEAR') }}"
+ "> '2023-01-01'",
],
)
def test_time_dimension_date_part(where: str) -> None: # noqa
param_sets = WhereFilterParser.parse_call_parameter_sets(where)
assert len(param_sets.time_dimension_call_parameter_sets) == 1
assert param_sets.time_dimension_call_parameter_sets[0].date_part == DatePart.YEAR


def test_dimension_date_part() -> None: # noqa
where = "{{ Dimension('metric_time').grain('DAY').date_part('YEAR') }} > '2023-01-01'"
param_sets = WhereFilterParser.parse_call_parameter_sets(where)
assert len(param_sets.time_dimension_call_parameter_sets) == 1
assert param_sets.time_dimension_call_parameter_sets[0].date_part == DatePart.YEAR

0 comments on commit 4c98d80

Please sign in to comment.