You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by jl...@apache.org on 2026/07/15 22:33:58 UTC

(superset) branch master updated: fix(semantic-layers): separate grain and offset time-axis resolvers (#42093)

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

sadpandajoe pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 6fe4655ba2b fix(semantic-layers): separate grain and offset time-axis resolvers (#42093)
6fe4655ba2b is described below

commit 6fe4655ba2bab165a4fc8d1be7d3735643a59a17
Author: Joe Li <jo...@preset.io>
AuthorDate: Wed Jul 15 15:33:52 2026 -0700

    fix(semantic-layers): separate grain and offset time-axis resolvers (#42093)
    
    Co-authored-by: Claude Opus 4.8 <no...@anthropic.com>
---
 superset/semantic_layers/mapper.py              |  6 +++---
 tests/unit_tests/semantic_layers/mapper_test.py | 27 +++++++++++++------------
 2 files changed, 17 insertions(+), 16 deletions(-)

diff --git a/superset/semantic_layers/mapper.py b/superset/semantic_layers/mapper.py
index de3e4258ddf..936a0d576dc 100644
--- a/superset/semantic_layers/mapper.py
+++ b/superset/semantic_layers/mapper.py
@@ -337,7 +337,7 @@ def map_query_object(query_object: ValidatedQueryObject) -> list[SemanticQuery]:
     metrics = [all_metrics[metric] for metric in (query_object.metrics or [])]
 
     grain = _convert_time_grain(query_object.extras.get("time_grain_sqla"))
-    time_axis_column = _get_time_axis_column(query_object, all_dimensions)
+    time_axis_column = _get_grain_time_axis_column(query_object, all_dimensions)
     # A semantic view can expose multiple Dimension variants per name (one per
     # supported time grain). Pick exactly one variant per selected column:
     # for the time-axis column we honor the user's grain selection, falling
@@ -1037,7 +1037,7 @@ def _get_group_limit_filters(
     return filters if filters else None
 
 
-def _get_time_axis_column(
+def _get_grain_time_axis_column(
     query_object: ValidatedQueryObject,
     all_dimensions: dict[str, Dimension],
 ) -> str | None:
@@ -1180,7 +1180,7 @@ def _validate_granularity(query_object: ValidatedQueryObject) -> None:
         )
 
     if time_grain := query_object.extras.get("time_grain_sqla"):
-        time_column = _get_time_axis_column(query_object, all_dimensions)
+        time_column = _get_grain_time_axis_column(query_object, all_dimensions)
         if not time_column:
             raise ValueError(
                 "A time column must be specified when a time grain is provided."
diff --git a/tests/unit_tests/semantic_layers/mapper_test.py b/tests/unit_tests/semantic_layers/mapper_test.py
index dd90a7c14ad..3821455aca0 100644
--- a/tests/unit_tests/semantic_layers/mapper_test.py
+++ b/tests/unit_tests/semantic_layers/mapper_test.py
@@ -48,6 +48,7 @@ from superset.semantic_layers.mapper import (
     _convert_time_grain,
     _get_filters_from_extras,
     _get_filters_from_query_object,
+    _get_grain_time_axis_column,
     _get_group_limit_filters,
     _get_group_limit_from_query_object,
     _get_order_from_query_object,
@@ -1280,19 +1281,19 @@ def test_map_query_object_picks_raw_variant_for_non_axis_time_dim(
     assert shipped_dims[0].grain is None
 
 
-def test_get_time_axis_column_returns_granularity_when_set(
+def test_get_grain_time_axis_column_returns_granularity_when_set(
     mocker: MockerFixture,
 ) -> None:
     """Legacy path: ``granularity`` short-circuits scanning ``columns``."""
     qo = mocker.Mock()
     qo.granularity = "order_date"
-    assert _get_time_axis_column(qo, {}) == "order_date"
+    assert _get_grain_time_axis_column(qo, {}) == "order_date"
 
 
-def test_get_time_axis_column_finds_temporal_in_columns(
+def test_get_grain_time_axis_column_finds_temporal_in_columns(
     mocker: MockerFixture,
 ) -> None:
-    """Modern x_axis path: pick the first temporal dim from ``columns``."""
+    """Modern x_axis path: the sole temporal dim in ``columns`` is the axis."""
     all_dims = {
         "category": Dimension(
             id="products.category",
@@ -1310,10 +1311,10 @@ def test_get_time_axis_column_finds_temporal_in_columns(
     qo = mocker.Mock()
     qo.granularity = None
     qo.columns = ["category", "order_date"]
-    assert _get_time_axis_column(qo, all_dims) == "order_date"
+    assert _get_grain_time_axis_column(qo, all_dims) == "order_date"
 
 
-def test_get_time_axis_column_skips_unknown_columns(
+def test_get_grain_time_axis_column_skips_unknown_columns(
     mocker: MockerFixture,
 ) -> None:
     """A column not present in the semantic view is silently skipped."""
@@ -1329,10 +1330,10 @@ def test_get_time_axis_column_skips_unknown_columns(
     qo.granularity = None
     qo.columns = ["does_not_exist", "category"]
     # No temporal dim in columns — returns None.
-    assert _get_time_axis_column(qo, all_dims) is None
+    assert _get_grain_time_axis_column(qo, all_dims) is None
 
 
-def test_get_time_axis_column_skips_unparseable_adhoc_columns(
+def test_get_grain_time_axis_column_skips_unparseable_adhoc_columns(
     mocker: MockerFixture,
 ) -> None:
     """An adhoc column that ``_normalize_column`` rejects is silently skipped."""
@@ -1349,10 +1350,10 @@ def test_get_time_axis_column_skips_unparseable_adhoc_columns(
     # First column is an adhoc dict without ``isColumnReference`` — raises in
     # ``_normalize_column`` and the loop should keep going.
     qo.columns = [{"label": "unsupported", "sqlExpression": "lower(x)"}, "order_date"]
-    assert _get_time_axis_column(qo, all_dims) == "order_date"
+    assert _get_grain_time_axis_column(qo, all_dims) == "order_date"
 
 
-def test_get_time_axis_column_returns_none_on_multiple_temporal_columns(
+def test_get_grain_time_axis_column_returns_none_on_multiple_temporal_columns(
     mocker: MockerFixture,
 ) -> None:
     """
@@ -1379,10 +1380,10 @@ def test_get_time_axis_column_returns_none_on_multiple_temporal_columns(
     qo = mocker.Mock()
     qo.granularity = None
     qo.columns = ["created_at", "shipped_at"]
-    assert _get_time_axis_column(qo, all_dims) is None
+    assert _get_grain_time_axis_column(qo, all_dims) is None
 
 
-def test_get_time_axis_column_returns_none_when_no_temporal_columns(
+def test_get_grain_time_axis_column_returns_none_when_no_temporal_columns(
     mocker: MockerFixture,
 ) -> None:
     """Without ``granularity`` and without a temporal column we return None."""
@@ -1397,7 +1398,7 @@ def test_get_time_axis_column_returns_none_when_no_temporal_columns(
     qo = mocker.Mock()
     qo.granularity = None
     qo.columns = ["category"]
-    assert _get_time_axis_column(qo, all_dims) is None
+    assert _get_grain_time_axis_column(qo, all_dims) is None
 
 
 def test_convert_query_object_filter_unknown_operator(