feat(table/pivot-table): correct non-additive totals/subtotals via DB rollup [SIP-216] (#41184)

Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
This commit is contained in:
Evan Rusackas
2026-07-22 10:06:05 -07:00
committed by GitHub
parent 10ff470702
commit c5935b6904
35 changed files with 2474 additions and 271 deletions

View File

@@ -2151,3 +2151,108 @@ def test_raise_for_access_evaluates_access_before_validate():
processor.raise_for_access()
query.validate.assert_not_called()
def test_grouping_sets_fallback_handles_adhoc_and_physical_columns() -> None:
"""
The fallback used on engines without native GROUPING SETS support must
build ``label_to_column`` from the same labels used to run each level's
subquery, for both physical (string) and adhoc (dict) columns, and in the
original column order. Otherwise an adhoc column ahead of a physical one
in ``query_object.columns`` misaligns the mapping, and adhoc groupby
columns referenced in ``grouping_sets`` are silently dropped.
"""
import copy
from datetime import timedelta
from superset.common.query_object import QueryObject
from superset.models.helpers import QueryResult
from superset.superset_typing import AdhocColumn
adhoc_col: AdhocColumn = {
"sqlExpression": "DATE_TRUNC('month', dttm)",
"label": "month",
}
mock_datasource = MagicMock()
query_obj = QueryObject(
datasource=mock_datasource,
columns=[adhoc_col, "state"],
grouping_sets=[["month", "state"], ["month"], []],
)
processor = QueryContextProcessor(MagicMock())
processor._qc_datasource = mock_datasource
captured_columns: list[list[Any]] = []
def fake_get_query_result(sub_query: QueryObject) -> QueryResult:
captured_columns.append(copy.copy(sub_query.columns))
return QueryResult(
df=pd.DataFrame({"metric": [1]}),
query="SELECT 1",
duration=timedelta(seconds=0),
)
mock_datasource.get_query_result.side_effect = fake_get_query_result
processor._grouping_sets_fallback(query_obj)
# Level ["month", "state"] must include both the adhoc and physical column,
# each mapped to its own definition (not swapped).
assert captured_columns[0] == [adhoc_col, "state"]
# Level ["month"] must still include the adhoc column, not drop it.
assert captured_columns[1] == [adhoc_col]
# Grand total level has no groupby columns.
assert captured_columns[2] == []
def test_grouping_sets_fallback_applies_row_offset_once_globally() -> None:
"""
The native GROUPING SETS path applies `row_offset` exactly once, to the
combined multi-level result (see the unconditional `qry.offset()` call in
`models/helpers.py`). The fallback must match that: it must not apply the
same offset independently to every per-level subquery, since that would
apply it once per level (and can drop an entire low-row-count level, e.g.
a single grand-total row, outright).
"""
from datetime import timedelta
from superset.common.query_object import QueryObject
from superset.models.helpers import QueryResult
mock_datasource = MagicMock()
query_obj = QueryObject(
datasource=mock_datasource,
columns=["state"],
grouping_sets=[["state"], []],
row_offset=1,
)
processor = QueryContextProcessor(MagicMock())
processor._qc_datasource = mock_datasource
captured_offsets: list[int] = []
# Each level returns 2 rows regardless of the (should-be-ignored) offset,
# emulating a real datasource that would otherwise apply row_offset itself.
def fake_get_query_result(sub_query: QueryObject) -> QueryResult:
captured_offsets.append(sub_query.row_offset)
return QueryResult(
df=pd.DataFrame({"state": ["CA", "NY"]})
if sub_query.columns
else pd.DataFrame({"state": ["total"]}),
query="SELECT 1",
duration=timedelta(seconds=0),
)
mock_datasource.get_query_result.side_effect = fake_get_query_result
result = processor._grouping_sets_fallback(query_obj)
# Each per-level subquery must run unoffset...
assert captured_offsets == [0, 0]
# ...and the requested offset is applied exactly once, to the combined
# result: 2 + 1 = 3 total rows in, minus an offset of 1 = 2 rows out.
assert len(result.df) == 2