You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by be...@apache.org on 2025/10/27 16:50:16 UTC
(superset) branch explorable updated: Add unit tests
This is an automated email from the ASF dual-hosted git repository.
beto pushed a commit to branch explorable
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/explorable by this push:
new 5ba6db46a7 Add unit tests
5ba6db46a7 is described below
commit 5ba6db46a788b73f4c39ff25b16f6c95bd0b7a86
Author: Beto Dealmeida <ro...@dealmeida.net>
AuthorDate: Mon Oct 27 12:50:06 2025 -0400
Add unit tests
---
superset/semantic_layers/mapper.py | 12 +-
tests/unit_tests/semantic_layers/__init__.py | 16 +
tests/unit_tests/semantic_layers/test_mapper.py | 1153 +++++++++++++++++++++++
3 files changed, 1179 insertions(+), 2 deletions(-)
diff --git a/superset/semantic_layers/mapper.py b/superset/semantic_layers/mapper.py
index 1cd41e02a9..eaa3ee9f50 100644
--- a/superset/semantic_layers/mapper.py
+++ b/superset/semantic_layers/mapper.py
@@ -84,7 +84,9 @@ def map_query_object(query_object: QueryObject) -> list[SemanticQuery]:
queries = []
for time_offset in [None] + query_object.time_offsets:
filters = _get_filters_from_query_object(
- query_object, time_offset, all_dimensions
+ query_object,
+ time_offset,
+ all_dimensions,
)
queries.append(
@@ -247,7 +249,13 @@ def _get_time_bounds(
return query_object.from_dttm, query_object.to_dttm
# Time offset query: calculate shifted bounds
- outer_from, outer_to = get_since_until_from_query_object(query_object)
+ # Use from_dttm/to_dttm if available, otherwise try to get from time_range
+ outer_from = query_object.from_dttm
+ outer_to = query_object.to_dttm
+
+ if not outer_from or not outer_to:
+ # Fall back to parsing time_range if from_dttm/to_dttm not set
+ outer_from, outer_to = get_since_until_from_query_object(query_object)
if not outer_from or not outer_to:
return None, None
diff --git a/tests/unit_tests/semantic_layers/__init__.py b/tests/unit_tests/semantic_layers/__init__.py
new file mode 100644
index 0000000000..13a83393a9
--- /dev/null
+++ b/tests/unit_tests/semantic_layers/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/tests/unit_tests/semantic_layers/test_mapper.py b/tests/unit_tests/semantic_layers/test_mapper.py
new file mode 100644
index 0000000000..abe9e08096
--- /dev/null
+++ b/tests/unit_tests/semantic_layers/test_mapper.py
@@ -0,0 +1,1153 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from datetime import datetime
+from unittest.mock import Mock
+
+import pytest
+
+from superset.common.query_object import QueryObject
+from superset.semantic_layers.mapper import (
+ _convert_query_object_filter,
+ _convert_time_grain,
+ _get_filters_from_extras,
+ _get_filters_from_query_object,
+ _get_group_limit_filters,
+ _get_group_limit_from_query_object,
+ _get_order_from_query_object,
+ _get_time_bounds,
+ _get_time_filter,
+ map_query_object,
+)
+from superset.semantic_layers.types import (
+ AdhocExpression,
+ AdhocFilter,
+ DateGrain,
+ Dimension,
+ Filter,
+ GroupLimit,
+ INTEGER,
+ Metric,
+ NUMBER,
+ Operator,
+ OrderDirection,
+ PredicateType,
+ SemanticQuery,
+ SemanticViewFeature,
+ STRING,
+ TimeGrain,
+)
+from superset.utils.core import FilterOperator
+
+
+class MockSemanticViewImplementation:
+ """
+ Mock implementation of SemanticViewImplementation protocol.
+ """
+
+ def __init__(
+ self,
+ dimensions: set[Dimension],
+ metrics: set[Metric],
+ features: frozenset[SemanticViewFeature],
+ ):
+ self.dimensions = dimensions
+ self.metrics = metrics
+ self.features = features
+
+ def uid(self) -> str:
+ return "mock_semantic_view"
+
+ def get_dimensions(self) -> set[Dimension]:
+ return self.dimensions
+
+ def get_metrics(self) -> set[Metric]:
+ return self.metrics
+
+
+@pytest.fixture
+def mock_datasource() -> Mock:
+ """
+ Create a mock datasource with semantic view implementation.
+ """
+ datasource = Mock()
+
+ # Create dimensions
+ time_dim = Dimension(
+ id="orders.order_date",
+ name="order_date",
+ type=STRING,
+ description="Order date",
+ definition="order_date",
+ )
+ category_dim = Dimension(
+ id="products.category",
+ name="category",
+ type=STRING,
+ description="Product category",
+ definition="category",
+ )
+ region_dim = Dimension(
+ id="customers.region",
+ name="region",
+ type=STRING,
+ description="Customer region",
+ definition="region",
+ )
+
+ # Create metrics
+ sales_metric = Metric(
+ id="orders.total_sales",
+ name="total_sales",
+ type=NUMBER,
+ definition="SUM(amount)",
+ description="Total sales",
+ )
+ count_metric = Metric(
+ id="orders.order_count",
+ name="order_count",
+ type=INTEGER,
+ definition="COUNT(*)",
+ description="Order count",
+ )
+
+ # Create semantic view implementation
+ implementation = MockSemanticViewImplementation(
+ dimensions={time_dim, category_dim, region_dim},
+ metrics={sales_metric, count_metric},
+ features=frozenset(
+ {
+ SemanticViewFeature.GROUP_LIMIT,
+ SemanticViewFeature.GROUP_OTHERS,
+ }
+ ),
+ )
+
+ datasource.implementation = implementation
+ datasource.fetch_values_predicate = None
+
+ return datasource
+
+
+def test_convert_time_grain_time() -> None:
+ """
+ Test conversion of time grains (hour, minute, second).
+ """
+ assert _convert_time_grain("PT1S") == TimeGrain.PT1S
+ assert _convert_time_grain("PT1M") == TimeGrain.PT1M
+ assert _convert_time_grain("PT1H") == TimeGrain.PT1H
+
+
+def test_convert_time_grain_date() -> None:
+ """
+ Test conversion of date grains (day, week, month, year).
+ """
+ assert _convert_time_grain("P1D") == DateGrain.P1D
+ assert _convert_time_grain("P1W") == DateGrain.P1W
+ assert _convert_time_grain("P1M") == DateGrain.P1M
+ assert _convert_time_grain("P1Y") == DateGrain.P1Y
+
+
+def test_convert_time_grain_invalid() -> None:
+ """
+ Test that invalid grain returns None.
+ """
+ assert _convert_time_grain("INVALID") is None
+ assert _convert_time_grain("") is None
+
+
+def test_get_filters_from_extras_empty() -> None:
+ """
+ Test that empty extras returns empty set.
+ """
+ result = _get_filters_from_extras({})
+ assert result == set()
+
+
+def test_get_filters_from_extras_where() -> None:
+ """
+ Test extraction of WHERE clause from extras.
+ """
+ extras = {"where": "customer_id > 100"}
+ result = _get_filters_from_extras(extras)
+
+ assert len(result) == 1
+ filter_ = next(iter(result))
+ assert isinstance(filter_, AdhocFilter)
+ assert filter_.type == PredicateType.WHERE
+ assert filter_.definition == "customer_id > 100"
+
+
+def test_get_filters_from_extras_having() -> None:
+ """
+ Test extraction of HAVING clause from extras.
+ """
+ extras = {"having": "SUM(sales) > 1000"}
+ result = _get_filters_from_extras(extras)
+
+ assert result == {
+ AdhocFilter(type=PredicateType.HAVING, definition="SUM(sales) > 1000"),
+ }
+
+
+def test_get_filters_from_extras_both() -> None:
+ """
+ Test extraction of both WHERE and HAVING from extras.
+ """
+ extras = {
+ "where": "region = 'US'",
+ "having": "COUNT(*) > 10",
+ }
+ result = _get_filters_from_extras(extras)
+
+ assert result == {
+ AdhocFilter(type=PredicateType.WHERE, definition="region = 'US'"),
+ AdhocFilter(type=PredicateType.HAVING, definition="COUNT(*) > 10"),
+ }
+
+
+def test_get_time_bounds_no_offset(mock_datasource):
+ """
+ Test time bounds without offset.
+ """
+ from_dttm = datetime(2025, 10, 15, 0, 0, 0)
+ to_dttm = datetime(2025, 10, 22, 23, 59, 59)
+
+ query_object = QueryObject(
+ datasource=mock_datasource,
+ from_dttm=from_dttm,
+ to_dttm=to_dttm,
+ metrics=["total_sales"],
+ columns=["category"],
+ )
+
+ result_from, result_to = _get_time_bounds(query_object, None)
+
+ assert result_from == from_dttm
+ assert result_to == to_dttm
+
+
+def test_get_time_filter_no_granularity(mock_datasource):
+ """
+ Test that no time filter is created without granularity.
+ """
+ query_object = QueryObject(
+ datasource=mock_datasource,
+ from_dttm=datetime(2025, 10, 15),
+ to_dttm=datetime(2025, 10, 22),
+ metrics=["total_sales"],
+ columns=["category"],
+ granularity=None,
+ )
+
+ all_dimensions = {
+ dim.name: dim for dim in mock_datasource.implementation.dimensions
+ }
+
+ result = _get_time_filter(query_object, None, all_dimensions)
+
+ assert result == set()
+
+
+def test_get_time_filter_with_granularity(mock_datasource):
+ """
+ Test time filter creation with granularity.
+ """
+ from_dttm = datetime(2025, 10, 15, 0, 0, 0)
+ to_dttm = datetime(2025, 10, 22, 23, 59, 59)
+
+ query_object = QueryObject(
+ datasource=mock_datasource,
+ from_dttm=from_dttm,
+ to_dttm=to_dttm,
+ metrics=["total_sales"],
+ columns=["order_date", "category"],
+ granularity="order_date",
+ )
+
+ all_dimensions = {
+ dim.name: dim for dim in mock_datasource.implementation.dimensions
+ }
+
+ result = _get_time_filter(query_object, None, all_dimensions)
+
+ assert result == {
+ Filter(
+ type=PredicateType.WHERE,
+ column=all_dimensions["order_date"],
+ operator=Operator.GREATER_THAN_OR_EQUAL,
+ value=from_dttm,
+ ),
+ Filter(
+ type=PredicateType.WHERE,
+ column=all_dimensions["order_date"],
+ operator=Operator.LESS_THAN,
+ value=to_dttm,
+ ),
+ }
+
+
+def test_convert_query_object_filter_sql() -> None:
+ """
+ Test conversion of SQL adhoc filter.
+ """
+ all_dimensions = {}
+ filter_ = {
+ "expressionType": "SQL",
+ "sqlExpression": "customer_id > 100",
+ }
+
+ result = _convert_query_object_filter(filter_, all_dimensions)
+
+ assert result == AdhocFilter(
+ type=PredicateType.WHERE,
+ definition="customer_id > 100",
+ )
+
+
+def test_convert_query_object_filter_temporal_range() -> None:
+ """
+ Test that TEMPORAL_RANGE filters are skipped.
+ """
+ all_dimensions = {}
+ filter_ = {
+ "op": FilterOperator.TEMPORAL_RANGE.value,
+ "col": "order_date",
+ "val": "Last 7 days",
+ }
+
+ result = _convert_query_object_filter(filter_, all_dimensions)
+
+ assert result is None
+
+
+def test_convert_query_object_filter_equals(mock_datasource):
+ """
+ Test conversion of EQUALS filter.
+ """
+ all_dimensions = {
+ dim.name: dim for dim in mock_datasource.implementation.dimensions
+ }
+
+ filter_ = {
+ "op": FilterOperator.EQUALS.value,
+ "col": "region",
+ "val": "US",
+ }
+
+ result = _convert_query_object_filter(filter_, all_dimensions)
+
+ assert result == Filter(
+ type=PredicateType.WHERE,
+ column=all_dimensions["region"],
+ operator=Operator.EQUALS,
+ value="US",
+ )
+
+
+def test_convert_query_object_filter_in(mock_datasource):
+ """
+ Test conversion of IN filter.
+ """
+ all_dimensions = {
+ dim.name: dim for dim in mock_datasource.implementation.dimensions
+ }
+
+ filter_ = {
+ "op": FilterOperator.IN.value,
+ "col": "category",
+ "val": ["Electronics", "Books"],
+ }
+
+ result = _convert_query_object_filter(filter_, all_dimensions)
+
+ assert result == Filter(
+ type=PredicateType.WHERE,
+ column=all_dimensions["category"],
+ operator=Operator.IN,
+ value=["Electronics", "Books"],
+ )
+
+
+def test_convert_query_object_filter_is_null(mock_datasource):
+ """
+ Test conversion of IS_NULL filter.
+ """
+ all_dimensions = {
+ dim.name: dim for dim in mock_datasource.implementation.dimensions
+ }
+
+ filter_ = {
+ "op": FilterOperator.IS_NULL.value,
+ "col": "region",
+ }
+
+ result = _convert_query_object_filter(filter_, all_dimensions)
+
+ assert result == Filter(
+ type=PredicateType.WHERE,
+ column=all_dimensions["region"],
+ operator=Operator.IS_NULL,
+ value=None,
+ )
+
+
+def test_get_filters_from_query_object_basic(mock_datasource):
+ """
+ Test basic filter extraction from query object.
+ """
+ query_object = QueryObject(
+ datasource=mock_datasource,
+ from_dttm=datetime(2025, 10, 15),
+ to_dttm=datetime(2025, 10, 22),
+ metrics=["total_sales"],
+ columns=["order_date", "category"],
+ granularity="order_date",
+ )
+
+ all_dimensions = {
+ dim.name: dim for dim in mock_datasource.implementation.dimensions
+ }
+
+ result = _get_filters_from_query_object(query_object, None, all_dimensions)
+
+ assert result == {
+ Filter(
+ type=PredicateType.WHERE,
+ column=all_dimensions["order_date"],
+ operator=Operator.GREATER_THAN_OR_EQUAL,
+ value=datetime(2025, 10, 15),
+ ),
+ Filter(
+ type=PredicateType.WHERE,
+ column=all_dimensions["order_date"],
+ operator=Operator.LESS_THAN,
+ value=datetime(2025, 10, 22),
+ ),
+ }
+
+
+def test_get_filters_from_query_object_with_extras(mock_datasource):
+ """
+ Test filter extraction with extras.
+ """
+ query_object = QueryObject(
+ datasource=mock_datasource,
+ from_dttm=datetime(2025, 10, 15),
+ to_dttm=datetime(2025, 10, 22),
+ metrics=["total_sales"],
+ columns=["category"],
+ granularity="order_date",
+ extras={"where": "customer_id > 100"},
+ )
+
+ all_dimensions = {
+ dim.name: dim for dim in mock_datasource.implementation.dimensions
+ }
+
+ result = _get_filters_from_query_object(query_object, None, all_dimensions)
+
+ assert result == {
+ Filter(
+ type=PredicateType.WHERE,
+ column=all_dimensions["order_date"],
+ operator=Operator.GREATER_THAN_OR_EQUAL,
+ value=datetime(2025, 10, 15),
+ ),
+ Filter(
+ type=PredicateType.WHERE,
+ column=all_dimensions["order_date"],
+ operator=Operator.LESS_THAN,
+ value=datetime(2025, 10, 22),
+ ),
+ AdhocFilter(
+ type=PredicateType.WHERE,
+ definition="customer_id > 100",
+ ),
+ }
+
+
+def test_get_filters_from_query_object_with_fetch_values(mock_datasource):
+ """
+ Test filter extraction with fetch values predicate.
+ """
+ mock_datasource.fetch_values_predicate = "tenant_id = 123"
+
+ query_object = QueryObject(
+ datasource=mock_datasource,
+ from_dttm=datetime(2025, 10, 15),
+ to_dttm=datetime(2025, 10, 22),
+ metrics=["total_sales"],
+ columns=["category"],
+ granularity="order_date",
+ apply_fetch_values_predicate=True,
+ )
+
+ all_dimensions = {
+ dim.name: dim for dim in mock_datasource.implementation.dimensions
+ }
+
+ result = _get_filters_from_query_object(query_object, None, all_dimensions)
+
+ assert result == {
+ Filter(
+ type=PredicateType.WHERE,
+ column=all_dimensions["order_date"],
+ operator=Operator.GREATER_THAN_OR_EQUAL,
+ value=datetime(2025, 10, 15),
+ ),
+ Filter(
+ type=PredicateType.WHERE,
+ column=all_dimensions["order_date"],
+ operator=Operator.LESS_THAN,
+ value=datetime(2025, 10, 22),
+ ),
+ AdhocFilter(
+ type=PredicateType.WHERE,
+ definition="tenant_id = 123",
+ ),
+ }
+
+
+def test_get_order_from_query_object_metric(mock_datasource):
+ """
+ Test order extraction with metric.
+ """
+ all_metrics = {
+ metric.name: metric for metric in mock_datasource.implementation.metrics
+ }
+ all_dimensions = {
+ dim.name: dim for dim in mock_datasource.implementation.dimensions
+ }
+
+ query_object = QueryObject(
+ datasource=mock_datasource,
+ metrics=["total_sales"],
+ columns=["category"],
+ orderby=[("total_sales", False)], # DESC
+ )
+
+ result = _get_order_from_query_object(query_object, all_metrics, all_dimensions)
+
+ assert result == [(all_metrics["total_sales"], OrderDirection.DESC)]
+
+
+def test_get_order_from_query_object_dimension(mock_datasource):
+ """
+ Test order extraction with dimension.
+ """
+ all_metrics = {
+ metric.name: metric for metric in mock_datasource.implementation.metrics
+ }
+ all_dimensions = {
+ dim.name: dim for dim in mock_datasource.implementation.dimensions
+ }
+
+ query_object = QueryObject(
+ datasource=mock_datasource,
+ metrics=["total_sales"],
+ columns=["category"],
+ orderby=[("category", True)], # ASC
+ )
+
+ result = _get_order_from_query_object(query_object, all_metrics, all_dimensions)
+
+ assert result == [(all_dimensions["category"], OrderDirection.ASC)]
+
+
+def test_get_order_from_query_object_adhoc(mock_datasource):
+ """
+ Test order extraction with adhoc expression.
+ """
+ all_metrics = {
+ metric.name: metric for metric in mock_datasource.implementation.metrics
+ }
+ all_dimensions = {
+ dim.name: dim for dim in mock_datasource.implementation.dimensions
+ }
+
+ query_object = QueryObject(
+ datasource=mock_datasource,
+ metrics=["total_sales"],
+ columns=["category"],
+ orderby=[({"label": "custom_order", "sqlExpression": "RAND()"}, True)],
+ )
+
+ result = _get_order_from_query_object(query_object, all_metrics, all_dimensions)
+
+ assert result == [
+ (
+ AdhocExpression(
+ id="custom_order",
+ definition="RAND()",
+ ),
+ OrderDirection.ASC,
+ )
+ ]
+
+
+def test_get_group_limit_from_query_object_none(mock_datasource):
+ """
+ Test that None is returned with no columns.
+ """
+ all_metrics = {
+ metric.name: metric for metric in mock_datasource.implementation.metrics
+ }
+ all_dimensions = {
+ dim.name: dim for dim in mock_datasource.implementation.dimensions
+ }
+
+ query_object = QueryObject(
+ datasource=mock_datasource,
+ metrics=["total_sales"],
+ columns=[], # No columns
+ )
+
+ result = _get_group_limit_from_query_object(
+ query_object,
+ all_metrics,
+ all_dimensions,
+ )
+
+ assert result is None
+
+
+def test_get_group_limit_from_query_object_basic(mock_datasource):
+ """
+ Test basic group limit creation.
+ """
+ all_metrics = {
+ metric.name: metric for metric in mock_datasource.implementation.metrics
+ }
+ all_dimensions = {
+ dim.name: dim for dim in mock_datasource.implementation.dimensions
+ }
+
+ query_object = QueryObject(
+ datasource=mock_datasource,
+ metrics=["total_sales"],
+ columns=["category", "region"],
+ series_columns=["category"],
+ series_limit=10,
+ series_limit_metric="total_sales",
+ order_desc=True,
+ )
+
+ result = _get_group_limit_from_query_object(
+ query_object,
+ all_metrics,
+ all_dimensions,
+ )
+
+ assert result == GroupLimit(
+ top=10,
+ dimensions=[all_dimensions["category"]],
+ metric=all_metrics["total_sales"],
+ direction=OrderDirection.DESC,
+ group_others=False,
+ filters=None,
+ )
+
+
+def test_get_group_limit_from_query_object_with_group_others(mock_datasource):
+ """
+ Test group limit with group_others enabled.
+ """
+ all_metrics = {
+ metric.name: metric for metric in mock_datasource.implementation.metrics
+ }
+ all_dimensions = {
+ dim.name: dim for dim in mock_datasource.implementation.dimensions
+ }
+
+ query_object = QueryObject(
+ datasource=mock_datasource,
+ metrics=["total_sales"],
+ columns=["category"],
+ series_columns=["category"],
+ series_limit=5,
+ series_limit_metric="total_sales",
+ group_others_when_limit_reached=True,
+ )
+
+ result = _get_group_limit_from_query_object(
+ query_object,
+ all_metrics,
+ all_dimensions,
+ )
+
+ assert result.group_others is True
+
+
+def test_get_group_limit_filters_no_inner_bounds(mock_datasource):
+ """
+ Test that None is returned when no inner bounds.
+ """
+ all_dimensions = {
+ dim.name: dim for dim in mock_datasource.implementation.dimensions
+ }
+
+ query_object = QueryObject(
+ datasource=mock_datasource,
+ from_dttm=datetime(2025, 10, 15),
+ to_dttm=datetime(2025, 10, 22),
+ inner_from_dttm=None,
+ inner_to_dttm=None,
+ metrics=["total_sales"],
+ columns=["category"],
+ )
+
+ result = _get_group_limit_filters(query_object, all_dimensions)
+
+ assert result is None
+
+
+def test_get_group_limit_filters_same_bounds(mock_datasource):
+ """
+ Test that None is returned when inner bounds equal outer bounds.
+ """
+ all_dimensions = {
+ dim.name: dim for dim in mock_datasource.implementation.dimensions
+ }
+
+ from_dttm = datetime(2025, 10, 15)
+ to_dttm = datetime(2025, 10, 22)
+
+ query_object = QueryObject(
+ datasource=mock_datasource,
+ from_dttm=from_dttm,
+ to_dttm=to_dttm,
+ inner_from_dttm=from_dttm, # Same
+ inner_to_dttm=to_dttm, # Same
+ metrics=["total_sales"],
+ columns=["category"],
+ granularity="order_date",
+ )
+
+ result = _get_group_limit_filters(query_object, all_dimensions)
+
+ assert result is None
+
+
+def test_get_group_limit_filters_different_bounds(mock_datasource):
+ """
+ Test filter creation when inner bounds differ.
+ """
+ all_dimensions = {
+ dim.name: dim for dim in mock_datasource.implementation.dimensions
+ }
+
+ query_object = QueryObject(
+ datasource=mock_datasource,
+ from_dttm=datetime(2025, 10, 15),
+ to_dttm=datetime(2025, 10, 22),
+ inner_from_dttm=datetime(2025, 9, 22), # Different (30 days)
+ inner_to_dttm=datetime(2025, 10, 22),
+ metrics=["total_sales"],
+ columns=["category"],
+ granularity="order_date",
+ )
+
+ result = _get_group_limit_filters(query_object, all_dimensions)
+
+ assert result == {
+ Filter(
+ type=PredicateType.WHERE,
+ column=all_dimensions["order_date"],
+ operator=Operator.GREATER_THAN_OR_EQUAL,
+ value=datetime(2025, 9, 22),
+ ),
+ Filter(
+ type=PredicateType.WHERE,
+ column=all_dimensions["order_date"],
+ operator=Operator.LESS_THAN,
+ value=datetime(2025, 10, 22),
+ ),
+ }
+
+
+def test_get_group_limit_filters_with_extras(mock_datasource):
+ """
+ Test that extras filters are included in group limit filters.
+ """
+ all_dimensions = {
+ dim.name: dim for dim in mock_datasource.implementation.dimensions
+ }
+
+ query_object = QueryObject(
+ datasource=mock_datasource,
+ from_dttm=datetime(2025, 10, 15),
+ to_dttm=datetime(2025, 10, 22),
+ inner_from_dttm=datetime(2025, 9, 22),
+ inner_to_dttm=datetime(2025, 10, 22),
+ metrics=["total_sales"],
+ columns=["category"],
+ granularity="order_date",
+ extras={"where": "customer_id > 100"},
+ )
+
+ result = _get_group_limit_filters(query_object, all_dimensions)
+
+ assert result == {
+ Filter(
+ type=PredicateType.WHERE,
+ column=all_dimensions["order_date"],
+ operator=Operator.GREATER_THAN_OR_EQUAL,
+ value=datetime(2025, 9, 22),
+ ),
+ Filter(
+ type=PredicateType.WHERE,
+ column=all_dimensions["order_date"],
+ operator=Operator.LESS_THAN,
+ value=datetime(2025, 10, 22),
+ ),
+ AdhocFilter(
+ type=PredicateType.WHERE,
+ definition="customer_id > 100",
+ ),
+ }
+
+
+def test_map_query_object_basic(mock_datasource):
+ """
+ Test basic query object mapping.
+ """
+ query_object = QueryObject(
+ datasource=mock_datasource,
+ from_dttm=datetime(2025, 10, 15),
+ to_dttm=datetime(2025, 10, 22),
+ metrics=["total_sales"],
+ columns=["category"],
+ granularity="order_date",
+ row_limit=100,
+ row_offset=10,
+ )
+
+ result = map_query_object(query_object)
+
+ assert result == [
+ SemanticQuery(
+ metrics={
+ Metric(
+ id="orders.total_sales",
+ name="total_sales",
+ type=NUMBER,
+ definition="SUM(amount)",
+ description="Total sales",
+ ),
+ },
+ dimensions={
+ Dimension(
+ id="products.category",
+ name="category",
+ type=STRING,
+ definition="category",
+ description="Product category",
+ grain=None,
+ ),
+ },
+ filters={
+ Filter(
+ type=PredicateType.WHERE,
+ column=Dimension(
+ id="orders.order_date",
+ name="order_date",
+ type=STRING,
+ definition="order_date",
+ description="Order date",
+ grain=None,
+ ),
+ operator=Operator.GREATER_THAN_OR_EQUAL,
+ value=datetime(2025, 10, 15, 0, 0),
+ ),
+ Filter(
+ type=PredicateType.WHERE,
+ column=Dimension(
+ id="orders.order_date",
+ name="order_date",
+ type=STRING,
+ definition="order_date",
+ description="Order date",
+ grain=None,
+ ),
+ operator=Operator.LESS_THAN,
+ value=datetime(2025, 10, 22, 0, 0),
+ ),
+ },
+ order=[],
+ limit=100,
+ offset=10,
+ group_limit=GroupLimit(
+ dimensions=[],
+ top=0,
+ metric=None,
+ direction=OrderDirection.DESC,
+ group_others=False,
+ filters=None,
+ ),
+ )
+ ]
+
+
+def test_map_query_object_with_time_offsets(mock_datasource):
+ """
+ Test mapping with time offsets.
+ """
+ query_object = QueryObject(
+ datasource=mock_datasource,
+ from_dttm=datetime(2025, 10, 15),
+ to_dttm=datetime(2025, 10, 22),
+ metrics=["total_sales"],
+ columns=["category"],
+ granularity="order_date",
+ time_offsets=["1 week ago", "1 month ago"],
+ )
+
+ result = map_query_object(query_object)
+
+ # Should have 3 queries: main + 2 offsets
+ assert len(result) == 3
+ assert result[0].filters == {
+ Filter(
+ type=PredicateType.WHERE,
+ column=Dimension(
+ id="orders.order_date",
+ name="order_date",
+ type=STRING,
+ definition="order_date",
+ description="Order date",
+ grain=None,
+ ),
+ operator=Operator.GREATER_THAN_OR_EQUAL,
+ value=datetime(2025, 10, 15, 0, 0),
+ ),
+ Filter(
+ type=PredicateType.WHERE,
+ column=Dimension(
+ id="orders.order_date",
+ name="order_date",
+ type=STRING,
+ definition="order_date",
+ description="Order date",
+ grain=None,
+ ),
+ operator=Operator.LESS_THAN,
+ value=datetime(2025, 10, 22, 0, 0),
+ ),
+ }
+ assert result[1].filters == {
+ Filter(
+ type=PredicateType.WHERE,
+ column=Dimension(
+ id="orders.order_date",
+ name="order_date",
+ type=STRING,
+ definition="order_date",
+ description="Order date",
+ grain=None,
+ ),
+ operator=Operator.GREATER_THAN_OR_EQUAL,
+ value=datetime(2025, 10, 8, 0, 0),
+ ),
+ Filter(
+ type=PredicateType.WHERE,
+ column=Dimension(
+ id="orders.order_date",
+ name="order_date",
+ type=STRING,
+ definition="order_date",
+ description="Order date",
+ grain=None,
+ ),
+ operator=Operator.LESS_THAN,
+ value=datetime(2025, 10, 15, 0, 0),
+ ),
+ }
+ assert result[2].filters == {
+ Filter(
+ type=PredicateType.WHERE,
+ column=Dimension(
+ id="orders.order_date",
+ name="order_date",
+ type=STRING,
+ definition="order_date",
+ description="Order date",
+ grain=None,
+ ),
+ operator=Operator.GREATER_THAN_OR_EQUAL,
+ value=datetime(2025, 9, 15, 0, 0),
+ ),
+ Filter(
+ type=PredicateType.WHERE,
+ column=Dimension(
+ id="orders.order_date",
+ name="order_date",
+ type=STRING,
+ definition="order_date",
+ description="Order date",
+ grain=None,
+ ),
+ operator=Operator.LESS_THAN,
+ value=datetime(2025, 9, 22, 0, 0),
+ ),
+ }
+
+
+def test_map_query_object_with_group_limit(mock_datasource):
+ """
+ Test mapping with group limit.
+ """
+ query_object = QueryObject(
+ datasource=mock_datasource,
+ from_dttm=datetime(2025, 10, 15),
+ to_dttm=datetime(2025, 10, 22),
+ metrics=["total_sales"],
+ columns=["category", "region"],
+ series_columns=["category"],
+ series_limit=10,
+ series_limit_metric="total_sales",
+ granularity="order_date",
+ )
+
+ result = map_query_object(query_object)
+
+ assert len(result) == 1
+
+ semantic_query = result[0]
+ print(semantic_query.group_limit.dimensions)
+ assert semantic_query.group_limit == GroupLimit(
+ top=10,
+ dimensions=[
+ Dimension(
+ id="products.category",
+ name="category",
+ type=STRING,
+ definition="category",
+ description="Product category",
+ grain=None,
+ )
+ ],
+ metric=Metric(
+ id="orders.total_sales",
+ name="total_sales",
+ type=NUMBER,
+ definition="SUM(amount)",
+ description="Total sales",
+ ),
+ direction=OrderDirection.DESC,
+ group_others=False,
+ filters=None,
+ )
+
+
+def test_map_query_object_with_order(mock_datasource):
+ """
+ Test mapping with order by (testing _get_order directly).
+ """
+ all_metrics = {
+ metric.name: metric for metric in mock_datasource.implementation.metrics
+ }
+ all_dimensions = {
+ dim.name: dim for dim in mock_datasource.implementation.dimensions
+ }
+
+ query_object = QueryObject(
+ datasource=mock_datasource,
+ from_dttm=datetime(2025, 10, 15),
+ to_dttm=datetime(2025, 10, 22),
+ metrics=["total_sales"],
+ columns=["order_date", "category"],
+ granularity="order_date",
+ orderby=[("total_sales", False)],
+ )
+
+ # Test _get_order_from_query_object directly to avoid validation
+ order = _get_order_from_query_object(query_object, all_metrics, all_dimensions)
+
+ assert order == [(all_metrics["total_sales"], OrderDirection.DESC)]
+
+
+def test_map_query_object_filters_all_sources(mock_datasource):
+ """
+ Test that filters from all sources are combined.
+ """
+ mock_datasource.fetch_values_predicate = "tenant_id = 123"
+
+ query_object = QueryObject(
+ datasource=mock_datasource,
+ from_dttm=datetime(2025, 10, 15),
+ to_dttm=datetime(2025, 10, 22),
+ metrics=["total_sales"],
+ columns=["order_date", "category"],
+ granularity="order_date",
+ apply_fetch_values_predicate=True,
+ extras={
+ "where": "customer_id > 100",
+ "having": "SUM(amount) > 1000",
+ },
+ filter=[
+ {
+ "op": FilterOperator.EQUALS.value,
+ "col": "region",
+ "val": "US",
+ }
+ ],
+ )
+
+ result = map_query_object(query_object)
+
+ assert len(result) == 1
+
+ semantic_query = result[0]
+ filters = semantic_query.filters
+
+ # Should have at least:
+ # - 2 time filters (>= and <)
+ # - 1 fetch values filter
+ # - 2 extras filters (where and having)
+ # May also have region filter if it gets converted
+ assert filters == {
+ Filter(
+ type=PredicateType.WHERE,
+ column=Dimension(
+ id="orders.order_date",
+ name="order_date",
+ type=STRING,
+ definition="order_date",
+ description="Order date",
+ grain=None,
+ ),
+ operator=Operator.LESS_THAN,
+ value=datetime(2025, 10, 22, 0, 0),
+ ),
+ AdhocFilter(type=PredicateType.WHERE, definition="tenant_id = 123"),
+ Filter(
+ type=PredicateType.WHERE,
+ column=Dimension(
+ id="orders.order_date",
+ name="order_date",
+ type=STRING,
+ definition="order_date",
+ description="Order date",
+ grain=None,
+ ),
+ operator=Operator.GREATER_THAN_OR_EQUAL,
+ value=datetime(2025, 10, 15, 0, 0),
+ ),
+ AdhocFilter(type=PredicateType.HAVING, definition="SUM(amount) > 1000"),
+ AdhocFilter(type=PredicateType.WHERE, definition="customer_id > 100"),
+ }