codeant-ai-for-open-source[bot] commented on code in PR #41184:
URL: https://github.com/apache/superset/pull/41184#discussion_r3565632155


##########
superset/common/query_context_processor.py:
##########
@@ -252,9 +256,69 @@ def get_query_result(self, query_object: QueryObject) -> 
QueryResult:
         This method delegates to the datasource's get_query_result method,
         which handles query execution, normalization, time offsets, and
         post-processing.
+
+        When the query requests rollup ``grouping_sets`` but the engine does 
not
+        support native ``GROUPING SETS``, fall back to one query per level and
+        concatenate the results with ``GROUPING()``-equivalent markers, so the
+        combined result matches the shape the native path produces (SIP.md,
+        phase 3b). Engines that support it run the single native query.
         """
+        if query_object.grouping_sets and not self._supports_grouping_sets():
+            return self._grouping_sets_fallback(query_object)
         return self._qc_datasource.get_query_result(query_object)
 
+    def _supports_grouping_sets(self) -> bool:
+        engine_spec: BaseEngineSpec | None = getattr(
+            self._qc_datasource, "db_engine_spec", None
+        )
+        return bool(engine_spec and engine_spec.supports_grouping_sets)
+
+    def _grouping_sets_fallback(self, query_object: QueryObject) -> 
QueryResult:
+        """
+        Emulate a GROUPING SETS query on engines without native support: run 
one
+        query per rollup level and concatenate, tagging each level's rows with
+        the same per-column markers the native path emits.
+        """
+        levels: list[list[str]] = query_object.grouping_sets
+        # Use the same label derivation as the native path (physical column 
name
+        # or adhoc column label) so both column kinds are represented and each
+        # label maps back to its own column, in the same order as the source
+        # list.
+        all_labels: list[str] = [get_column_name(col) for col in 
query_object.columns]
+        label_to_column: dict[str, Column] = dict(
+            zip(all_labels, query_object.columns, strict=True)
+        )
+
+        frames: list[pd.DataFrame] = []
+        result: QueryResult | None = None
+        for level in levels:
+            level_labels: set[str] = set(level)
+            sub_query = copy.copy(query_object)
+            sub_query.grouping_sets = []
+            sub_query.columns = [
+                label_to_column[label] for label in all_labels if label in 
level_labels
+            ]
+            # A GROUPING SETS query computes a bounded set of rollup levels, so
+            # the native path never applies row_limit to it (see the
+            # `use_grouping_sets` check in models/helpers.py). Match that here:
+            # limiting each level's fallback sub-query independently would
+            # truncate subtotal/grand-total rows and diverge from the native
+            # result shape.
+            sub_query.row_limit = None

Review Comment:
   **Suggestion:** The fallback path removes `row_limit` per level but leaves 
`row_offset` untouched, so each per-level subquery applies the offset 
independently. That diverges from native `GROUPING SETS` behavior (single 
global offset) and can silently drop subtotal/grand-total rows on paginated 
requests. Reset per-level `row_offset` and apply offset once after 
concatenation (or disable offset for this fallback path) to keep results 
consistent with native execution. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ❌ Grouping-sets rollup loses subtotal/grand total rows on pagination.
   - ⚠️ Pivot and Table charts misrepresent non-additive totals when paginated.
   - ⚠️ Engines without GROUPING SETS behave inconsistently versus supported 
engines.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. In `superset/tests/integration_tests/non_additive_totals_tests.py` inside
   `TestGroupingSetsRollup.test_grouping_sets_returns_all_levels` (around lines 
70-89),
   extend the existing query payload by adding a non-zero offset, for example:
   
      - set `payload["queries"][0]["row_offset"] = 1` alongside the existing 
`row_limit` and
      `grouping_sets` fields.
   
   
   
   2. Observe that this test constructs a `QueryContext` via
   `ChartDataQueryContextSchema().load(payload)` at lines 100-101 of the same 
file:
   
      - `query_context: QueryContext = 
ChartDataQueryContextSchema().load(payload)`
   
      - `df = query_context.get_query_result(query_context.queries[0]).df`
   
      This calls `QueryContext.get_query_result` in
      `superset/common/query_context.py:134-135`, which delegates directly to
      `QueryContextProcessor.get_query_result`.
   
   
   
   3. In `superset/common/query_context_processor.py`,
   `QueryContextProcessor.get_query_result` (lines 252-268) checks for rollup 
queries:
   
      - If `query_object.grouping_sets` is non-empty and 
`_supports_grouping_sets()` is
      False, it calls `self._grouping_sets_fallback(query_object)` at line 267.
   
      - `_supports_grouping_sets` (lines 270-274) looks up
      `self._qc_datasource.db_engine_spec.supports_grouping_sets`.
   
      For the birth_names test dataset, the engine spec inherits from 
`BaseEngineSpec` where
      `supports_grouping_sets = False` by default 
(`superset/db_engine_specs/base.py:567`),
      so the fallback path is taken for engines like SQLite that lack native 
GROUPING SETS.
   
   
   
   4. Inside `_grouping_sets_fallback` in
   `superset/common/query_context_processor.py:282-61`, for each rollup `level`:
   
      - A shallow copy of the original `QueryObject` is created: `sub_query =
      copy.copy(query_object)` at line 37.
   
      - `sub_query.grouping_sets` is cleared and `sub_query.columns` is 
restricted to that
      level (lines 38-41).
   
      - Crucially, `sub_query.row_offset` is not changed; it keeps the original 
non-zero
      value set in the payload via `QueryObject.__init__`
      (`superset/common/query_object.py:37-39`, where `self.row_offset = 
row_offset or 0`).
   
      - Only `sub_query.row_limit` is reset to `None` at line 48 (the PR line 
under review),
      and then `self._qc_datasource.get_query_result(sub_query)` is called at 
line 49.
   
   
   
   5. The datasource’s SQL construction in 
`superset/models/helpers.py:3990-4016` shows how
   pagination is applied:
   
      - `use_grouping_sets` is computed earlier (lines 7-16 of the first 
BulkRead snippet)
      and is False for these per-level fallback queries because 
`sub_query.grouping_sets` was
      cleared.
   
      - Row limit is only applied when `row_limit and not use_grouping_sets` 
(lines 10-14),
      but row offset is always applied when non-zero: `if row_offset: qry =
      qry.offset(row_offset)` at line 15.
   
      Therefore, every per-level fallback query runs with the same non-zero 
`row_offset`
      inherited from the original query object.
   
   
   
   6. For rollup levels with very few rows (especially the grand-total level 
`[]` which
   produces exactly one row in `TestGroupingSetsRollup`), applying `OFFSET 1` 
at the
   sub-query level drops that entire level:
   
      - The grand-total sub-query returns an empty `DataFrame` because all rows 
have been
      skipped before aggregation.
   
      - `_grouping_sets_fallback` still tags each per-level frame with
      `grouping_marker_label` columns (lines 50-54) and concatenates them into 
`result.df`
      via `pd.concat(frames, ignore_index=True)` at line 60, but the 
grand-total and possibly
      some subtotal frames contribute no rows.
   
   
   
   7. Downstream, the test splits the combined result back into per-level 
frames using
   `split_grouping_sets_result` from `superset/common/grouping_sets.py:82-117`:
   
      - Lines 107-116 of `non_additive_totals_tests.py` call `leaf, gender_sub, 
grand =
      split_grouping_sets_result(df, levels, ["gender", "state"])`.
   
      - The test then asserts `assert len(grand) == 1` and checks that the 
grand total ratio
      is valid (lines 110-114).
   
      Under the fallback with per-level offsets, `grand` becomes empty 
(`len(grand) == 0`)
      and these assertions fail, demonstrating that subtotal/grand-total rows 
have been
      silently dropped by the per-level row_offset.
   
   
   
   8. Contrast this with the native GROUPING SETS path: for an engine where
   `supports_grouping_sets = True`, such as Presto 
(`superset/db_engine_specs/presto.py:919`)
   or Postgres (`superset/db_engine_specs/postgres.py:281`), the same payload 
builds a single
   GROUPING SETS query in `models/helpers.py`:
   
      - `use_grouping_sets` is True, so row limit is skipped for that query 
(`if row_limit
      and not use_grouping_sets`), but a single global `row_offset` is still 
applied once at
      the end (`if row_offset: qry = qry.offset(row_offset)`).
   
      - The grand-total row remains present after offsetting, so 
`split_grouping_sets_result`
      still finds `len(grand) == 1`.
   
      This shows that the fallback path’s per-level offset semantics are 
inconsistent with
      the native GROUPING SETS behavior and can drop rollup rows only when the 
fallback is
      used.
   ```
   </details>
   
   [![Fix in 
Cursor](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-cursor-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=0f4d1905d6154bd7a71f79c2c879acc4&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
 [![Fix in VSCode 
Claude](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-vscode-claude-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=0f4d1905d6154bd7a71f79c2c879acc4&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
   
   *(Use Cmd/Ctrl + Click for best experience)*
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/common/query_context_processor.py
   **Line:** 307:307
   **Comment:**
        *Logic Error: The fallback path removes `row_limit` per level but 
leaves `row_offset` untouched, so each per-level subquery applies the offset 
independently. That diverges from native `GROUPING SETS` behavior (single 
global offset) and can silently drop subtotal/grand-total rows on paginated 
requests. Reset per-level `row_offset` and apply offset once after 
concatenation (or disable offset for this fallback path) to keep results 
consistent with native execution.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   Once fix is implemented, also check other comments on the same PR, and ask 
user if the user wants to fix the rest of the comments as well. if said yes, 
then fetch all the comments validate the correctness and implement a minimal fix
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41184&comment_hash=e2fe2b2008ae46989420c80b9289fe06bb3a2233fb6d082979b8d3e318c50e8b&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41184&comment_hash=e2fe2b2008ae46989420c80b9289fe06bb3a2233fb6d082979b8d3e318c50e8b&reaction=dislike'>👎</a>



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to