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/29 19:36:38 UTC

(superset) branch explorable updated (fb39bcbde3 -> f6be0b4dea)

This is an automated email from the ASF dual-hosted git repository.

beto pushed a change to branch explorable
in repository https://gitbox.apache.org/repos/asf/superset.git


    from fb39bcbde3 Return with queries
     new 836dddafc6 Add tests
     new f6be0b4dea Fix tests

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 superset/semantic_layers/mapper.py              |  60 ++-
 tests/unit_tests/semantic_layers/test_mapper.py | 492 ++++++++++++++++++++++--
 2 files changed, 485 insertions(+), 67 deletions(-)


(superset) 01/02: Add tests

Posted by be...@apache.org.
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

commit 836dddafc623aa4efc65e82c43686a9f37dca4dc
Author: Beto Dealmeida <ro...@dealmeida.net>
AuthorDate: Wed Oct 29 15:21:31 2025 -0400

    Add tests
---
 superset/semantic_layers/mapper.py              |  58 ++--
 tests/unit_tests/semantic_layers/test_mapper.py | 391 ++++++++++++++++++++++++
 2 files changed, 414 insertions(+), 35 deletions(-)

diff --git a/superset/semantic_layers/mapper.py b/superset/semantic_layers/mapper.py
index 0fbf3f6ec9..3cf0e724d5 100644
--- a/superset/semantic_layers/mapper.py
+++ b/superset/semantic_layers/mapper.py
@@ -18,7 +18,6 @@
 from datetime import datetime
 
 import numpy as np
-import pandas as pd
 
 from superset.common.query_object import QueryObject
 from superset.common.utils.time_range_utils import get_since_until_from_query_object
@@ -614,12 +613,6 @@ def get_results(query_object: QueryObject) -> SemanticResult:
     """
     Run a query based on the `QueryObject` and return the results as a SemanticResult.
 
-    This function handles the complete flow:
-    1. Converts QueryObject to SemanticQuery objects (one per time offset)
-    2. Executes all queries via the semantic view
-    3. Joins the results into a single DataFrame
-    4. Collects all requests from each query for troubleshooting
-
     :param query_object: The QueryObject containing query specifications
     :return: SemanticResult with combined DataFrame and all requests
     """
@@ -688,18 +681,13 @@ def get_results(query_object: QueryObject) -> SemanticResult:
 
         offset_df = result.results
 
-        # Handle empty results - create a DataFrame with NaN values
-        # This ensures the join doesn't fail and produces NULL values for missing data
+        # Handle empty results - add NaN columns directly instead of merging
+        # This avoids dtype mismatch issues with empty DataFrames
         if offset_df.empty:
-            offset_df = pd.DataFrame(
-                {
-                    **{col: [np.nan] for col in join_keys},
-                    **{
-                        TIME_COMPARISON.join([metric, time_offset]): [np.nan]
-                        for metric in metric_names
-                    },
-                }
-            )
+            # Add offset metric columns with NaN values directly to main_df
+            for metric in metric_names:
+                offset_col_name = TIME_COMPARISON.join([metric, time_offset])
+                main_df[offset_col_name] = np.nan
         else:
             # Rename metric columns with time offset suffix
             # Format: "{metric_name}__{time_offset}"
@@ -711,22 +699,22 @@ def get_results(query_object: QueryObject) -> SemanticResult:
                 }
             )
 
-        # Step 5: Perform left join on dimension columns
-        # This preserves all rows from main_df and adds offset metrics where they match
-        main_df = main_df.merge(
-            offset_df,
-            on=join_keys,
-            how="left",
-            suffixes=("", "__duplicate"),
-        )
+            # Step 5: Perform left join on dimension columns
+            # This preserves all rows from main_df and adds offset metrics
+            # where they match
+            main_df = main_df.merge(
+                offset_df,
+                on=join_keys,
+                how="left",
+                suffixes=("", "__duplicate"),
+            )
 
-        # Clean up any duplicate columns that might have been created
-        # (shouldn't happen with proper join keys, but defensive programming)
-        duplicate_cols = [col for col in main_df.columns if col.endswith("__duplicate")]
-        if duplicate_cols:
-            main_df = main_df.drop(columns=duplicate_cols)
+            # Clean up any duplicate columns that might have been created
+            # (shouldn't happen with proper join keys, but defensive programming)
+            duplicate_cols = [
+                col for col in main_df.columns if col.endswith("__duplicate")
+            ]
+            if duplicate_cols:
+                main_df = main_df.drop(columns=duplicate_cols)
 
-    return SemanticResult(
-        requests=all_requests,
-        results=main_df,
-    )
+    return SemanticResult(requests=all_requests, results=main_df)
diff --git a/tests/unit_tests/semantic_layers/test_mapper.py b/tests/unit_tests/semantic_layers/test_mapper.py
index f9f8b0d17a..26bd98ae6f 100644
--- a/tests/unit_tests/semantic_layers/test_mapper.py
+++ b/tests/unit_tests/semantic_layers/test_mapper.py
@@ -18,6 +18,7 @@
 from datetime import datetime
 from unittest.mock import Mock
 
+import pandas as pd
 import pytest
 
 from superset.common.query_object import QueryObject
@@ -31,6 +32,7 @@ from superset.semantic_layers.mapper import (
     _get_order_from_query_object,
     _get_time_bounds,
     _get_time_filter,
+    get_results,
     map_query_object,
     validate_query_object,
 )
@@ -48,6 +50,8 @@ from superset.semantic_layers.types import (
     OrderDirection,
     PredicateType,
     SemanticQuery,
+    SemanticRequest,
+    SemanticResult,
     SemanticViewFeature,
     STRING,
     TimeGrain,
@@ -1168,3 +1172,390 @@ def test_convert_query_object_filter_like():
         operator=Operator.LIKE,
         value="%test%",
     )
+
+
+def test_get_results_without_time_offsets(mock_datasource):
+    """
+    Test get_results without time offsets returns main query result.
+    """
+    # Create mock dataframe for main query
+    main_df = pd.DataFrame(
+        {
+            "category": ["Electronics", "Books", "Clothing"],
+            "total_sales": [1000.0, 500.0, 750.0],
+        }
+    )
+
+    # Mock the semantic view's get_dataframe method
+    mock_result = SemanticResult(
+        requests=[
+            SemanticRequest(
+                type="SQL",
+                definition="SELECT category, SUM(amount) FROM orders GROUP BY category",
+            )
+        ],
+        results=main_df,
+    )
+
+    mock_datasource.implementation.get_dataframe = Mock(return_value=mock_result)
+
+    # Create query object without 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",
+    )
+
+    # Call get_results
+    result = get_results(query_object)
+
+    # Verify result
+    assert isinstance(result, SemanticResult)
+    assert len(result.requests) == 1
+    assert result.requests[0].type == "SQL"
+
+    # Verify DataFrame matches main query result
+    pd.testing.assert_frame_equal(result.results, main_df)
+
+
+def test_get_results_with_single_time_offset(mock_datasource):
+    """
+    Test get_results with a single time offset joins correctly.
+    """
+    # Create mock dataframes
+    main_df = pd.DataFrame(
+        {
+            "category": ["Electronics", "Books", "Clothing"],
+            "total_sales": [1000.0, 500.0, 750.0],
+        }
+    )
+
+    offset_df = pd.DataFrame(
+        {
+            "category": ["Electronics", "Books", "Clothing"],
+            "total_sales": [950.0, 480.0, 700.0],
+        }
+    )
+
+    # Mock the semantic view's get_dataframe method
+    # It will be called twice: once for main, once for offset
+    mock_main_result = SemanticResult(
+        requests=[
+            SemanticRequest(
+                type="SQL",
+                definition=(
+                    "SELECT category, SUM(amount) FROM orders "
+                    "WHERE date >= '2025-10-15' GROUP BY category"
+                ),
+            )
+        ],
+        results=main_df.copy(),
+    )
+
+    mock_offset_result = SemanticResult(
+        requests=[
+            SemanticRequest(
+                type="SQL",
+                definition=(
+                    "SELECT category, SUM(amount) FROM orders "
+                    "WHERE date >= '2025-10-08' GROUP BY category"
+                ),
+            )
+        ],
+        results=offset_df.copy(),
+    )
+
+    mock_datasource.implementation.get_dataframe = Mock(
+        side_effect=[mock_main_result, mock_offset_result]
+    )
+
+    # Create query object with time offset
+    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"],
+    )
+
+    # Call get_results
+    result = get_results(query_object)
+
+    # Verify result structure
+    assert isinstance(result, SemanticResult)
+    assert len(result.requests) == 2  # Main + offset query
+
+    # Verify DataFrame has both main and offset metrics
+    expected_df = pd.DataFrame(
+        {
+            "category": ["Electronics", "Books", "Clothing"],
+            "total_sales": [1000.0, 500.0, 750.0],
+            "total_sales__1 week ago": [950.0, 480.0, 700.0],
+        }
+    )
+
+    pd.testing.assert_frame_equal(result.results, expected_df)
+
+
+def test_get_results_with_multiple_time_offsets(mock_datasource):
+    """
+    Test get_results with multiple time offsets joins all correctly.
+    """
+    # Create mock dataframes
+    main_df = pd.DataFrame(
+        {
+            "region": ["US", "UK", "JP"],
+            "order_count": [100, 50, 75],
+        }
+    )
+
+    offset_1w_df = pd.DataFrame(
+        {
+            "region": ["US", "UK", "JP"],
+            "order_count": [95, 48, 70],
+        }
+    )
+
+    offset_1m_df = pd.DataFrame(
+        {
+            "region": ["US", "UK", "JP"],
+            "order_count": [80, 40, 60],
+        }
+    )
+
+    # Mock results
+    mock_main_result = SemanticResult(
+        requests=[SemanticRequest(type="SQL", definition="MAIN QUERY")],
+        results=main_df.copy(),
+    )
+
+    mock_offset_1w_result = SemanticResult(
+        requests=[SemanticRequest(type="SQL", definition="OFFSET 1W QUERY")],
+        results=offset_1w_df.copy(),
+    )
+
+    mock_offset_1m_result = SemanticResult(
+        requests=[SemanticRequest(type="SQL", definition="OFFSET 1M QUERY")],
+        results=offset_1m_df.copy(),
+    )
+
+    mock_datasource.implementation.get_dataframe = Mock(
+        side_effect=[mock_main_result, mock_offset_1w_result, mock_offset_1m_result]
+    )
+
+    # Create query object with multiple time offsets
+    query_object = QueryObject(
+        datasource=mock_datasource,
+        from_dttm=datetime(2025, 10, 15),
+        to_dttm=datetime(2025, 10, 22),
+        metrics=["order_count"],
+        columns=["region"],
+        granularity="order_date",
+        time_offsets=["1 week ago", "1 month ago"],
+    )
+
+    # Call get_results
+    result = get_results(query_object)
+
+    # Verify result structure
+    assert isinstance(result, SemanticResult)
+    assert len(result.requests) == 3  # Main + 2 offset queries
+
+    # Verify all requests are collected
+    assert result.requests[0].definition == "MAIN QUERY"
+    assert result.requests[1].definition == "OFFSET 1W QUERY"
+    assert result.requests[2].definition == "OFFSET 1M QUERY"
+
+    # Verify DataFrame has all metrics
+    expected_df = pd.DataFrame(
+        {
+            "region": ["US", "UK", "JP"],
+            "order_count": [100, 50, 75],
+            "order_count__1 week ago": [95, 48, 70],
+            "order_count__1 month ago": [80, 40, 60],
+        }
+    )
+
+    pd.testing.assert_frame_equal(result.results, expected_df)
+
+
+def test_get_results_with_empty_offset_result(mock_datasource):
+    """
+    Test get_results handles empty offset results gracefully.
+    """
+    # Create mock dataframes
+    main_df = pd.DataFrame(
+        {
+            "category": ["Electronics", "Books"],
+            "total_sales": [1000.0, 500.0],
+        }
+    )
+
+    # Empty offset result
+    offset_df = pd.DataFrame()
+
+    # Mock results
+    mock_main_result = SemanticResult(
+        requests=[SemanticRequest(type="SQL", definition="MAIN QUERY")],
+        results=main_df.copy(),
+    )
+
+    mock_offset_result = SemanticResult(
+        requests=[SemanticRequest(type="SQL", definition="OFFSET QUERY")],
+        results=offset_df,
+    )
+
+    mock_datasource.implementation.get_dataframe = Mock(
+        side_effect=[mock_main_result, mock_offset_result]
+    )
+
+    # Create query object with time offset
+    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"],
+    )
+
+    # Call get_results
+    result = get_results(query_object)
+
+    # Verify result structure
+    assert isinstance(result, SemanticResult)
+    assert len(result.requests) == 2
+
+    # Verify DataFrame has NaN for missing offset data
+    assert "total_sales__1 week ago" in result.results.columns
+    assert result.results["total_sales__1 week ago"].isna().all()
+
+
+def test_get_results_with_partial_offset_match(mock_datasource):
+    """
+    Test get_results with partial matches in offset data (left join behavior).
+    """
+    # Main query has 3 categories
+    main_df = pd.DataFrame(
+        {
+            "category": ["Electronics", "Books", "Clothing"],
+            "total_sales": [1000.0, 500.0, 750.0],
+        }
+    )
+
+    # Offset query only has 2 categories (Books missing)
+    offset_df = pd.DataFrame(
+        {
+            "category": ["Electronics", "Clothing"],
+            "total_sales": [950.0, 700.0],
+        }
+    )
+
+    # Mock results
+    mock_main_result = SemanticResult(
+        requests=[SemanticRequest(type="SQL", definition="MAIN QUERY")],
+        results=main_df.copy(),
+    )
+
+    mock_offset_result = SemanticResult(
+        requests=[SemanticRequest(type="SQL", definition="OFFSET QUERY")],
+        results=offset_df.copy(),
+    )
+
+    mock_datasource.implementation.get_dataframe = Mock(
+        side_effect=[mock_main_result, mock_offset_result]
+    )
+
+    # Create query object
+    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"],
+    )
+
+    # Call get_results
+    result = get_results(query_object)
+
+    # Verify DataFrame structure
+    expected_df = pd.DataFrame(
+        {
+            "category": ["Electronics", "Books", "Clothing"],
+            "total_sales": [1000.0, 500.0, 750.0],
+            "total_sales__1 week ago": [950.0, None, 700.0],
+        }
+    )
+
+    pd.testing.assert_frame_equal(result.results, expected_df)
+
+
+def test_get_results_with_multiple_dimensions(mock_datasource):
+    """
+    Test get_results with multiple dimension columns in join.
+    """
+    # Create mock dataframes with multiple dimensions
+    main_df = pd.DataFrame(
+        {
+            "category": ["Electronics", "Electronics", "Books"],
+            "region": ["US", "UK", "US"],
+            "total_sales": [1000.0, 800.0, 500.0],
+        }
+    )
+
+    offset_df = pd.DataFrame(
+        {
+            "category": ["Electronics", "Electronics", "Books"],
+            "region": ["US", "UK", "US"],
+            "total_sales": [950.0, 780.0, 480.0],
+        }
+    )
+
+    # Mock results
+    mock_main_result = SemanticResult(
+        requests=[SemanticRequest(type="SQL", definition="MAIN QUERY")],
+        results=main_df.copy(),
+    )
+
+    mock_offset_result = SemanticResult(
+        requests=[SemanticRequest(type="SQL", definition="OFFSET QUERY")],
+        results=offset_df.copy(),
+    )
+
+    mock_datasource.implementation.get_dataframe = Mock(
+        side_effect=[mock_main_result, mock_offset_result]
+    )
+
+    # Create query object with multiple dimensions
+    query_object = QueryObject(
+        datasource=mock_datasource,
+        from_dttm=datetime(2025, 10, 15),
+        to_dttm=datetime(2025, 10, 22),
+        metrics=["total_sales"],
+        columns=["category", "region"],
+        granularity="order_date",
+        time_offsets=["1 week ago"],
+    )
+
+    # Call get_results
+    result = get_results(query_object)
+
+    # Verify DataFrame structure - join should be on both category and region
+    expected_df = pd.DataFrame(
+        {
+            "category": ["Electronics", "Electronics", "Books"],
+            "region": ["US", "UK", "US"],
+            "total_sales": [1000.0, 800.0, 500.0],
+            "total_sales__1 week ago": [950.0, 780.0, 480.0],
+        }
+    )
+
+    pd.testing.assert_frame_equal(result.results, expected_df)


(superset) 02/02: Fix tests

Posted by be...@apache.org.
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

commit f6be0b4deaed64804d0fbb6ae88a0cbff9f5d968
Author: Beto Dealmeida <ro...@dealmeida.net>
AuthorDate: Wed Oct 29 15:36:30 2025 -0400

    Fix tests
---
 superset/semantic_layers/mapper.py              |   2 +
 tests/unit_tests/semantic_layers/test_mapper.py | 125 +++++++++++++++---------
 2 files changed, 83 insertions(+), 44 deletions(-)

diff --git a/superset/semantic_layers/mapper.py b/superset/semantic_layers/mapper.py
index 3cf0e724d5..413508a9ce 100644
--- a/superset/semantic_layers/mapper.py
+++ b/superset/semantic_layers/mapper.py
@@ -617,6 +617,8 @@ def get_results(query_object: QueryObject) -> SemanticResult:
     :return: SemanticResult with combined DataFrame and all requests
     """
     semantic_view = query_object.datasource.implementation
+    validate_query_object(query_object, semantic_view)
+
     dispatcher = (
         semantic_view.get_row_count
         if query_object.is_rowcount
diff --git a/tests/unit_tests/semantic_layers/test_mapper.py b/tests/unit_tests/semantic_layers/test_mapper.py
index 26bd98ae6f..735fde83e5 100644
--- a/tests/unit_tests/semantic_layers/test_mapper.py
+++ b/tests/unit_tests/semantic_layers/test_mapper.py
@@ -16,10 +16,11 @@
 # under the License.
 
 from datetime import datetime
-from unittest.mock import Mock
+from unittest.mock import MagicMock
 
 import pandas as pd
 import pytest
+from pytest_mock import MockerFixture
 
 from superset.common.query_object import QueryObject
 from superset.semantic_layers.mapper import (
@@ -88,11 +89,11 @@ class MockSemanticViewImplementation:
 
 
 @pytest.fixture
-def mock_datasource() -> Mock:
+def mock_datasource(mocker: MockerFixture) -> MagicMock:
     """
     Create a mock datasource with semantic view implementation.
     """
-    datasource = Mock()
+    datasource = mocker.Mock()
 
     # Create dimensions
     time_dim = Dimension(
@@ -226,7 +227,7 @@ def test_get_filters_from_extras_both() -> None:
     }
 
 
-def test_get_time_bounds_no_offset(mock_datasource):
+def test_get_time_bounds_no_offset(mock_datasource: MagicMock) -> None:
     """
     Test time bounds without offset.
     """
@@ -247,7 +248,7 @@ def test_get_time_bounds_no_offset(mock_datasource):
     assert result_to == to_dttm
 
 
-def test_get_time_filter_no_granularity(mock_datasource):
+def test_get_time_filter_no_granularity(mock_datasource: MagicMock) -> None:
     """
     Test that no time filter is created without granularity.
     """
@@ -269,7 +270,7 @@ def test_get_time_filter_no_granularity(mock_datasource):
     assert result == set()
 
 
-def test_get_time_filter_with_granularity(mock_datasource):
+def test_get_time_filter_with_granularity(mock_datasource: MagicMock) -> None:
     """
     Test time filter creation with granularity.
     """
@@ -341,7 +342,7 @@ def test_convert_query_object_filter_temporal_range() -> None:
     assert result is None
 
 
-def test_convert_query_object_filter_in(mock_datasource):
+def test_convert_query_object_filter_in(mock_datasource: MagicMock) -> None:
     """
     Test conversion of IN filter.
     """
@@ -365,7 +366,7 @@ def test_convert_query_object_filter_in(mock_datasource):
     )
 
 
-def test_convert_query_object_filter_is_null(mock_datasource):
+def test_convert_query_object_filter_is_null(mock_datasource: MagicMock) -> None:
     """
     Test conversion of IS_NULL filter.
     """
@@ -388,7 +389,7 @@ def test_convert_query_object_filter_is_null(mock_datasource):
     )
 
 
-def test_get_filters_from_query_object_basic(mock_datasource):
+def test_get_filters_from_query_object_basic(mock_datasource: MagicMock) -> None:
     """
     Test basic filter extraction from query object.
     """
@@ -423,7 +424,7 @@ def test_get_filters_from_query_object_basic(mock_datasource):
     }
 
 
-def test_get_filters_from_query_object_with_extras(mock_datasource):
+def test_get_filters_from_query_object_with_extras(mock_datasource: MagicMock) -> None:
     """
     Test filter extraction with extras.
     """
@@ -463,7 +464,9 @@ def test_get_filters_from_query_object_with_extras(mock_datasource):
     }
 
 
-def test_get_filters_from_query_object_with_fetch_values(mock_datasource):
+def test_get_filters_from_query_object_with_fetch_values(
+    mock_datasource: MagicMock,
+) -> None:
     """
     Test filter extraction with fetch values predicate.
     """
@@ -505,7 +508,7 @@ def test_get_filters_from_query_object_with_fetch_values(mock_datasource):
     }
 
 
-def test_get_order_from_query_object_metric(mock_datasource):
+def test_get_order_from_query_object_metric(mock_datasource: MagicMock) -> None:
     """
     Test order extraction with metric.
     """
@@ -528,7 +531,7 @@ def test_get_order_from_query_object_metric(mock_datasource):
     assert result == [(all_metrics["total_sales"], OrderDirection.DESC)]
 
 
-def test_get_order_from_query_object_dimension(mock_datasource):
+def test_get_order_from_query_object_dimension(mock_datasource: MagicMock) -> None:
     """
     Test order extraction with dimension.
     """
@@ -551,7 +554,7 @@ def test_get_order_from_query_object_dimension(mock_datasource):
     assert result == [(all_dimensions["category"], OrderDirection.ASC)]
 
 
-def test_get_order_from_query_object_adhoc(mock_datasource):
+def test_get_order_from_query_object_adhoc(mock_datasource: MagicMock) -> None:
     """
     Test order extraction with adhoc expression.
     """
@@ -582,7 +585,7 @@ def test_get_order_from_query_object_adhoc(mock_datasource):
     ]
 
 
-def test_get_group_limit_from_query_object_none(mock_datasource):
+def test_get_group_limit_from_query_object_none(mock_datasource: MagicMock) -> None:
     """
     Test that None is returned with no columns.
     """
@@ -608,7 +611,7 @@ def test_get_group_limit_from_query_object_none(mock_datasource):
     assert result is None
 
 
-def test_get_group_limit_from_query_object_basic(mock_datasource):
+def test_get_group_limit_from_query_object_basic(mock_datasource: MagicMock) -> None:
     """
     Test basic group limit creation.
     """
@@ -645,7 +648,9 @@ def test_get_group_limit_from_query_object_basic(mock_datasource):
     )
 
 
-def test_get_group_limit_from_query_object_with_group_others(mock_datasource):
+def test_get_group_limit_from_query_object_with_group_others(
+    mock_datasource: MagicMock,
+) -> None:
     """
     Test group limit with group_others enabled.
     """
@@ -675,7 +680,7 @@ def test_get_group_limit_from_query_object_with_group_others(mock_datasource):
     assert result.group_others is True
 
 
-def test_get_group_limit_filters_no_inner_bounds(mock_datasource):
+def test_get_group_limit_filters_no_inner_bounds(mock_datasource: MagicMock) -> None:
     """
     Test that None is returned when no inner bounds.
     """
@@ -698,7 +703,7 @@ def test_get_group_limit_filters_no_inner_bounds(mock_datasource):
     assert result is None
 
 
-def test_get_group_limit_filters_same_bounds(mock_datasource):
+def test_get_group_limit_filters_same_bounds(mock_datasource: MagicMock) -> None:
     """
     Test that None is returned when inner bounds equal outer bounds.
     """
@@ -725,7 +730,7 @@ def test_get_group_limit_filters_same_bounds(mock_datasource):
     assert result is None
 
 
-def test_get_group_limit_filters_different_bounds(mock_datasource):
+def test_get_group_limit_filters_different_bounds(mock_datasource: MagicMock) -> None:
     """
     Test filter creation when inner bounds differ.
     """
@@ -762,7 +767,7 @@ def test_get_group_limit_filters_different_bounds(mock_datasource):
     }
 
 
-def test_get_group_limit_filters_with_extras(mock_datasource):
+def test_get_group_limit_filters_with_extras(mock_datasource: MagicMock) -> None:
     """
     Test that extras filters are included in group limit filters.
     """
@@ -804,7 +809,7 @@ def test_get_group_limit_filters_with_extras(mock_datasource):
     }
 
 
-def test_map_query_object_basic(mock_datasource):
+def test_map_query_object_basic(mock_datasource: MagicMock) -> None:
     """
     Test basic query object mapping.
     """
@@ -885,7 +890,7 @@ def test_map_query_object_basic(mock_datasource):
     ]
 
 
-def test_map_query_object_with_time_offsets(mock_datasource):
+def test_map_query_object_with_time_offsets(mock_datasource: MagicMock) -> None:
     """
     Test mapping with time offsets.
     """
@@ -989,7 +994,9 @@ def test_map_query_object_with_time_offsets(mock_datasource):
     }
 
 
-def test_convert_query_object_filter_unknown_operator(mock_datasource):
+def test_convert_query_object_filter_unknown_operator(
+    mock_datasource: MagicMock,
+) -> None:
     """
     Test filter with unknown operator returns None.
     """
@@ -1008,7 +1015,9 @@ def test_convert_query_object_filter_unknown_operator(mock_datasource):
     assert result is None
 
 
-def test_validate_query_object_undefined_metric_error(mock_datasource):
+def test_validate_query_object_undefined_metric_error(
+    mock_datasource: MagicMock,
+) -> None:
     """
     Test validation error for undefined metrics.
     """
@@ -1022,7 +1031,9 @@ def test_validate_query_object_undefined_metric_error(mock_datasource):
         validate_query_object(query_object, mock_datasource.implementation)
 
 
-def test_validate_query_object_undefined_dimension_error(mock_datasource):
+def test_validate_query_object_undefined_dimension_error(
+    mock_datasource: MagicMock,
+) -> None:
     """
     Test validation error for undefined dimensions.
     """
@@ -1036,7 +1047,9 @@ def test_validate_query_object_undefined_dimension_error(mock_datasource):
         validate_query_object(query_object, mock_datasource.implementation)
 
 
-def test_validate_query_object_time_grain_without_column_error(mock_datasource):
+def test_validate_query_object_time_grain_without_column_error(
+    mock_datasource: MagicMock,
+) -> None:
     """
     Test validation error when time grain provided without time column.
     """
@@ -1052,7 +1065,9 @@ def test_validate_query_object_time_grain_without_column_error(mock_datasource):
         validate_query_object(query_object, mock_datasource.implementation)
 
 
-def test_validate_query_object_unsupported_time_grain_error(mock_datasource):
+def test_validate_query_object_unsupported_time_grain_error(
+    mock_datasource: MagicMock,
+) -> None:
     """
     Test validation error for unsupported time grain.
     """
@@ -1073,11 +1088,13 @@ def test_validate_query_object_unsupported_time_grain_error(mock_datasource):
         validate_query_object(query_object, mock_datasource.implementation)
 
 
-def test_validate_query_object_group_limit_not_supported_error():
+def test_validate_query_object_group_limit_not_supported_error(
+    mocker: MockerFixture,
+) -> None:
     """
     Test validation error when group limit not supported.
     """
-    mock_datasource = Mock()
+    mock_datasource = mocker.Mock()
     time_dim = Dimension("order_date", "order_date", STRING, "order_date", "Date")
     category_dim = Dimension("category", "category", STRING, "category", "Category")
     sales_metric = Metric("total_sales", "total_sales", NUMBER, "SUM(amount)", "Sales")
@@ -1098,7 +1115,9 @@ def test_validate_query_object_group_limit_not_supported_error():
         validate_query_object(query_object, mock_datasource.implementation)
 
 
-def test_validate_query_object_undefined_series_column_error(mock_datasource):
+def test_validate_query_object_undefined_series_column_error(
+    mock_datasource: MagicMock,
+) -> None:
     """
     Test validation error for undefined series columns.
     """
@@ -1152,7 +1171,7 @@ def test_convert_query_object_filter(
     )
 
 
-def test_convert_query_object_filter_like():
+def test_convert_query_object_filter_like() -> None:
     """
     Test filter with LIKE operator.
     """
@@ -1174,7 +1193,10 @@ def test_convert_query_object_filter_like():
     )
 
 
-def test_get_results_without_time_offsets(mock_datasource):
+def test_get_results_without_time_offsets(
+    mock_datasource: MagicMock,
+    mocker: MockerFixture,
+) -> None:
     """
     Test get_results without time offsets returns main query result.
     """
@@ -1197,7 +1219,7 @@ def test_get_results_without_time_offsets(mock_datasource):
         results=main_df,
     )
 
-    mock_datasource.implementation.get_dataframe = Mock(return_value=mock_result)
+    mock_datasource.implementation.get_dataframe = mocker.Mock(return_value=mock_result)
 
     # Create query object without time offsets
     query_object = QueryObject(
@@ -1221,7 +1243,10 @@ def test_get_results_without_time_offsets(mock_datasource):
     pd.testing.assert_frame_equal(result.results, main_df)
 
 
-def test_get_results_with_single_time_offset(mock_datasource):
+def test_get_results_with_single_time_offset(
+    mock_datasource: MagicMock,
+    mocker: MockerFixture,
+) -> None:
     """
     Test get_results with a single time offset joins correctly.
     """
@@ -1268,7 +1293,7 @@ def test_get_results_with_single_time_offset(mock_datasource):
         results=offset_df.copy(),
     )
 
-    mock_datasource.implementation.get_dataframe = Mock(
+    mock_datasource.implementation.get_dataframe = mocker.Mock(
         side_effect=[mock_main_result, mock_offset_result]
     )
 
@@ -1302,7 +1327,10 @@ def test_get_results_with_single_time_offset(mock_datasource):
     pd.testing.assert_frame_equal(result.results, expected_df)
 
 
-def test_get_results_with_multiple_time_offsets(mock_datasource):
+def test_get_results_with_multiple_time_offsets(
+    mock_datasource: MagicMock,
+    mocker: MockerFixture,
+) -> None:
     """
     Test get_results with multiple time offsets joins all correctly.
     """
@@ -1344,7 +1372,7 @@ def test_get_results_with_multiple_time_offsets(mock_datasource):
         results=offset_1m_df.copy(),
     )
 
-    mock_datasource.implementation.get_dataframe = Mock(
+    mock_datasource.implementation.get_dataframe = mocker.Mock(
         side_effect=[mock_main_result, mock_offset_1w_result, mock_offset_1m_result]
     )
 
@@ -1384,7 +1412,10 @@ def test_get_results_with_multiple_time_offsets(mock_datasource):
     pd.testing.assert_frame_equal(result.results, expected_df)
 
 
-def test_get_results_with_empty_offset_result(mock_datasource):
+def test_get_results_with_empty_offset_result(
+    mock_datasource: MagicMock,
+    mocker: MockerFixture,
+) -> None:
     """
     Test get_results handles empty offset results gracefully.
     """
@@ -1410,7 +1441,7 @@ def test_get_results_with_empty_offset_result(mock_datasource):
         results=offset_df,
     )
 
-    mock_datasource.implementation.get_dataframe = Mock(
+    mock_datasource.implementation.get_dataframe = mocker.Mock(
         side_effect=[mock_main_result, mock_offset_result]
     )
 
@@ -1437,7 +1468,10 @@ def test_get_results_with_empty_offset_result(mock_datasource):
     assert result.results["total_sales__1 week ago"].isna().all()
 
 
-def test_get_results_with_partial_offset_match(mock_datasource):
+def test_get_results_with_partial_offset_match(
+    mock_datasource: MagicMock,
+    mocker: MockerFixture,
+) -> None:
     """
     Test get_results with partial matches in offset data (left join behavior).
     """
@@ -1468,7 +1502,7 @@ def test_get_results_with_partial_offset_match(mock_datasource):
         results=offset_df.copy(),
     )
 
-    mock_datasource.implementation.get_dataframe = Mock(
+    mock_datasource.implementation.get_dataframe = mocker.Mock(
         side_effect=[mock_main_result, mock_offset_result]
     )
 
@@ -1498,7 +1532,10 @@ def test_get_results_with_partial_offset_match(mock_datasource):
     pd.testing.assert_frame_equal(result.results, expected_df)
 
 
-def test_get_results_with_multiple_dimensions(mock_datasource):
+def test_get_results_with_multiple_dimensions(
+    mock_datasource: MagicMock,
+    mocker: MockerFixture,
+) -> None:
     """
     Test get_results with multiple dimension columns in join.
     """
@@ -1530,7 +1567,7 @@ def test_get_results_with_multiple_dimensions(mock_datasource):
         results=offset_df.copy(),
     )
 
-    mock_datasource.implementation.get_dataframe = Mock(
+    mock_datasource.implementation.get_dataframe = mocker.Mock(
         side_effect=[mock_main_result, mock_offset_result]
     )