Skip to content

Commit

Permalink
Make SemanticManifestLookup available for spec pattern creation (#1403)
Browse files Browse the repository at this point in the history
MetricFlow's query input parameter processing runs through a
pattern matching/parsing step that then produces a parameter spec
for subsequent matching during group by resolution. In practice,
every query parameter either directly references or is derived
directly from one or more elements in a semantic manifest.

Converting a parameter into a corresponsing spec pattern for a
given query input can, therefore, benefit from access to
the semantic manifest lookup objects we provide. This will
allow us to do things like map custom granularities to their
corresponding base granularities when doing group by resolution.

This change simply makes the semantic manifest available to the
method that converts between the query input parameter and the
spec pattern. It isolates the availability of the semantic
manifest lookup to this method because the other methods in the
relevant classes should generally not be using the lookup to
perform their operations, and the change as implemented
here also has a more limited blast radius.
  • Loading branch information
tlento authored Sep 18, 2024
1 parent 4e16f2e commit 9bd4b43
Show file tree
Hide file tree
Showing 15 changed files with 195 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from dbt_semantic_interfaces.type_enums.date_part import DatePart
from typing_extensions import override

from metricflow_semantics.model.semantic_manifest_lookup import SemanticManifestLookup
from metricflow_semantics.naming.naming_scheme import QueryItemNamingScheme
from metricflow_semantics.specs.instance_spec import InstanceSpec
from metricflow_semantics.specs.patterns.entity_link_pattern import (
Expand Down Expand Up @@ -50,7 +51,7 @@ def input_str(self, instance_spec: InstanceSpec) -> Optional[str]:
return names[0]

@override
def spec_pattern(self, input_str: str) -> EntityLinkPattern:
def spec_pattern(self, input_str: str, semantic_manifest_lookup: SemanticManifestLookup) -> EntityLinkPattern:
if not self.input_str_follows_scheme(input_str):
raise ValueError(f"{repr(input_str)} does not follow this scheme.")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from dbt_semantic_interfaces.references import MetricReference
from typing_extensions import override

from metricflow_semantics.model.semantic_manifest_lookup import SemanticManifestLookup
from metricflow_semantics.naming.naming_scheme import QueryItemNamingScheme
from metricflow_semantics.specs.instance_spec import InstanceSpec
from metricflow_semantics.specs.patterns.metric_pattern import MetricSpecPattern
Expand All @@ -25,7 +26,7 @@ def input_str(self, instance_spec: InstanceSpec) -> Optional[str]:
return names[0]

@override
def spec_pattern(self, input_str: str) -> MetricSpecPattern:
def spec_pattern(self, input_str: str, semantic_manifest_lookup: SemanticManifestLookup) -> MetricSpecPattern:
input_str = input_str.lower()
if not self.input_str_follows_scheme(input_str):
raise RuntimeError(f"{repr(input_str)} does not follow this scheme.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Optional

from metricflow_semantics.model.semantic_manifest_lookup import SemanticManifestLookup
from metricflow_semantics.specs.patterns.spec_pattern import SpecPattern

if TYPE_CHECKING:
Expand All @@ -29,11 +30,14 @@ def input_str(self, instance_spec: InstanceSpec) -> Optional[str]:
pass

@abstractmethod
def spec_pattern(self, input_str: str) -> SpecPattern:
def spec_pattern(self, input_str: str, semantic_manifest_lookup: SemanticManifestLookup) -> SpecPattern:
"""Given an input that follows this scheme, return a spec pattern that matches the described input.
If the input_str does not follow this scheme, raise a ValueError. In practice, input_str_follows_scheme() should
be called on the input_str beforehand.
This is used to generate suggestions from available group-by-items if the user specifies a group-by-item that is
invalid.
If this scheme cannot accommodate the spec, return None. This is needed to handle unsupported cases in
DunderNamingScheme, such as DatePart, but naming schemes should otherwise be complete.
"""
pass

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from dbt_semantic_interfaces.references import EntityReference
from typing_extensions import override

from metricflow_semantics.model.semantic_manifest_lookup import SemanticManifestLookup
from metricflow_semantics.naming.naming_scheme import QueryItemNamingScheme
from metricflow_semantics.naming.object_builder_str import ObjectBuilderNameConverter
from metricflow_semantics.specs.instance_spec import InstanceSpec
Expand All @@ -36,7 +37,7 @@ def input_str(self, instance_spec: InstanceSpec) -> Optional[str]:
return ObjectBuilderNameConverter.input_str_from_spec(instance_spec)

@override
def spec_pattern(self, input_str: str) -> SpecPattern:
def spec_pattern(self, input_str: str, semantic_manifest_lookup: SemanticManifestLookup) -> SpecPattern:
if not self.input_str_follows_scheme(input_str):
raise ValueError(
f"The specified input {repr(input_str)} does not match the input described by the object builder "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from dbt_semantic_interfaces.type_enums.date_part import DatePart

if TYPE_CHECKING:
from metricflow_semantics.model.semantic_manifest_lookup import SemanticManifestLookup
from metricflow_semantics.query.resolver_inputs.query_resolver_inputs import (
ResolverInputForGroupByItem,
ResolverInputForMetric,
Expand All @@ -22,8 +23,9 @@ def name(self) -> str:
"""The name of the metric."""
raise NotImplementedError

@property
def query_resolver_input(self) -> ResolverInputForMetric: # noqa: D102
def query_resolver_input( # noqa: D102
self, semantic_manifest_lookup: SemanticManifestLookup
) -> ResolverInputForMetric:
raise NotImplementedError


Expand All @@ -36,8 +38,9 @@ def name(self) -> str:
"""The name of the metric."""
raise NotImplementedError

@property
def query_resolver_input(self) -> ResolverInputForGroupByItem: # noqa: D102
def query_resolver_input( # noqa: D102
self, semantic_manifest_lookup: SemanticManifestLookup
) -> ResolverInputForGroupByItem:
raise NotImplementedError


Expand All @@ -58,8 +61,9 @@ def date_part(self) -> Optional[DatePart]:
"""Date part to extract from the dimension."""
raise NotImplementedError

@property
def query_resolver_input(self) -> ResolverInputForGroupByItem: # noqa: D102
def query_resolver_input( # noqa: D102
self, semantic_manifest_lookup: SemanticManifestLookup
) -> ResolverInputForGroupByItem:
raise NotImplementedError


Expand All @@ -80,8 +84,9 @@ def descending(self) -> bool:
"""Indicates if the order should be ascending or descending."""
raise NotImplementedError

@property
def query_resolver_input(self) -> ResolverInputForOrderByItem: # noqa: D102
def query_resolver_input( # noqa: D102
self, semantic_manifest_lookup: SemanticManifestLookup
) -> ResolverInputForOrderByItem:
raise NotImplementedError


Expand Down
33 changes: 24 additions & 9 deletions metricflow-semantics/metricflow_semantics/query/query_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,9 @@ def _parse_order_by_names(
ResolverInputForGroupByItem(
input_obj=order_by_name,
input_obj_naming_scheme=group_by_item_naming_scheme,
spec_pattern=group_by_item_naming_scheme.spec_pattern(order_by_name_without_prefix),
spec_pattern=group_by_item_naming_scheme.spec_pattern(
order_by_name_without_prefix, semantic_manifest_lookup=self._manifest_lookup
),
)
)
break
Expand All @@ -226,7 +228,9 @@ def _parse_order_by_names(
ResolverInputForMetric(
input_obj=order_by_name,
naming_scheme=metric_naming_scheme,
spec_pattern=metric_naming_scheme.spec_pattern(order_by_name_without_prefix),
spec_pattern=metric_naming_scheme.spec_pattern(
order_by_name_without_prefix, semantic_manifest_lookup=self._manifest_lookup
),
)
)

Expand All @@ -240,11 +244,14 @@ def _parse_order_by_names(

return resolver_inputs

@staticmethod
def _parse_order_by(
self,
order_by: Sequence[OrderByQueryParameter],
) -> Sequence[ResolverInputForOrderByItem]:
return tuple(order_by_query_parameter.query_resolver_input for order_by_query_parameter in order_by)
return tuple(
order_by_query_parameter.query_resolver_input(semantic_manifest_lookup=self._manifest_lookup)
for order_by_query_parameter in order_by
)

@staticmethod
def generate_error_message(
Expand Down Expand Up @@ -370,7 +377,9 @@ def _parse_and_validate_query(
resolver_input_for_metric = ResolverInputForMetric(
input_obj=metric_name,
naming_scheme=metric_naming_scheme,
spec_pattern=metric_naming_scheme.spec_pattern(metric_name),
spec_pattern=metric_naming_scheme.spec_pattern(
metric_name, semantic_manifest_lookup=self._manifest_lookup
),
)
resolver_inputs_for_metrics.append(resolver_input_for_metric)
break
Expand All @@ -388,14 +397,18 @@ def _parse_and_validate_query(
)

for metric_query_parameter in metrics:
resolver_inputs_for_metrics.append(metric_query_parameter.query_resolver_input)
resolver_inputs_for_metrics.append(
metric_query_parameter.query_resolver_input(semantic_manifest_lookup=self._manifest_lookup)
)

resolver_inputs_for_group_by_items: List[ResolverInputForGroupByItem] = []
for group_by_name in group_by_names:
resolver_input_for_group_by_item: Optional[MetricFlowQueryResolverInput] = None
for group_by_item_naming_scheme in self._group_by_item_naming_schemes:
if group_by_item_naming_scheme.input_str_follows_scheme(group_by_name):
spec_pattern = group_by_item_naming_scheme.spec_pattern(group_by_name)
spec_pattern = group_by_item_naming_scheme.spec_pattern(
group_by_name, semantic_manifest_lookup=self._manifest_lookup
)
resolver_input_for_group_by_item = ResolverInputForGroupByItem(
input_obj=group_by_name,
input_obj_naming_scheme=group_by_item_naming_scheme,
Expand Down Expand Up @@ -424,7 +437,9 @@ def _parse_and_validate_query(
)

for group_by_parameter in group_by:
resolver_input_for_group_by_parameter = group_by_parameter.query_resolver_input
resolver_input_for_group_by_parameter = group_by_parameter.query_resolver_input(
semantic_manifest_lookup=self._manifest_lookup
)
resolver_inputs_for_group_by_items.append(resolver_input_for_group_by_parameter)
logger.info(
"Converted group-by-item input:\n"
Expand Down Expand Up @@ -454,7 +469,7 @@ def _parse_and_validate_query(
order_by_names=order_by_names,
)
)
resolver_inputs_for_order_by.extend(MetricFlowQueryParser._parse_order_by(order_by=order_by))
resolver_inputs_for_order_by.extend(self._parse_order_by(order_by=order_by))

resolver_input_for_limit = ResolverInputForLimit(limit=limit)
resolver_input_for_min_max_only = ResolverInputForMinMaxOnly(min_max_only=min_max_only)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@
from dbt_semantic_interfaces.type_enums.time_granularity import TimeGranularity
from typing_extensions import override

from metricflow_semantics.model.semantic_manifest_lookup import SemanticManifestLookup
from metricflow_semantics.naming.linkable_spec_name import StructuredLinkableSpecName
from metricflow_semantics.naming.metric_scheme import MetricNamingScheme
from metricflow_semantics.naming.object_builder_scheme import ObjectBuilderNamingScheme
from metricflow_semantics.protocols.query_parameter import (
DimensionOrEntityQueryParameter,
InputOrderByParameter,
MetricQueryParameter,
OrderByQueryParameter,
TimeDimensionQueryParameter,
)
from metricflow_semantics.protocols.query_parameter import SavedQueryParameter as SavedQueryParameterProtocol
Expand All @@ -41,8 +44,11 @@ def _implements_protocol(self) -> TimeDimensionQueryParameter:
grain: Optional[TimeGranularity] = None
date_part: Optional[DatePart] = None

@property
def query_resolver_input(self) -> ResolverInputForGroupByItem: # noqa: D102
def query_resolver_input( # noqa: D102
self,
semantic_manifest_lookup: SemanticManifestLookup,
) -> ResolverInputForGroupByItem:
# TODO: [custom granularity] use manifest lookup to handle custom granularities
fields_to_compare = [
ParameterSetField.ELEMENT_NAME,
ParameterSetField.ENTITY_LINKS,
Expand Down Expand Up @@ -81,8 +87,16 @@ class DimensionOrEntityParameter(ProtocolHint[DimensionOrEntityQueryParameter]):
def _implements_protocol(self) -> DimensionOrEntityQueryParameter:
return self

@property
def query_resolver_input(self) -> ResolverInputForGroupByItem: # noqa: D102
def query_resolver_input(self, semantic_manifest_lookup: SemanticManifestLookup) -> ResolverInputForGroupByItem:
"""Produces resolver input from a query parameter representing a dimension or entity.
Note these parameters do not currently have a direct need for the semantic_manifest_lookup, but since these
can be lumped in with other items that do require it we keep this method signature consistent across
the class sets.
TODO: Refine these query input classes so that this kind of thing is either enforced in self-documenting
ways or removed from the codebase
"""
name_structure = StructuredLinkableSpecName.from_name(self.name.lower())

return ResolverInputForGroupByItem(
Expand All @@ -105,33 +119,43 @@ def query_resolver_input(self) -> ResolverInputForGroupByItem: # noqa: D102


@dataclass(frozen=True)
class MetricParameter:
class MetricParameter(ProtocolHint[MetricQueryParameter]):
"""Metric requested in a query."""

name: str

@property
def query_resolver_input(self) -> ResolverInputForMetric: # noqa: D102
@override
def _implements_protocol(self) -> MetricQueryParameter:
return self

def query_resolver_input( # noqa: D102
self, semantic_manifest_lookup: SemanticManifestLookup
) -> ResolverInputForMetric:
naming_scheme = MetricNamingScheme()
return ResolverInputForMetric(
input_obj=self,
naming_scheme=naming_scheme,
spec_pattern=naming_scheme.spec_pattern(self.name),
spec_pattern=naming_scheme.spec_pattern(self.name, semantic_manifest_lookup=semantic_manifest_lookup),
)


@dataclass(frozen=True)
class OrderByParameter:
class OrderByParameter(ProtocolHint[OrderByQueryParameter]):
"""Order by requested in a query."""

order_by: InputOrderByParameter
descending: bool = False

@property
def query_resolver_input(self) -> ResolverInputForOrderByItem: # noqa: D102
@override
def _implements_protocol(self) -> OrderByQueryParameter:
return self

def query_resolver_input( # noqa: D102
self, semantic_manifest_lookup: SemanticManifestLookup
) -> ResolverInputForOrderByItem:
return ResolverInputForOrderByItem(
input_obj=self,
possible_inputs=(self.order_by.query_resolver_input,),
possible_inputs=(self.order_by.query_resolver_input(semantic_manifest_lookup=semantic_manifest_lookup),),
descending=self.descending,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,9 +343,11 @@ def assert_spec_set_snapshot_equal( # noqa: D103


def assert_linkable_spec_set_snapshot_equal( # noqa: D103
request: FixtureRequest, mf_test_configuration: SnapshotConfiguration, set_id: str, spec_set: LinkableSpecSet
request: FixtureRequest,
mf_test_configuration: SnapshotConfiguration,
set_id: str,
spec_set: LinkableSpecSet,
) -> None:
# TODO: This will be used in a later PR and this message will be removed.
naming_scheme = ObjectBuilderNamingScheme()
assert_snapshot_text_equal(
request=request,
Expand Down
Loading

0 comments on commit 9bd4b43

Please sign in to comment.