-
Notifications
You must be signed in to change notification settings - Fork 14
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
Support for Dimension.grain(...)
in where/filter
#152
Merged
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
7faca70
added support for Dimension(...).grain
DevonFulcher 1aff3c9
added test
DevonFulcher 29eeceb
fixed formatting & linting
DevonFulcher 23edab7
changie
DevonFulcher 1c7b68a
moved query_interface to protocols directory
DevonFulcher 584ef92
removed raise NotImplementedError in protocol. added abstractmethod d…
DevonFulcher 3835584
removed alias
DevonFulcher 5309a49
made error message more user friendly
DevonFulcher bcc6d8e
improved another error message
DevonFulcher e3ef46c
appending to time_dimension_call_parameter_sets
DevonFulcher 3d7f5fe
added comments
DevonFulcher 13ebd52
fixed error message
DevonFulcher 804937a
slight language change
DevonFulcher a374894
fixed linter & formatter
DevonFulcher 31a1bb0
Merge branch 'main' into dimension_dot_grain_support
DevonFulcher ef68e74
fixed import
DevonFulcher 03dc547
fixed formatting
DevonFulcher File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
kind: Features | ||
body: Support for `Dimension.grain(...)` in where/filter | ||
time: 2023-09-18T11:21:59.459474-05:00 | ||
custom: | ||
Author: DevonFulcher | ||
Issue: None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
95 changes: 95 additions & 0 deletions
95
dbt_semantic_interfaces/parsing/where_filter/parameter_set_factory.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
from typing import Sequence | ||
|
||
from dbt_semantic_interfaces.call_parameter_sets import ( | ||
DimensionCallParameterSet, | ||
EntityCallParameterSet, | ||
ParseWhereFilterException, | ||
TimeDimensionCallParameterSet, | ||
) | ||
from dbt_semantic_interfaces.naming.dundered import DunderedNameFormatter | ||
from dbt_semantic_interfaces.naming.keywords import ( | ||
METRIC_TIME_ELEMENT_NAME, | ||
is_metric_time_name, | ||
) | ||
from dbt_semantic_interfaces.references import ( | ||
DimensionReference, | ||
EntityReference, | ||
TimeDimensionReference, | ||
) | ||
from dbt_semantic_interfaces.type_enums import TimeGranularity | ||
|
||
|
||
class ParameterSetFactory: | ||
"""Creates parameter sets for use in the Jinja sandbox.""" | ||
|
||
@staticmethod | ||
def _exception_message_for_incorrect_format(element_name: str) -> str: | ||
return ( | ||
f"Name is in an incorrect format: '{element_name}'. It should be of the form: " | ||
f"<primary entity name>__<dimension_name>" | ||
) | ||
|
||
@staticmethod | ||
def create_time_dimension( | ||
time_dimension_name: str, time_granularity_name: str, entity_path: Sequence[str] = () | ||
) -> TimeDimensionCallParameterSet: | ||
"""Gets called by Jinja when rendering {{ TimeDimension(...) }}.""" | ||
group_by_item_name = DunderedNameFormatter.parse_name(time_dimension_name) | ||
|
||
# metric_time is the only time dimension that does not have an associated primary entity, so the | ||
# GroupByItemName would not have any entity links. | ||
if is_metric_time_name(group_by_item_name.element_name): | ||
if len(group_by_item_name.entity_links) != 0 or group_by_item_name.time_granularity is not None: | ||
raise ParseWhereFilterException( | ||
f"Name is in an incorrect format: {time_dimension_name} " | ||
f"When referencing {METRIC_TIME_ELEMENT_NAME}," | ||
"the name should not have any dunders (double underscores, or __)." | ||
) | ||
else: | ||
if len(group_by_item_name.entity_links) != 1 or group_by_item_name.time_granularity is not None: | ||
raise ParseWhereFilterException( | ||
ParameterSetFactory._exception_message_for_incorrect_format(time_dimension_name) | ||
) | ||
|
||
return TimeDimensionCallParameterSet( | ||
time_dimension_reference=TimeDimensionReference(element_name=group_by_item_name.element_name), | ||
entity_path=( | ||
tuple(EntityReference(element_name=arg) for arg in entity_path) + group_by_item_name.entity_links | ||
), | ||
time_granularity=TimeGranularity(time_granularity_name), | ||
) | ||
|
||
@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) | ||
if is_metric_time_name(group_by_item_name.element_name): | ||
raise ParseWhereFilterException( | ||
f"{METRIC_TIME_ELEMENT_NAME} is a time dimension, so it should be referenced using " | ||
f"TimeDimension(...) or Dimension(...).grain(...)" | ||
) | ||
|
||
if len(group_by_item_name.entity_links) != 1: | ||
raise ParseWhereFilterException(ParameterSetFactory._exception_message_for_incorrect_format(dimension_name)) | ||
|
||
return DimensionCallParameterSet( | ||
dimension_reference=DimensionReference(element_name=group_by_item_name.element_name), | ||
entity_path=( | ||
tuple(EntityReference(element_name=arg) for arg in entity_path) + group_by_item_name.entity_links | ||
), | ||
) | ||
|
||
@staticmethod | ||
def create_entity(entity_name: str, entity_path: Sequence[str] = ()) -> EntityCallParameterSet: | ||
"""Gets called by Jinja when rendering {{ Entity(...) }}.""" | ||
group_by_item_name = DunderedNameFormatter.parse_name(entity_name) | ||
if len(group_by_item_name.entity_links) > 0 or group_by_item_name.time_granularity is not None: | ||
ParameterSetFactory._exception_message_for_incorrect_format( | ||
f"Name is in an incorrect format: {entity_name} " | ||
f"When referencing entities, the name should not have any dunders (double underscores, or __)." | ||
) | ||
|
||
return EntityCallParameterSet( | ||
entity_path=tuple(EntityReference(element_name=arg) for arg in entity_path), | ||
entity_reference=EntityReference(element_name=entity_name), | ||
) |
53 changes: 53 additions & 0 deletions
53
dbt_semantic_interfaces/parsing/where_filter/where_filter_dimension.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
from __future__ import annotations | ||
|
||
from typing import List, Optional, Sequence | ||
|
||
from typing_extensions import override | ||
|
||
from dbt_semantic_interfaces.protocols.protocol_hint import ProtocolHint | ||
from dbt_semantic_interfaces.protocols.query_interface import ( | ||
QueryInterfaceDimension, | ||
QueryInterfaceDimensionFactory, | ||
) | ||
|
||
|
||
class WhereFilterDimension(ProtocolHint[QueryInterfaceDimension]): | ||
"""A dimension that is passed in through the where filter parameter.""" | ||
|
||
@override | ||
def _implements_protocol(self) -> QueryInterfaceDimension: | ||
return self | ||
|
||
def __init__( # noqa | ||
self, | ||
name: str, | ||
entity_path: Sequence[str], | ||
) -> None: | ||
self.name = name | ||
self.entity_path = entity_path | ||
self.time_granularity_name: Optional[str] = None | ||
|
||
def grain(self, time_granularity: str) -> QueryInterfaceDimension: | ||
"""The time granularity.""" | ||
self.time_granularity_name = time_granularity | ||
return self | ||
|
||
|
||
class WhereFilterDimensionFactory(ProtocolHint[QueryInterfaceDimensionFactory]): | ||
"""Creates a WhereFilterDimension. | ||
|
||
Each call to `create` adds a WhereFilterDimension to `created`. | ||
""" | ||
|
||
@override | ||
def _implements_protocol(self) -> QueryInterfaceDimensionFactory: | ||
return self | ||
|
||
def __init__(self) -> None: # noqa | ||
self.created: List[WhereFilterDimension] = [] | ||
|
||
def create(self, dimension_name: str, entity_path: Sequence[str] = ()) -> WhereFilterDimension: | ||
"""Gets called by Jinja when rendering {{ Dimension(...) }}.""" | ||
dimension = WhereFilterDimension(dimension_name, entity_path) | ||
self.created.append(dimension) | ||
return dimension |
43 changes: 43 additions & 0 deletions
43
dbt_semantic_interfaces/parsing/where_filter/where_filter_entity.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
from __future__ import annotations | ||
|
||
from typing import List, Sequence | ||
|
||
from typing_extensions import override | ||
|
||
from dbt_semantic_interfaces.call_parameter_sets import EntityCallParameterSet | ||
from dbt_semantic_interfaces.parsing.where_filter.parameter_set_factory import ( | ||
ParameterSetFactory, | ||
) | ||
from dbt_semantic_interfaces.protocols.protocol_hint import ProtocolHint | ||
from dbt_semantic_interfaces.protocols.query_interface import ( | ||
QueryInterfaceEntity, | ||
QueryInterfaceEntityFactory, | ||
) | ||
|
||
|
||
class EntityStub(ProtocolHint[QueryInterfaceEntity]): | ||
"""An Entity implementation that just satisfies the protocol. | ||
|
||
QueryInterfaceEntity currently has no methods and the parameter set is created in the factory. | ||
So, there is nothing to do here. | ||
""" | ||
|
||
@override | ||
def _implements_protocol(self) -> QueryInterfaceEntity: | ||
return self | ||
|
||
|
||
class WhereFilterEntityFactory(ProtocolHint[QueryInterfaceEntityFactory]): | ||
"""Executes in the Jinja sandbox to produce parameter sets and append them to a list.""" | ||
|
||
@override | ||
def _implements_protocol(self) -> QueryInterfaceEntityFactory: | ||
return self | ||
|
||
def __init__(self) -> None: # noqa | ||
self.entity_call_parameter_sets: List[EntityCallParameterSet] = [] | ||
|
||
def create(self, entity_name: str, entity_path: Sequence[str] = ()) -> EntityStub: | ||
"""Gets called by Jinja when rendering {{ Entity(...) }}.""" | ||
self.entity_call_parameter_sets.append(ParameterSetFactory.create_entity(entity_name, entity_path)) | ||
return EntityStub() |
68 changes: 68 additions & 0 deletions
68
dbt_semantic_interfaces/parsing/where_filter/where_filter_parser.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
from __future__ import annotations | ||
|
||
from jinja2 import StrictUndefined | ||
from jinja2.exceptions import SecurityError, TemplateSyntaxError, UndefinedError | ||
from jinja2.sandbox import SandboxedEnvironment | ||
|
||
from dbt_semantic_interfaces.call_parameter_sets import ( | ||
FilterCallParameterSets, | ||
ParseWhereFilterException, | ||
) | ||
from dbt_semantic_interfaces.parsing.where_filter.parameter_set_factory import ( | ||
ParameterSetFactory, | ||
) | ||
from dbt_semantic_interfaces.parsing.where_filter.where_filter_dimension import ( | ||
WhereFilterDimensionFactory, | ||
) | ||
from dbt_semantic_interfaces.parsing.where_filter.where_filter_entity import ( | ||
WhereFilterEntityFactory, | ||
) | ||
from dbt_semantic_interfaces.parsing.where_filter.where_filter_time_dimension import ( | ||
WhereFilterTimeDimensionFactory, | ||
) | ||
|
||
|
||
class WhereFilterParser: | ||
"""Parses the template in the WhereFilter into FilterCallParameterSets.""" | ||
|
||
@staticmethod | ||
def parse_call_parameter_sets(where_sql_template: str) -> FilterCallParameterSets: | ||
"""Return the result of extracting the semantic objects referenced in the where SQL template string.""" | ||
time_dimension_factory = WhereFilterTimeDimensionFactory() | ||
dimension_factory = WhereFilterDimensionFactory() | ||
entity_factory = WhereFilterEntityFactory() | ||
|
||
try: | ||
# the string that the sandbox renders is unused | ||
SandboxedEnvironment(undefined=StrictUndefined).from_string(where_sql_template).render( | ||
Dimension=dimension_factory.create, | ||
TimeDimension=time_dimension_factory.create, | ||
Entity=entity_factory.create, | ||
) | ||
except (UndefinedError, TemplateSyntaxError, SecurityError) as e: | ||
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 | ||
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: | ||
time_dimension_factory.time_dimension_call_parameter_sets.append( | ||
ParameterSetFactory.create_time_dimension( | ||
dimension.name, | ||
dimension.time_granularity_name, | ||
dimension.entity_path, | ||
) | ||
) | ||
else: | ||
dimension_call_parameter_sets.append( | ||
ParameterSetFactory.create_dimension(dimension.name, dimension.entity_path) | ||
) | ||
|
||
return FilterCallParameterSets( | ||
dimension_call_parameter_sets=tuple(dimension_call_parameter_sets), | ||
time_dimension_call_parameter_sets=tuple(time_dimension_factory.time_dimension_call_parameter_sets), | ||
entity_call_parameter_sets=tuple(entity_factory.entity_call_parameter_sets), | ||
) |
47 changes: 47 additions & 0 deletions
47
dbt_semantic_interfaces/parsing/where_filter/where_filter_time_dimension.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
from __future__ import annotations | ||
|
||
from typing import List, Sequence | ||
|
||
from typing_extensions import override | ||
|
||
from dbt_semantic_interfaces.call_parameter_sets import TimeDimensionCallParameterSet | ||
from dbt_semantic_interfaces.parsing.where_filter.parameter_set_factory import ( | ||
ParameterSetFactory, | ||
) | ||
from dbt_semantic_interfaces.protocols.protocol_hint import ProtocolHint | ||
from dbt_semantic_interfaces.protocols.query_interface import ( | ||
QueryInterfaceTimeDimension, | ||
QueryInterfaceTimeDimensionFactory, | ||
) | ||
|
||
|
||
class TimeDimensionStub(ProtocolHint[QueryInterfaceTimeDimension]): | ||
"""A TimeDimension implementation that just satisfies the protocol. | ||
|
||
QueryInterfaceTimeDimension currently has no methods and the parameter set is created in the factory. | ||
So, there is nothing to do here. | ||
""" | ||
|
||
@override | ||
def _implements_protocol(self) -> QueryInterfaceTimeDimension: | ||
return self | ||
|
||
|
||
class WhereFilterTimeDimensionFactory(ProtocolHint[QueryInterfaceTimeDimensionFactory]): | ||
"""Executes in the Jinja sandbox to produce parameter sets and append them to a list.""" | ||
|
||
@override | ||
def _implements_protocol(self) -> QueryInterfaceTimeDimensionFactory: | ||
return self | ||
|
||
def __init__(self) -> None: # noqa | ||
self.time_dimension_call_parameter_sets: List[TimeDimensionCallParameterSet] = [] | ||
|
||
def create( | ||
self, time_dimension_name: str, time_granularity_name: str, entity_path: Sequence[str] = () | ||
) -> TimeDimensionStub: | ||
"""Gets called by Jinja when rendering {{ TimeDimension(...) }}.""" | ||
self.time_dimension_call_parameter_sets.append( | ||
ParameterSetFactory.create_time_dimension(time_dimension_name, time_granularity_name, entity_path) | ||
) | ||
return TimeDimensionStub() |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice. 😁