diff --git a/.changes/unreleased/Features-20241112-154335.yaml b/.changes/unreleased/Features-20241112-154335.yaml new file mode 100644 index 0000000..0f12605 --- /dev/null +++ b/.changes/unreleased/Features-20241112-154335.yaml @@ -0,0 +1,3 @@ +kind: Features +body: Support for adhoc queries with only groupby. This is equivalent to listing dimension values. +time: 2024-11-12T15:43:35.746064+01:00 diff --git a/dbtsl/api/graphql/client/asyncio.pyi b/dbtsl/api/graphql/client/asyncio.pyi index 7006af2..df84431 100644 --- a/dbtsl/api/graphql/client/asyncio.pyi +++ b/dbtsl/api/graphql/client/asyncio.pyi @@ -55,6 +55,15 @@ class AsyncGraphQLClient: read_cache: bool = True, ) -> str: ... @overload + async def compile_sql( + self, + group_by: List[str], + limit: Optional[int] = None, + order_by: Optional[List[Union[str, OrderByGroupBy]]] = None, + where: Optional[List[str]] = None, + read_cache: bool = True, + ) -> str: ... + @overload async def compile_sql( self, saved_query: str, @@ -78,6 +87,15 @@ class AsyncGraphQLClient: read_cache: bool = True, ) -> "pa.Table": ... @overload + async def query( + self, + group_by: List[str], + limit: Optional[int] = None, + order_by: Optional[List[Union[str, OrderByGroupBy]]] = None, + where: Optional[List[str]] = None, + read_cache: bool = True, + ) -> "pa.Table": ... + @overload async def query( self, saved_query: str, diff --git a/dbtsl/api/graphql/client/sync.pyi b/dbtsl/api/graphql/client/sync.pyi index c424dc0..d29ccae 100644 --- a/dbtsl/api/graphql/client/sync.pyi +++ b/dbtsl/api/graphql/client/sync.pyi @@ -55,6 +55,15 @@ class SyncGraphQLClient: read_cache: bool = True, ) -> str: ... @overload + def compile_sql( + self, + group_by: List[str], + limit: Optional[int] = None, + order_by: Optional[List[Union[str, OrderByGroupBy]]] = None, + where: Optional[List[str]] = None, + read_cache: bool = True, + ) -> str: ... + @overload def compile_sql( self, saved_query: str, @@ -78,6 +87,15 @@ class SyncGraphQLClient: read_cache: bool = True, ) -> "pa.Table": ... @overload + def query( + self, + group_by: List[str], + limit: Optional[int] = None, + order_by: Optional[List[Union[str, OrderByGroupBy]]] = None, + where: Optional[List[str]] = None, + read_cache: bool = True, + ) -> "pa.Table": ... + @overload def query( self, saved_query: str, diff --git a/dbtsl/api/graphql/protocol.py b/dbtsl/api/graphql/protocol.py index d7faa3a..4115f99 100644 --- a/dbtsl/api/graphql/protocol.py +++ b/dbtsl/api/graphql/protocol.py @@ -219,7 +219,7 @@ def get_query_request_variables(environment_id: int, params: QueryParameters) -> if isinstance(strict_params, AdhocQueryParametersStrict): return { "savedQuery": None, - "metrics": [{"name": m} for m in strict_params.metrics], + "metrics": [{"name": m} for m in strict_params.metrics] if strict_params.metrics is not None else None, "groupBy": [{"name": g} for g in strict_params.group_by] if strict_params.group_by is not None else None, **shared_vars, } diff --git a/dbtsl/api/shared/query_params.py b/dbtsl/api/shared/query_params.py index 538de28..3537715 100644 --- a/dbtsl/api/shared/query_params.py +++ b/dbtsl/api/shared/query_params.py @@ -44,7 +44,7 @@ class QueryParameters(TypedDict, total=False): class AdhocQueryParametersStrict: """The parameters of an adhoc query, strictly validated.""" - metrics: List[str] + metrics: Optional[List[str]] group_by: Optional[List[str]] limit: Optional[int] order_by: Optional[List[OrderBySpec]] @@ -125,11 +125,8 @@ def validate_query_parameters( **shared_params, ) - if "metrics" not in params or len(params["metrics"]) == 0: - raise ValueError("You need to specify at least one metric.") - return AdhocQueryParametersStrict( - metrics=params["metrics"], + metrics=params.get("metrics"), group_by=params.get("group_by"), **shared_params, ) diff --git a/dbtsl/client/asyncio.pyi b/dbtsl/client/asyncio.pyi index 08a78f2..94c4148 100644 --- a/dbtsl/client/asyncio.pyi +++ b/dbtsl/client/asyncio.pyi @@ -25,6 +25,15 @@ class AsyncSemanticLayerClient: read_cache: bool = True, ) -> str: ... @overload + async def compile_sql( + self, + group_by: List[str], + limit: Optional[int] = None, + order_by: Optional[List[Union[str, OrderByGroupBy]]] = None, + where: Optional[List[str]] = None, + read_cache: bool = True, + ) -> str: ... + @overload async def compile_sql( self, saved_query: str, @@ -48,6 +57,15 @@ class AsyncSemanticLayerClient: read_cache: bool = True, ) -> "pa.Table": ... @overload + async def query( + self, + group_by: List[str], + limit: Optional[int] = None, + order_by: Optional[List[Union[str, OrderByGroupBy]]] = None, + where: Optional[List[str]] = None, + read_cache: bool = True, + ) -> "pa.Table": ... + @overload async def query( self, saved_query: str, diff --git a/dbtsl/client/sync.pyi b/dbtsl/client/sync.pyi index 1b0f577..ffdf59e 100644 --- a/dbtsl/client/sync.pyi +++ b/dbtsl/client/sync.pyi @@ -25,6 +25,15 @@ class SyncSemanticLayerClient: read_cache: bool = True, ) -> str: ... @overload + def compile_sql( + self, + group_by: List[str], + limit: Optional[int] = None, + order_by: Optional[List[Union[str, OrderByGroupBy]]] = None, + where: Optional[List[str]] = None, + read_cache: bool = True, + ) -> str: ... + @overload def compile_sql( self, saved_query: str, @@ -48,6 +57,15 @@ class SyncSemanticLayerClient: read_cache: bool = True, ) -> "pa.Table": ... @overload + def query( + self, + group_by: List[str], + limit: Optional[int] = None, + order_by: Optional[List[Union[str, OrderByGroupBy]]] = None, + where: Optional[List[str]] = None, + read_cache: bool = True, + ) -> "pa.Table": ... + @overload def query( self, saved_query: str, diff --git a/tests/query_test_cases.py b/tests/query_test_cases.py index 70c4af8..7480937 100644 --- a/tests/query_test_cases.py +++ b/tests/query_test_cases.py @@ -17,6 +17,10 @@ { "metrics": ["order_total"], }, + # ad hoc query, only group by + { + "group_by": ["customer__customer_type"], + }, # ad hoc query, metric and group by { "metrics": ["order_total"], diff --git a/tests/test_models.py b/tests/test_models.py index 49c3fff..3b98320 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -178,7 +178,7 @@ def test_validate_order_by_not_found() -> None: validate_order_by(["a"], ["b"], "c") -def test_validate_query_params_adhoc_query_valid() -> None: +def test_validate_query_params_adhoc_query_valid_metrics_and_groupby() -> None: p: QueryParameters = { "metrics": ["a", "b"], "group_by": ["c", "d"], @@ -197,6 +197,18 @@ def test_validate_query_params_adhoc_query_valid() -> None: assert not r.read_cache +def test_validate_query_params_adhoc_query_valid_only_groupby() -> None: + p: QueryParameters = {"group_by": ["gb"], "limit": 1, "where": ["1=1"], "order_by": ["gb"], "read_cache": False} + r = validate_query_parameters(p) + assert isinstance(r, AdhocQueryParametersStrict) + assert r.metrics is None + assert r.group_by == ["gb"] + assert r.order_by == [OrderByGroupBy(name="gb", grain=None)] + assert r.where == ["1=1"] + assert r.limit == 1 + assert not r.read_cache + + def test_validate_query_params_saved_query_valid() -> None: p: QueryParameters = { "saved_query": "a", @@ -214,15 +226,6 @@ def test_validate_query_params_saved_query_valid() -> None: assert not r.read_cache -def test_validate_query_params_adhoc_query_no_metrics() -> None: - p: QueryParameters = { - "metrics": [], - "group_by": ["a", "b"], - } - with pytest.raises(ValueError): - validate_query_parameters(p) - - def test_validate_query_params_saved_query_group_by() -> None: p: QueryParameters = { "saved_query": "sq", @@ -239,6 +242,6 @@ def test_validate_query_params_adhoc_and_saved_query() -> None: def test_validate_query_params_no_query() -> None: - p: QueryParameters = {"group_by": ["gb"], "limit": 1, "where": ["1=1"], "order_by": ["a"], "read_cache": False} + p: QueryParameters = {"limit": 1, "where": ["1=1"], "order_by": ["a"], "read_cache": False} with pytest.raises(ValueError): validate_query_parameters(p)