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

Support for sort order in query interface #775

Merged
merged 8 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all 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/Features-20230918-155524.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: Features
body: Support for sort order in query interface
time: 2023-09-18T15:55:24.086263-05:00
custom:
Author: DevonFulcher
Issue: None
22 changes: 11 additions & 11 deletions metricflow/engine/metricflow_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@
DataflowToExecutionPlanConverter,
)
from metricflow.plan_conversion.dataflow_to_sql import DataflowToSqlQueryPlanConverter
from metricflow.protocols.query_parameter import QueryParameterDimension, QueryParameterMetric
from metricflow.protocols.sql_client import SqlClient
from metricflow.query.query_exceptions import InvalidQueryException
from metricflow.query.query_parser import MetricFlowQueryParser
from metricflow.random_id import random_id
from metricflow.specs.column_assoc import ColumnAssociationResolver
from metricflow.specs.query_interface import QueryInterfaceMetric, QueryParameter
from metricflow.specs.specs import InstanceSpecSet, MetricFlowQuerySpec
from metricflow.sql.optimizer.optimization_levels import SqlQueryOptimizationLevel
from metricflow.telemetry.models import TelemetryLevel
Expand Down Expand Up @@ -98,31 +98,31 @@ class MetricFlowQueryRequest:

request_id: MetricFlowRequestId
metric_names: Optional[Sequence[str]] = None
metrics: Optional[Sequence[QueryInterfaceMetric]] = None
metrics: Optional[Sequence[QueryParameterMetric]] = None
group_by_names: Optional[Sequence[str]] = None
group_by: Optional[Sequence[QueryParameter]] = None
group_by: Optional[Sequence[QueryParameterDimension]] = None
limit: Optional[int] = None
time_constraint_start: Optional[datetime.datetime] = None
time_constraint_end: Optional[datetime.datetime] = None
where_constraint: Optional[str] = None
order_by_names: Optional[Sequence[str]] = None
order_by: Optional[Sequence[QueryParameter]] = None
order_by: Optional[Sequence[QueryParameterDimension]] = None
output_table: Optional[str] = None
sql_optimization_level: SqlQueryOptimizationLevel = SqlQueryOptimizationLevel.O4
query_type: MetricFlowQueryType = MetricFlowQueryType.METRIC

@staticmethod
def create_with_random_request_id( # noqa: D
metric_names: Optional[Sequence[str]] = None,
metrics: Optional[Sequence[QueryInterfaceMetric]] = None,
metrics: Optional[Sequence[QueryParameterMetric]] = None,
group_by_names: Optional[Sequence[str]] = None,
group_by: Optional[Sequence[QueryParameter]] = None,
group_by: Optional[Sequence[QueryParameterDimension]] = None,
limit: Optional[int] = None,
time_constraint_start: Optional[datetime.datetime] = None,
time_constraint_end: Optional[datetime.datetime] = None,
where_constraint: Optional[str] = None,
order_by_names: Optional[Sequence[str]] = None,
order_by: Optional[Sequence[QueryParameter]] = None,
order_by: Optional[Sequence[QueryParameterDimension]] = None,
output_table: Optional[str] = None,
sql_optimization_level: SqlQueryOptimizationLevel = SqlQueryOptimizationLevel.O4,
query_type: MetricFlowQueryType = MetricFlowQueryType.METRIC,
Expand Down Expand Up @@ -286,9 +286,9 @@ def get_dimension_values(
def explain_get_dimension_values( # noqa: D
self,
metric_names: Optional[List[str]] = None,
metrics: Optional[Sequence[QueryInterfaceMetric]] = None,
metrics: Optional[Sequence[QueryParameterMetric]] = None,
get_group_by_values: Optional[str] = None,
group_by: Optional[QueryParameter] = None,
group_by: Optional[QueryParameterDimension] = None,
time_constraint_start: Optional[datetime.datetime] = None,
time_constraint_end: Optional[datetime.datetime] = None,
) -> MetricFlowExplainResult:
Expand Down Expand Up @@ -682,9 +682,9 @@ def get_dimension_values( # noqa: D
def explain_get_dimension_values( # noqa: D
self,
metric_names: Optional[List[str]] = None,
metrics: Optional[Sequence[QueryInterfaceMetric]] = None,
metrics: Optional[Sequence[QueryParameterMetric]] = None,
get_group_by_values: Optional[str] = None,
group_by: Optional[QueryParameter] = None,
group_by: Optional[QueryParameterDimension] = None,
time_constraint_start: Optional[datetime.datetime] = None,
time_constraint_end: Optional[datetime.datetime] = None,
) -> MetricFlowExplainResult:
Expand Down
7 changes: 7 additions & 0 deletions metricflow/errors/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,10 @@ class SqlBindParametersNotSupportedError(Exception):

class UnknownMetricLinkingError(ValueError):
"""Raised during linking when a user attempts to use a metric that isn't specified."""


class InvalidQuerySyntax(Exception):
"""Raised when query syntax is invalid. Primarily used in the where clause."""

def __init__(self, msg: str) -> None: # noqa: D
super().__init__(msg)
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,12 @@

from typing import Optional, Protocol, Sequence

from dbt_semantic_interfaces.type_enums import TimeGranularity

from metricflow.time.date_part import DatePart


class QueryInterfaceMetric(Protocol):
"""Metric in the query interface."""

@property
def name(self) -> str:
"""The name of the metric."""
raise NotImplementedError
"""Represents the interface for Metric in the query interface."""


class QueryParameter(Protocol):
"""A query parameter with a grain."""

@property
def name(self) -> str:
"""The name of the item."""
raise NotImplementedError

@property
def grain(self) -> Optional[TimeGranularity]:
"""The time granularity."""
raise NotImplementedError

@property
def date_part(self) -> Optional[DatePart]:
"""Date part to extract from the dimension."""
def descending(self, _is_descending: bool) -> QueryInterfaceMetric:
"""Set the sort order for order-by."""
raise NotImplementedError


Expand All @@ -46,6 +22,9 @@ def alias(self, _alias: str) -> QueryInterfaceDimension:
"""Renaming the column."""
raise NotImplementedError

def descending(self, _is_descending: bool) -> QueryInterfaceDimension:
"""Set the sort order for order-by."""

def date_part(self, _date_part: str) -> QueryInterfaceDimension:
"""Date part to extract from the dimension."""
raise NotImplementedError
Expand Down Expand Up @@ -78,6 +57,7 @@ def create(
self,
time_dimension_name: str,
time_granularity_name: str,
descending: bool = False,
date_part_name: Optional[str] = None,
entity_path: Sequence[str] = (),
) -> QueryInterfaceTimeDimension:
Expand All @@ -88,7 +68,9 @@ def create(
class QueryInterfaceEntity(Protocol):
"""Represents the interface for Entity in the query interface."""

pass
def descending(self, _is_descending: bool) -> QueryInterfaceEntity:
"""Set the sort order for order-by."""
raise NotImplementedError


class QueryInterfaceEntityFactory(Protocol):
Expand Down
45 changes: 45 additions & 0 deletions metricflow/protocols/query_parameter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from __future__ import annotations

from typing import Optional, Protocol

from dbt_semantic_interfaces.type_enums import TimeGranularity

from metricflow.time.date_part import DatePart


class QueryParameterDimension(Protocol):
"""A query parameter with a grain."""

@property
def name(self) -> str:
"""The name of the item."""
raise NotImplementedError

@property
def grain(self) -> Optional[TimeGranularity]:
"""The time granularity."""
raise NotImplementedError

@property
def descending(self) -> bool:
"""Set the sort order for order-by."""
raise NotImplementedError

@property
def date_part(self) -> Optional[DatePart]:
"""Date part to extract from the dimension."""
raise NotImplementedError


class QueryParameterMetric(Protocol):
"""Metric in the query interface."""

@property
def name(self) -> str:
"""The name of the metric."""
raise NotImplementedError

@property
def descending(self) -> bool:
"""Set the sort order for order-by."""
raise NotImplementedError
22 changes: 12 additions & 10 deletions metricflow/query/query_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
from metricflow.filters.time_constraint import TimeRangeConstraint
from metricflow.model.semantic_manifest_lookup import SemanticManifestLookup
from metricflow.naming.linkable_spec_name import StructuredLinkableSpecName
from metricflow.protocols.query_parameter import QueryParameterDimension, QueryParameterMetric
from metricflow.query.query_exceptions import InvalidQueryException
from metricflow.specs.column_assoc import ColumnAssociationResolver
from metricflow.specs.query_interface import QueryInterfaceMetric, QueryParameter
from metricflow.specs.specs import (
DimensionSpec,
EntitySpec,
Expand Down Expand Up @@ -169,16 +169,16 @@ def _top_fuzzy_matches(
def parse_and_validate_query(
self,
metric_names: Optional[Sequence[str]] = None,
metrics: Optional[Sequence[QueryInterfaceMetric]] = None,
metrics: Optional[Sequence[QueryParameterMetric]] = None,
group_by_names: Optional[Sequence[str]] = None,
group_by: Optional[Sequence[QueryParameter]] = None,
group_by: Optional[Sequence[QueryParameterDimension]] = None,
limit: Optional[int] = None,
time_constraint_start: Optional[datetime.datetime] = None,
time_constraint_end: Optional[datetime.datetime] = None,
where_constraint: Optional[WhereFilter] = None,
where_constraint_str: Optional[str] = None,
order: Optional[Sequence[str]] = None,
order_by: Optional[Sequence[QueryParameter]] = None,
order_by: Optional[Sequence[QueryParameterDimension]] = None,
time_granularity: Optional[TimeGranularity] = None,
) -> MetricFlowQuerySpec:
"""Parse the query into spec objects, validating them in the process.
Expand Down Expand Up @@ -290,7 +290,7 @@ def _construct_metric_specs_for_query(
return tuple(metric_specs)

def _get_metric_names(
self, metric_names: Optional[Sequence[str]], metrics: Optional[Sequence[QueryInterfaceMetric]]
self, metric_names: Optional[Sequence[str]], metrics: Optional[Sequence[QueryParameterMetric]]
) -> Sequence[str]:
assert_exactly_one_arg_set(metric_names=metric_names, metrics=metrics)
return metric_names if metric_names else [m.name for m in metrics] if metrics else []
Expand All @@ -307,7 +307,9 @@ def _get_where_filter(
PydanticWhereFilter(where_sql_template=where_constraint_str) if where_constraint_str else where_constraint
)

def _get_order(self, order: Optional[Sequence[str]], order_by: Optional[Sequence[QueryParameter]]) -> Sequence[str]:
def _get_order(
self, order: Optional[Sequence[str]], order_by: Optional[Sequence[QueryParameterDimension]]
) -> Sequence[str]:
assert not (
order and order_by
), "Both order_by_names and order_by were set, but if an order by is specified you should only use one of these!"
Expand All @@ -316,16 +318,16 @@ def _get_order(self, order: Optional[Sequence[str]], order_by: Optional[Sequence
def _parse_and_validate_query(
self,
metric_names: Optional[Sequence[str]] = None,
metrics: Optional[Sequence[QueryInterfaceMetric]] = None,
metrics: Optional[Sequence[QueryParameterMetric]] = None,
group_by_names: Optional[Sequence[str]] = None,
group_by: Optional[Sequence[QueryParameter]] = None,
group_by: Optional[Sequence[QueryParameterDimension]] = None,
limit: Optional[int] = None,
time_constraint_start: Optional[datetime.datetime] = None,
time_constraint_end: Optional[datetime.datetime] = None,
where_constraint: Optional[WhereFilter] = None,
where_constraint_str: Optional[str] = None,
order: Optional[Sequence[str]] = None,
order_by: Optional[Sequence[QueryParameter]] = None,
order_by: Optional[Sequence[QueryParameterDimension]] = None,
time_granularity: Optional[TimeGranularity] = None,
) -> MetricFlowQuerySpec:
metric_names = self._get_metric_names(metric_names, metrics)
Expand Down Expand Up @@ -665,7 +667,7 @@ def _parse_linkable_elements(
self,
metric_references: Sequence[MetricReference],
qualified_linkable_names: Optional[Sequence[str]] = None,
linkable_elements: Optional[Sequence[QueryParameter]] = None,
linkable_elements: Optional[Sequence[QueryParameterDimension]] = None,
) -> QueryTimeLinkableSpecSet:
"""Convert the linkable spec names into the respective specification objects."""
# TODO: refactor to only support group_by object inputs (removing group_by_names param)
Expand Down
1 change: 1 addition & 0 deletions metricflow/specs/query_param_implementations.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class DimensionQueryParameter:

name: str
grain: Optional[TimeGranularity] = None
descending: bool = False
date_part: Optional[DatePart] = None

def __post_init__(self) -> None: # noqa: D
Expand Down
11 changes: 9 additions & 2 deletions metricflow/specs/where_filter_dimension.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
)
from typing_extensions import override

from metricflow.specs.column_assoc import ColumnAssociationResolver
from metricflow.specs.query_interface import (
from metricflow.errors.errors import InvalidQuerySyntax
from metricflow.protocols.query_interface import (
QueryInterfaceDimension,
QueryInterfaceDimensionFactory,
)
from metricflow.specs.column_assoc import ColumnAssociationResolver
from metricflow.specs.specs import DimensionSpec


Expand All @@ -44,6 +45,12 @@ def alias(self, _alias: str) -> QueryInterfaceDimension:
"""Renaming the column."""
raise NotImplementedError

def descending(self, _is_descending: bool) -> QueryInterfaceDimension:
"""Set the sort order for order-by."""
raise InvalidQuerySyntax(
"Can't set descending in the where clause. Try setting descending in the order_by clause instead"
)

def __str__(self) -> str:
"""Returns the column name.

Expand Down
9 changes: 8 additions & 1 deletion metricflow/specs/where_filter_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
from dbt_semantic_interfaces.references import EntityReference
from typing_extensions import override

from metricflow.errors.errors import InvalidQuerySyntax
from metricflow.protocols.query_interface import QueryInterfaceEntity, QueryInterfaceEntityFactory
from metricflow.specs.column_assoc import ColumnAssociationResolver
from metricflow.specs.query_interface import QueryInterfaceEntity, QueryInterfaceEntityFactory
from metricflow.specs.specs import EntitySpec


Expand All @@ -26,6 +27,12 @@ def _implements_protocol(self) -> QueryInterfaceEntity:
def __init__(self, column_name: str): # noqa
self.column_name = column_name

def descending(self, _is_descending: bool) -> QueryInterfaceEntity:
"""Set the sort order for order-by."""
raise InvalidQuerySyntax(
"Can't set descending in the where clause. Try setting descending in the order_by clause instead"
)

def __str__(self) -> str:
"""Returns the column name.

Expand Down
8 changes: 7 additions & 1 deletion metricflow/specs/where_filter_time_dimension.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
from dbt_semantic_interfaces.type_enums.time_granularity import TimeGranularity
from typing_extensions import override

from metricflow.errors.errors import InvalidQuerySyntax
from metricflow.protocols.query_interface import QueryInterfaceTimeDimension, QueryInterfaceTimeDimensionFactory
from metricflow.specs.column_assoc import ColumnAssociationResolver
from metricflow.specs.query_interface import QueryInterfaceTimeDimension, QueryInterfaceTimeDimensionFactory
from metricflow.specs.specs import TimeDimensionSpec


Expand Down Expand Up @@ -55,10 +56,15 @@ def create(
self,
time_dimension_name: str,
time_granularity_name: str,
descending: bool = False,
date_part_name: Optional[str] = None,
entity_path: Sequence[str] = (),
) -> WhereFilterTimeDimension:
"""Create a WhereFilterTimeDimension."""
if descending:
raise InvalidQuerySyntax(
"Can't set descending in the where clause. Try setting descending in the order_by clause instead"
)
structured_name = DunderedNameFormatter.parse_name(time_dimension_name)
call_parameter_set = TimeDimensionCallParameterSet(
time_dimension_reference=TimeDimensionReference(element_name=structured_name.element_name),
Expand Down
3 changes: 2 additions & 1 deletion metricflow/test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@


@dataclass
class MockQueryParameter:
class MockQueryParameterDimension:
"""This is a mock that is just used to test the query parser."""

name: str
grain: Optional[TimeGranularity] = None
descending: bool = False
date_part: Optional[DatePart] = None
Loading