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 2026/06/29 21:59:41 UTC

(superset) 01/01: fix(semantic layers): time comparison with 1 row

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

betodealmeida pushed a commit to branch sl-fix-1-row-join
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 5dfcdfad071c69f20054e7061ce97d6ba03e902d
Author: Beto Dealmeida <ro...@dealmeida.net>
AuthorDate: Mon Jun 29 17:59:19 2026 -0400

    fix(semantic layers): time comparison with 1 row
---
 superset/semantic_layers/mapper.py              | 12 +++++++
 tests/unit_tests/semantic_layers/mapper_test.py | 47 +++++++++++++++++++++++++
 2 files changed, 59 insertions(+)

diff --git a/superset/semantic_layers/mapper.py b/superset/semantic_layers/mapper.py
index e40629c3ce2..a966197a97a 100644
--- a/superset/semantic_layers/mapper.py
+++ b/superset/semantic_layers/mapper.py
@@ -165,6 +165,18 @@ def get_results(query_object: QueryObject) -> QueryResult:
             for metric in metric_names:
                 offset_col_name = TIME_COMPARISON.join([metric, time_offset])
                 main_df[offset_col_name] = np.nan
+        elif not join_keys:
+            # No dimensions to join on — this is an aggregate-only query
+            # (e.g. ``metrics: ["Orders Count"]`` with empty ``columns``),
+            # which produces a single-row DataFrame. ``pandas.merge`` on an
+            # empty ``on=`` list crashes with ``IndexError`` deep in the
+            # join-indexer code, so we lift the offset metric values from
+            # the first row of ``offset_df`` straight onto ``main_df``.
+            for metric in metric_names:
+                offset_col_name = TIME_COMPARISON.join([metric, time_offset])
+                main_df[offset_col_name] = (
+                    offset_df[metric].iloc[0] if metric in offset_df else np.nan
+                )
         else:
             # Rename metric columns with time offset suffix
             # Format: "{metric_name}__{time_offset}"
diff --git a/tests/unit_tests/semantic_layers/mapper_test.py b/tests/unit_tests/semantic_layers/mapper_test.py
index 0cbb59c8430..cce4e6c984d 100644
--- a/tests/unit_tests/semantic_layers/mapper_test.py
+++ b/tests/unit_tests/semantic_layers/mapper_test.py
@@ -1507,6 +1507,53 @@ def test_get_results_with_multiple_time_offsets(
     pd.testing.assert_frame_equal(result.df, expected_df)
 
 
+def test_get_results_with_time_offset_and_no_dimensions(
+    mock_datasource: MagicMock,
+    mocker: MockerFixture,
+) -> None:
+    """
+    Aggregate-only queries (no dimensions) with a time offset should not
+    crash. Regression for ``IndexError: list index out of range`` inside
+    pandas, which fired when ``main_df.merge(..., on=[])`` was attempted
+    because the empty ``columns`` list left no join keys.
+    """
+    # Aggregate without group-by produces a single row per query.
+    main_df = pd.DataFrame({"total_sales": [1000.0]})
+    offset_df = pd.DataFrame({"total_sales": [950.0]})
+
+    mock_main_result = SemanticResult(
+        requests=[SemanticRequest(type="SQL", definition="SELECT SUM(amount)")],
+        results=pa.Table.from_pandas(main_df.copy()),
+    )
+    mock_offset_result = SemanticResult(
+        requests=[SemanticRequest(type="SQL", definition="SELECT SUM(amount)")],
+        results=pa.Table.from_pandas(offset_df.copy()),
+    )
+    mock_datasource.implementation.get_table = mocker.Mock(
+        side_effect=[mock_main_result, mock_offset_result]
+    )
+
+    query_object = ValidatedQueryObject(
+        datasource=mock_datasource,
+        from_dttm=datetime(2025, 10, 15),
+        to_dttm=datetime(2025, 10, 22),
+        metrics=["total_sales"],
+        columns=[],
+        granularity="order_date",
+        time_offsets=["1 day ago"],
+    )
+
+    # No exception, and the offset value rides alongside the main one.
+    result = get_results(query_object)
+    expected_df = pd.DataFrame(
+        {
+            "total_sales": [1000.0],
+            "total_sales__1 day ago": [950.0],
+        }
+    )
+    pd.testing.assert_frame_equal(result.df, expected_df)
+
+
 def test_get_results_with_empty_offset_result(
     mock_datasource: MagicMock,
     mocker: MockerFixture,