From f07d7ada3b6d2eb6dec0370018b235b4f59e273a Mon Sep 17 00:00:00 2001 From: divyeshreddy02 Date: Fri, 29 May 2026 11:27:36 +0530 Subject: [PATCH 1/4] fix(pivot-table): Rename 'Subtotal' to 'Subvalue' with aggregation function The Pivot Table visualization previously used 'Subtotal' as a label for subgroup aggregations regardless of which aggregation function was used (e.g., Sum, Average, Max). This was confusing because 'Subtotal' implies a specific 'Sum' operation. This change: - Renames 'Subtotal' to 'Subvalue (%(aggfunc)s)' - Includes the actual aggregation function name (Sum, Average, etc.) - Makes the label consistent with 'Total (Sum)' at the top level Fixes #35089 --- superset/charts/client_processing.py | 4 +- .../charts/test_client_processing.py | 64 +++++++++---------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/superset/charts/client_processing.py b/superset/charts/client_processing.py index b2f443885a4b..97d64721a1ba 100644 --- a/superset/charts/client_processing.py +++ b/superset/charts/client_processing.py @@ -175,7 +175,7 @@ def pivot_df( # pylint: disable=too-many-locals, too-many-arguments, too-many-s slice_ = df.columns.get_loc(subgroup) subtotal = pivot_v2_aggfunc_map[aggfunc](df.iloc[:, slice_], axis=1) depth = df.columns.nlevels - len(subgroup) - 1 - total = metric_name if level == 0 else __("Subtotal") + total = metric_name if level == 0 else __("Subvalue (%(aggfunc)s)", aggfunc=aggfunc) subtotal_name = tuple([*subgroup, total, *([""] * depth)]) # noqa: C409 # insert column after subgroup df.insert(int(slice_.stop), subtotal_name, subtotal) @@ -201,7 +201,7 @@ def pivot_df( # pylint: disable=too-many-locals, too-many-arguments, too-many-s df.iloc[slice_, :].apply(pd.to_numeric, errors="coerce"), axis=0 ) depth = groups.nlevels - len(subgroup) - 1 - total = metric_name if level == 0 else __("Subtotal") + total = metric_name if level == 0 else __("Subvalue (%(aggfunc)s)", aggfunc=aggfunc) subtotal.name = tuple([*subgroup, total, *([""] * depth)]) # noqa: C409 # insert row after subgroup df = pd.concat( diff --git a/tests/unit_tests/charts/test_client_processing.py b/tests/unit_tests/charts/test_client_processing.py index eed1772b895a..29d20b4dc751 100644 --- a/tests/unit_tests/charts/test_client_processing.py +++ b/tests/unit_tests/charts/test_client_processing.py @@ -371,10 +371,10 @@ def test_pivot_df_single_row_two_metrics(): |:-------------------------|-------------------:| | ('SUM(num)', 'boy') | 47123 | | ('SUM(num)', 'girl') | 118065 | -| ('SUM(num)', 'Subtotal') | 165188 | +| ('SUM(num)', 'Subvalue') | 165188 | | ('MAX(num)', 'boy') | 1280 | | ('MAX(num)', 'girl') | 2588 | -| ('MAX(num)', 'Subtotal') | 3868 | +| ('MAX(num)', 'Subvalue') | 3868 | | ('{_("Total")} (Sum)', '') | 169056 | """.strip() ) @@ -399,10 +399,10 @@ def test_pivot_df_single_row_two_metrics(): |:---------------------|-------------------:| | ('boy', 'SUM(num)') | 47123 | | ('boy', 'MAX(num)') | 1280 | -| ('boy', 'Subtotal') | 48403 | +| ('boy', 'Subvalue') | 48403 | | ('girl', 'SUM(num)') | 118065 | | ('girl', 'MAX(num)') | 2588 | -| ('girl', 'Subtotal') | 120653 | +| ('girl', 'Subvalue') | 120653 | | ('{_("Total")} (Sum)', '') | 169056 | """.strip() ) @@ -540,10 +540,10 @@ def test_pivot_df_single_row_null_values(): |:-------------------------|-------------------:| | ('SUM(num)', 'boy') | nan | | ('SUM(num)', 'girl') | 118065 | -| ('SUM(num)', 'Subtotal') | 118065 | +| ('SUM(num)', 'Subvalue') | 118065 | | ('MAX(num)', 'boy') | nan | | ('MAX(num)', 'girl') | 2588 | -| ('MAX(num)', 'Subtotal') | 2588 | +| ('MAX(num)', 'Subvalue') | 2588 | | ('{_("Total")} (Sum)', '') | 120653 | """.strip() ) @@ -568,10 +568,10 @@ def test_pivot_df_single_row_null_values(): |:---------------------|-------------------:| | ('boy', 'SUM(num)') | nan | | ('boy', 'MAX(num)') | nan | -| ('boy', 'Subtotal') | 0 | +| ('boy', 'Subvalue') | 0 | | ('girl', 'SUM(num)') | 118065 | | ('girl', 'MAX(num)') | 2588 | -| ('girl', 'Subtotal') | 120653 | +| ('girl', 'Subvalue') | 120653 | | ('{_("Total")} (Sum)', '') | 120653 | """.strip() ) @@ -1072,16 +1072,16 @@ def test_pivot_df_complex(): assert ( pivoted.to_markdown() == """ -| | ('SUM(num)', 'CA') | ('SUM(num)', 'FL') | ('SUM(num)', 'Subtotal') | ('MAX(num)', 'CA') | ('MAX(num)', 'FL') | ('MAX(num)', 'Subtotal') | ('Total (Sum)', '') | +| | ('SUM(num)', 'CA') | ('SUM(num)', 'FL') | ('SUM(num)', 'Subvalue') | ('MAX(num)', 'CA') | ('MAX(num)', 'FL') | ('MAX(num)', 'Subvalue') | ('Total (Sum)', '') | |:---------------------|---------------------:|---------------------:|---------------------------:|---------------------:|---------------------:|---------------------------:|----------------------:| | ('boy', 'Edward') | 31290 | 9395 | 40685 | 1280 | 389 | 1669 | 42354 | | ('boy', 'Tony') | 3765 | 2673 | 6438 | 598 | 247 | 845 | 7283 | -| ('boy', 'Subtotal') | 35055 | 12068 | 47123 | 1878 | 636 | 2514 | 49637 | +| ('boy', 'Subvalue') | 35055 | 12068 | 47123 | 1878 | 636 | 2514 | 49637 | | ('girl', 'Amy') | 45426 | 14740 | 60166 | 2227 | 854 | 3081 | 63247 | | ('girl', 'Cindy') | 14149 | 1218 | 15367 | 842 | 217 | 1059 | 16426 | | ('girl', 'Dawn') | 11403 | 5089 | 16492 | 1157 | 461 | 1618 | 18110 | | ('girl', 'Sophia') | 18859 | 7181 | 26040 | 2588 | 1187 | 3775 | 29815 | -| ('girl', 'Subtotal') | 89837 | 28228 | 118065 | 6814 | 2719 | 9533 | 127598 | +| ('girl', 'Subvalue') | 89837 | 28228 | 118065 | 6814 | 2719 | 9533 | 127598 | | ('Total (Sum)', '') | 124892 | 40296 | 165188 | 8692 | 3355 | 12047 | 177235 | """.strip() # noqa: E501 ) @@ -1168,14 +1168,14 @@ def test_pivot_df_complex(): assert ( pivoted.to_markdown() == """ -| | ('boy', 'Edward') | ('boy', 'Tony') | ('boy', 'Subtotal') | ('girl', 'Amy') | ('girl', 'Cindy') | ('girl', 'Dawn') | ('girl', 'Sophia') | ('girl', 'Subtotal') | ('Total (Sum)', '') | +| | ('boy', 'Edward') | ('boy', 'Tony') | ('boy', 'Subvalue') | ('girl', 'Amy') | ('girl', 'Cindy') | ('girl', 'Dawn') | ('girl', 'Sophia') | ('girl', 'Subvalue') | ('Total (Sum)', '') | |:--------------------|--------------------:|------------------:|----------------------:|------------------:|--------------------:|-------------------:|---------------------:|-----------------------:|----------------------:| | ('CA', 'SUM(num)') | 31290 | 3765 | 35055 | 45426 | 14149 | 11403 | 18859 | 89837 | 124892 | | ('CA', 'MAX(num)') | 1280 | 598 | 1878 | 2227 | 842 | 1157 | 2588 | 6814 | 8692 | -| ('CA', 'Subtotal') | 32570 | 4363 | 36933 | 47653 | 14991 | 12560 | 21447 | 96651 | 133584 | +| ('CA', 'Subvalue') | 32570 | 4363 | 36933 | 47653 | 14991 | 12560 | 21447 | 96651 | 133584 | | ('FL', 'SUM(num)') | 9395 | 2673 | 12068 | 14740 | 1218 | 5089 | 7181 | 28228 | 40296 | | ('FL', 'MAX(num)') | 389 | 247 | 636 | 854 | 217 | 461 | 1187 | 2719 | 3355 | -| ('FL', 'Subtotal') | 9784 | 2920 | 12704 | 15594 | 1435 | 5550 | 8368 | 30947 | 43651 | +| ('FL', 'Subvalue') | 9784 | 2920 | 12704 | 15594 | 1435 | 5550 | 8368 | 30947 | 43651 | | ('Total (Sum)', '') | 42354 | 7283 | 49637 | 63247 | 16426 | 18110 | 29815 | 127598 | 177235 | """.strip() # noqa: E501 ) @@ -1200,12 +1200,12 @@ def test_pivot_df_complex(): |:-------------------------------------------|---------------------:|---------------------:|---------------------:|---------------------:| | ('boy', 'Edward') | 0.250536 | 0.23315 | 0.147262 | 0.115946 | | ('boy', 'Tony') | 0.030146 | 0.0663341 | 0.0687989 | 0.0736215 | -| ('boy', 'Subtotal') | 0.280683 | 0.299484 | 0.216061 | 0.189568 | +| ('boy', 'Subvalue') | 0.280683 | 0.299484 | 0.216061 | 0.189568 | | ('girl', 'Amy') | 0.363722 | 0.365793 | 0.256213 | 0.254545 | | ('girl', 'Cindy') | 0.11329 | 0.0302263 | 0.0968707 | 0.0646796 | | ('girl', 'Dawn') | 0.0913029 | 0.12629 | 0.133111 | 0.137407 | | ('girl', 'Sophia') | 0.151002 | 0.178206 | 0.297745 | 0.3538 | -| ('girl', 'Subtotal') | 0.719317 | 0.700516 | 0.783939 | 0.810432 | +| ('girl', 'Subvalue') | 0.719317 | 0.700516 | 0.783939 | 0.810432 | | ('Total (Sum as Fraction of Columns)', '') | 1 | 1 | 1 | 1 | """.strip() # noqa: E501 ) @@ -1362,7 +1362,7 @@ def test_pivot_df_multi_column(): assert ( pivoted.to_markdown() == """ -| | ('SUM(num)', 'boy') | ('SUM(num)', 'girl') | ('SUM(num)', 'Subtotal') | ('MAX(num)', 'boy') | ('MAX(num)', 'girl') | ('MAX(num)', 'Subtotal') | ('Total (Sum)', '') | +| | ('SUM(num)', 'boy') | ('SUM(num)', 'girl') | ('SUM(num)', 'Subvalue') | ('MAX(num)', 'boy') | ('MAX(num)', 'girl') | ('MAX(num)', 'Subvalue') | ('Total (Sum)', '') | |:-----------------|----------------------:|-----------------------:|---------------------------:|----------------------:|-----------------------:|---------------------------:|----------------------:| | ('CA',) | 35055 | 89837 | 124892 | 1878 | 6814 | 8692 | 133584 | | ('Total (Sum)',) | 12068 | 28228 | 40296 | 636 | 2719 | 3355 | 43651 | @@ -1436,10 +1436,10 @@ def test_pivot_df_multi_column(): |:---------------------|----------:|-------------------:| | ('boy', 'SUM(num)') | 35055 | 12068 | | ('boy', 'MAX(num)') | 1878 | 636 | -| ('boy', 'Subtotal') | 36933 | 12704 | +| ('boy', 'Subvalue') | 36933 | 12704 | | ('girl', 'SUM(num)') | 89837 | 28228 | | ('girl', 'MAX(num)') | 6814 | 2719 | -| ('girl', 'Subtotal') | 96651 | 30947 | +| ('girl', 'Subvalue') | 96651 | 30947 | | ('Total (Sum)', '') | 133584 | 43651 | """.strip() ) @@ -1658,16 +1658,16 @@ def test_pivot_df_complex_null_values(): assert ( pivoted.to_markdown() == """ -| | ('SUM(num)', nan) | ('SUM(num)', 'Subtotal') | ('MAX(num)', nan) | ('MAX(num)', 'Subtotal') | ('Total (Sum)', '') | +| | ('SUM(num)', nan) | ('SUM(num)', 'Subvalue') | ('MAX(num)', nan) | ('MAX(num)', 'Subvalue') | ('Total (Sum)', '') | |:---------------------|--------------------:|---------------------------:|--------------------:|---------------------------:|----------------------:| | ('boy', 'Edward') | 40685 | 40685 | 1669 | 1669 | 42354 | | ('boy', 'Tony') | 6438 | 6438 | 845 | 845 | 7283 | -| ('boy', 'Subtotal') | 47123 | 47123 | 2514 | 2514 | 49637 | +| ('boy', 'Subvalue') | 47123 | 47123 | 2514 | 2514 | 49637 | | ('girl', 'Amy') | 60166 | 60166 | 3081 | 3081 | 63247 | | ('girl', 'Cindy') | 15367 | 15367 | 1059 | 1059 | 16426 | | ('girl', 'Dawn') | 16492 | 16492 | 1618 | 1618 | 18110 | | ('girl', 'Sophia') | 26040 | 26040 | 3775 | 3775 | 29815 | -| ('girl', 'Subtotal') | 118065 | 118065 | 9533 | 9533 | 127598 | +| ('girl', 'Subvalue') | 118065 | 118065 | 9533 | 9533 | 127598 | | ('Total (Sum)', '') | 165188 | 165188 | 12047 | 12047 | 177235 | """.strip() # noqa: E501 ) @@ -1754,11 +1754,11 @@ def test_pivot_df_complex_null_values(): assert ( pivoted.to_markdown() == """ -| | ('boy', 'Edward') | ('boy', 'Tony') | ('boy', 'Subtotal') | ('girl', 'Amy') | ('girl', 'Cindy') | ('girl', 'Dawn') | ('girl', 'Sophia') | ('girl', 'Subtotal') | ('Total (Sum)', '') | +| | ('boy', 'Edward') | ('boy', 'Tony') | ('boy', 'Subvalue') | ('girl', 'Amy') | ('girl', 'Cindy') | ('girl', 'Dawn') | ('girl', 'Sophia') | ('girl', 'Subvalue') | ('Total (Sum)', '') | |:--------------------|--------------------:|------------------:|----------------------:|------------------:|--------------------:|-------------------:|---------------------:|-----------------------:|----------------------:| | (nan, 'SUM(num)') | 40685 | 6438 | 47123 | 60166 | 15367 | 16492 | 26040 | 118065 | 165188 | | (nan, 'MAX(num)') | 1669 | 845 | 2514 | 3081 | 1059 | 1618 | 3775 | 9533 | 12047 | -| (nan, 'Subtotal') | 42354 | 7283 | 49637 | 63247 | 16426 | 18110 | 29815 | 127598 | 177235 | +| (nan, 'Subvalue') | 42354 | 7283 | 49637 | 63247 | 16426 | 18110 | 29815 | 127598 | 177235 | | ('Total (Sum)', '') | 42354 | 7283 | 49637 | 63247 | 16426 | 18110 | 29815 | 127598 | 177235 | """.strip() # noqa: E501 ) @@ -1783,12 +1783,12 @@ def test_pivot_df_complex_null_values(): |:-------------------------------------------|--------------------:|--------------------:| | ('boy', 'Edward') | 0.246295 | 0.138541 | | ('boy', 'Tony') | 0.0389738 | 0.0701419 | -| ('boy', 'Subtotal') | 0.285269 | 0.208683 | +| ('boy', 'Subvalue') | 0.285269 | 0.208683 | | ('girl', 'Amy') | 0.364227 | 0.255748 | | ('girl', 'Cindy') | 0.0930273 | 0.0879057 | | ('girl', 'Dawn') | 0.0998378 | 0.134307 | | ('girl', 'Sophia') | 0.157639 | 0.313356 | -| ('girl', 'Subtotal') | 0.714731 | 0.791317 | +| ('girl', 'Subvalue') | 0.714731 | 0.791317 | | ('Total (Sum as Fraction of Columns)', '') | 1 | 1 | """.strip() # noqa: E501 ) @@ -2641,16 +2641,16 @@ def test_pivot_multi_level_index(): |:----------------------------------|---------------:|---------------:|---------------:| | ('Region1', 'State1', 'City1') | 10 | 5 | nan | | ('Region1', 'State1', 'City2') | 20 | 10 | nan | -| ('Region1', 'State1', 'Subtotal') | 30 | 15 | 0 | +| ('Region1', 'State1', 'Subvalue') | 30 | 15 | 0 | | ('Region1', 'State2', 'City3') | 30 | 15 | nan | -| ('Region1', 'State2', 'Subtotal') | 30 | 15 | 0 | -| ('Region1', 'Subtotal', '') | 60 | 30 | 0 | +| ('Region1', 'State2', 'Subvalue') | 30 | 15 | 0 | +| ('Region1', 'Subvalue', '') | 60 | 30 | 0 | | ('Region2', 'State3', 'City4') | 40 | 20 | nan | | ('Region2', 'State3', 'City5') | 50 | 25 | nan | -| ('Region2', 'State3', 'Subtotal') | 90 | 45 | 0 | +| ('Region2', 'State3', 'Subvalue') | 90 | 45 | 0 | | ('Region2', 'State4', 'City6') | 60 | 30 | nan | -| ('Region2', 'State4', 'Subtotal') | 60 | 30 | 0 | -| ('Region2', 'Subtotal', '') | 150 | 75 | 0 | +| ('Region2', 'State4', 'Subvalue') | 60 | 30 | 0 | +| ('Region2', 'Subvalue', '') | 150 | 75 | 0 | | ('Total (Sum)', '', '') | 210 | 105 | 0 | """.strip() ) From 5915dc0b5bc7b823d68a0d95942d1ede9f06093b Mon Sep 17 00:00:00 2001 From: divyeshreddy02 Date: Fri, 29 May 2026 12:30:50 +0530 Subject: [PATCH 2/4] fix(pivot-table): Update frontend subtotal label to match backend Align the frontend pivot table subtotal label with the backend change. Previously the backend said 'Subvalue (Sum)' while the UI said 'Subtotal'. This change: - Updates TableRenderers.tsx to use 'Subvalue (%(aggregatorName)s)' format - Adds the python-format marker to the translation file - Makes frontend consistent with backend and 'Total' labels Fixes PR #40513 review feedback --- .../src/react-pivottable/TableRenderers.tsx | 4 +++- superset/translations/en/LC_MESSAGES/messages.po | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx index d3e587638276..c22ec7cafa00 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx @@ -1333,7 +1333,9 @@ export function TableRenderer(props: TableRendererProps) { true, )} > - {t('Subtotal')} + {t('Subvalue (%(aggregatorName)s)', { + aggregatorName: t(aggregatorName), + })} ) : null; diff --git a/superset/translations/en/LC_MESSAGES/messages.po b/superset/translations/en/LC_MESSAGES/messages.po index b438a2c2ac65..846a6fd58ae8 100644 --- a/superset/translations/en/LC_MESSAGES/messages.po +++ b/superset/translations/en/LC_MESSAGES/messages.po @@ -11095,7 +11095,8 @@ msgstr "" msgid "Subtitle" msgstr "" -msgid "Subtotal" +#, python-format +msgid "Subvalue (%(aggregatorName)s)" msgstr "" msgid "Success" From fe574e6f14bcca6e6cbf8b649163cf317ebf7553 Mon Sep 17 00:00:00 2001 From: divyeshreddy02 Date: Fri, 29 May 2026 12:50:04 +0530 Subject: [PATCH 3/4] fix(streaming-export): Apply SQL mutation before executing streaming query The streaming CSV export path was executing raw SQL without running it through mutate_sql_based_on_config(). This caused: 1. Trino exports to fail on SQL with trailing semicolons (Trino rejects 'LIMIT N;' but accepts 'LIMIT N') The fix calls merged_database.mutate_sql_based_on_config(sql) before executing the query, ensuring SQL transformations are applied. This fixes Bug 1 of issue #40465. Bug 2 (user impersonation) will be addressed separately. --- superset/commands/streaming_export/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/superset/commands/streaming_export/base.py b/superset/commands/streaming_export/base.py index 39b7d233c34e..233a7a4d13a2 100644 --- a/superset/commands/streaming_export/base.py +++ b/superset/commands/streaming_export/base.py @@ -169,9 +169,11 @@ def _execute_query_and_stream( catalog=catalog, schema=schema ) as engine: with engine.connect() as connection: + # Apply config-based SQL mutations (e.g., remove trailing semicolons) + mutated_sql = merged_database.mutate_sql_based_on_config(sql) result_proxy = connection.execution_options( stream_results=True - ).execute(text(sql)) + ).execute(text(mutated_sql)) columns = list(result_proxy.keys()) From ea02f111a380c27fbff1f544bdd2a0b63767baf8 Mon Sep 17 00:00:00 2001 From: divyeshreddy02 Date: Fri, 29 May 2026 13:20:53 +0530 Subject: [PATCH 4/4] fix(streaming-export): Preserve user context for impersonation The streaming CSV export generator runs in a new Flask app context, but g.user was not being properly restored. This caused user impersonation to fail - all exports ran as the service account instead of the actual user. Changes: 1. Modified preserve_g_context() to accept an optional user parameter 2. Capture g.user separately before creating the generator (g.user may not serialize properly via __dict__.copy()) 3. Explicitly set g.user when restoring context This ensures get_username() returns the correct user, enabling: - Proper X-Trino-User header forwarding - Correct resource group routing - Accurate audit trails This fixes Bug 2 of issue #40465. --- superset/commands/streaming_export/base.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/superset/commands/streaming_export/base.py b/superset/commands/streaming_export/base.py index 233a7a4d13a2..811789201213 100644 --- a/superset/commands/streaming_export/base.py +++ b/superset/commands/streaming_export/base.py @@ -38,6 +38,7 @@ @contextmanager def preserve_g_context( captured_g: dict[str, Any], + user: Any | None = None, ) -> Generator[None, None, None]: """ Context manager that restores captured flask.g attributes. @@ -47,9 +48,13 @@ def preserve_g_context( Args: captured_g: Dictionary of g attributes captured before context switch + user: Optional user object to set as g.user (for impersonation) """ for key, value in captured_g.items(): setattr(g, key, value) + # Explicitly set g.user for user impersonation to work + if user is not None: + g.user = user yield @@ -224,11 +229,13 @@ def run(self) -> Callable[[], Generator[str, None, None]]: captured_g = ( g._get_current_object().__dict__.copy() if has_app_context() else {} ) + # Capture user separately for impersonation - g.user may not serialize properly + user = getattr(g, "user", None) if has_app_context() else None def csv_generator() -> Generator[str, None, None]: """Generator that yields CSV data chunks.""" with self._current_app.app_context(): - with preserve_g_context(captured_g): + with preserve_g_context(captured_g, user=user): try: yield from self._execute_query_and_stream( sql, database, limit, catalog, schema