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 4 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 @@ -52,7 +52,7 @@
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.query_parameter import QueryParameterDimension, QueryParameterMetric
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 @@ -681,9 +681,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)
22 changes: 12 additions & 10 deletions metricflow/query/query_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from metricflow.naming.linkable_spec_name import StructuredLinkableSpecName
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.query_parameter import QueryParameterDimension, QueryParameterMetric
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_group_by_names(
self, group_by_names: Optional[Sequence[str]], group_by: Optional[Sequence[QueryParameter]]
self, group_by_names: Optional[Sequence[str]], group_by: Optional[Sequence[QueryParameterDimension]]
) -> Sequence[str]:
assert not (
group_by_names and group_by
Expand All @@ -304,7 +304,7 @@ def _get_group_by_names(
)

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 @@ -321,7 +321,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 @@ -330,16 +332,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
34 changes: 12 additions & 22 deletions metricflow/specs/query_interface.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,13 @@
from __future__ import annotations

from typing import Optional, Protocol, Sequence

from dbt_semantic_interfaces.type_enums import TimeGranularity
from typing import Protocol, Sequence


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

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


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

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

@property
def grain(self) -> Optional[TimeGranularity]:
"""The time granularity."""
def descending(self, _is_descending: bool) -> QueryInterfaceMetric:
"""Set the sort order for order-by."""
raise NotImplementedError


Expand All @@ -39,6 +22,10 @@ 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 NotImplementedError


class QueryInterfaceDimensionFactory(Protocol):
"""Creates a Dimension for the query interface.
Expand Down Expand Up @@ -67,6 +54,7 @@ def create(
self,
time_dimension_name: str,
time_granularity_name: str,
descending: bool = False,
entity_path: Sequence[str] = (),
) -> QueryInterfaceTimeDimension:
"""Create a TimeDimension."""
Expand All @@ -76,7 +64,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
38 changes: 38 additions & 0 deletions metricflow/specs/query_parameter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from __future__ import annotations
DevonFulcher marked this conversation as resolved.
Show resolved Hide resolved

from typing import Optional, Protocol

from dbt_semantic_interfaces.type_enums import TimeGranularity


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


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
7 changes: 7 additions & 0 deletions metricflow/specs/where_filter_dimension.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
)
from typing_extensions import override

from metricflow.errors.errors import InvalidQuerySyntax
from metricflow.specs.column_assoc import ColumnAssociationResolver
from metricflow.specs.query_interface import (
QueryInterfaceDimension,
Expand All @@ -40,6 +41,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
7 changes: 7 additions & 0 deletions metricflow/specs/where_filter_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from dbt_semantic_interfaces.references import EntityReference
from typing_extensions import override

from metricflow.errors.errors import InvalidQuerySyntax
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
11 changes: 10 additions & 1 deletion metricflow/specs/where_filter_time_dimension.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from dbt_semantic_interfaces.type_enums.time_granularity import TimeGranularity
from typing_extensions import override

from metricflow.errors.errors import InvalidQuerySyntax
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 @@ -52,9 +53,17 @@ def __init__( # noqa
self.time_dimension_specs: List[TimeDimensionSpec] = []

def create(
self, time_dimension_name: str, time_granularity_name: str, entity_path: Sequence[str] = ()
self,
time_dimension_name: str,
time_granularity_name: str,
descending: bool = False,
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
5 changes: 3 additions & 2 deletions metricflow/test/query/test_query_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,14 +175,15 @@ class MockQueryParameter:
"""This is a mock that is just used to test the query parser."""

grain = None
descending = False

def __init__(self, name: str): # noqa: D
self.name = name


def test_query_parser_with_object_params(bookings_query_parser: MetricFlowQueryParser) -> None: # noqa: D
Metric = namedtuple("Metric", ["name"])
metric = Metric("bookings")
Metric = namedtuple("Metric", ["name", "descending"])
metric = Metric("bookings", False)
group_by = [
MockQueryParameter("booking__is_instant"),
MockQueryParameter("listing"),
Expand Down
11 changes: 11 additions & 0 deletions metricflow/test/specs/test_where_filter_dimension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from __future__ import annotations

import pytest

from metricflow.errors.errors import InvalidQuerySyntax
from metricflow.specs.where_filter_dimension import WhereFilterDimension


def test_descending_cannot_be_set() -> None: # noqa
with pytest.raises(InvalidQuerySyntax):
WhereFilterDimension("bookings").descending(True)
11 changes: 11 additions & 0 deletions metricflow/test/specs/test_where_filter_entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from __future__ import annotations

import pytest

from metricflow.errors.errors import InvalidQuerySyntax
from metricflow.specs.where_filter_entity import WhereFilterEntity


def test_descending_cannot_be_set() -> None: # noqa
with pytest.raises(InvalidQuerySyntax):
WhereFilterEntity("customer").descending(True)