Skip to content

[SPARK-57775][SQL][4.2] Return one row for GROUP BY GROUPING SETS (()) over empty input#57134

Open
uros-b wants to merge 2 commits into
apache:branch-4.2from
uros-b:SPARK-57775-branch-4.2
Open

[SPARK-57775][SQL][4.2] Return one row for GROUP BY GROUPING SETS (()) over empty input#57134
uros-b wants to merge 2 commits into
apache:branch-4.2from
uros-b:SPARK-57775-branch-4.2

Conversation

@uros-b

@uros-b uros-b commented Jul 8, 2026

Copy link
Copy Markdown
Member

What changes were proposed in this pull request?

Note: this is a 4.2 backport of #56891.

Lower the single-empty-grouping-set case to a global Aggregate (no grouping expressions, no Expand) in GroupingAnalyticsTransformer, so it returns one (grand total) row over empty input, matching the GROUP BY-less form and the SQL standard. grouping_id() folds to the constant 0, and grouping()/grouping_id() in HAVING/ORDER BY resolve against that constant. The fix lands in both the legacy fixed-point analyzer and the single-pass resolver, which share GroupingAnalyticsTransformer.

Why are the changes needed?

GROUP BY GROUPING SETS (()) is a grand total, semantically identical to an aggregation with no GROUP BY clause. It was lowered to a grouped Aggregate over an Expand (grouping by spark_grouping_id), so over empty input it returned zero rows instead of one. The same defect affected the equivalent empty GROUP BY CUBE() and GROUP BY ROLLUP(), which also lower to a single empty grouping set.

Does this PR introduce any user-facing change?

Yes, the new (correct) behavior is gated by an internal SQL config,
spark.sql.analyzer.lowerEmptyGroupingSetToGlobalAggregate.enabled (default true). When set to false, lowering reverts to the legacy Expand-based form (zero rows over empty input). The flag gates all three decision points (the transformer lowering and the grouping_id() resolution in each analyzer) so the off state reproduces pre-fix behavior identically in both analyzers.

How was this patch tested?

Tested via golden cases in grouping_set.sql (empty-input grand total, grouping_id() in SELECT/HAVING/ORDER BY, grouping() rejection, non-empty input), flag-off coverage in grouping_set_grand_total_disabled.sql, the regenerated group-analytics golden file, and ResolveGroupingAnalyticsSuite.

Was this patch authored or co-authored using generative AI tooling?

Generated-by: Opus 4.8

Co-authored-by: Isaac

@dongjoon-hyun dongjoon-hyun left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we introduce a breaking change forcefully like this, we need to update the SQL migration guide too, @uros-b .

cc @huaxingao since this is a correctness issue

Comment thread sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala Outdated
Co-authored-by: Isaac
@uros-b uros-b requested a review from dongjoon-hyun July 8, 2026 19:50

@uros-b uros-b left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the review @dongjoon-hyun, PTAL again.
Adding @huaxingao too, for 4.2 backport review.
Also, waiting for CI.

@uros-b uros-b requested a review from huaxingao July 8, 2026 19:52
@uros-b

uros-b commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

also cc @MaxGekk @cloud-fan who reviewed the original PR (#56891)

- Since Spark 4.2, when a SQL UDF has a parameter whose name matches a parameterless built-in function (`current_user`, `current_date`, `current_time`, `current_timestamp`, `user`, `session_user`, `grouping__id`), a bare reference to that name in the function body resolves to the built-in function instead of the parameter, matching the documented name resolution rules. Rename the parameter to avoid the collision, or set `spark.sql.legacy.allowUdfParameterToShadowParameterlessFunction` to `true` to restore the previous behavior.
- Since Spark 4.2, `SET CATALOG <name>` resolves a bare (unquoted) name as a session variable first, using the variable's value as the catalog name when such a variable exists, and otherwise treats the name as a literal catalog name. Use a string literal (`SET CATALOG 'name'`) to force it to be interpreted literally.
- Since Spark 4.2, when an error occurs while collecting observed metrics, `Observation.get` raises the underlying exception (for example, `SparkRuntimeException` in Scala or `PySparkException` in Python) instead of silently returning an empty result. Add error handling if your code relied on receiving an empty result on failure.
- Since Spark 4.2, `GROUP BY GROUPING SETS (())` (and the equivalent empty `GROUP BY CUBE()` / `GROUP BY ROLLUP()`) is treated as a grand total and returns one row over empty input, matching an aggregation with no `GROUP BY` clause; previously it returned no rows. To restore the previous behavior, set `spark.sql.analyzer.lowerEmptyGroupingSetToGlobalAggregate.enabled` to `false`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we decided to backport to 4.2, we should remove the migration guide in 4.2 to 4.3 section, as we don't need to duplicate it in two versions. (with a followup master branch pr)

val LOWER_EMPTY_GROUPING_SET_TO_GLOBAL_AGGREGATE =
buildConf("spark.sql.analyzer.lowerEmptyGroupingSetToGlobalAggregate.enabled")
.internal()
.version("4.2.0")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto, need a followup master branch to correct the conf version

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants