Skip to content

[SPARK-58291][SQL] Guard statistical-aggregate merge against empty-buffer overflow to NaN#57460

Open
ulysses-you wants to merge 1 commit into
apache:masterfrom
ulysses-you:agg-merge-overflow-fix
Open

[SPARK-58291][SQL] Guard statistical-aggregate merge against empty-buffer overflow to NaN#57460
ulysses-you wants to merge 1 commit into
apache:masterfrom
ulysses-you:agg-merge-overflow-fix

Conversation

@ulysses-you

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

The merge expressions of PearsonCorrelation (Corr), Covariance, and CentralMomentAgg overflow to Infinity and then to NaN via Infinity * 0 when one side is an empty aggregation buffer whose counterpart holds a large average.

Concretely, the merge computes terms like delta * deltaN * n1 * n2 (and the dx * dxN, dy * dyN variants). When one buffer is empty (n1 == 0 or n2 == 0), that term is mathematically zero, but delta * deltaN can overflow to Infinity before being multiplied by the zero count, and Infinity * 0 = NaN corrupts the merged moments.

This PR forces the delta/dx/dy terms to 0.0 when either side is empty, and sets the merged average directly to the non-empty side's average, so the empty-side terms vanish cleanly. This mirrors the guard the other aggregates already apply.

This is extracted from #57363 as a standalone correctness fix, independent of the spark.sql.execution.replaceHashWithSortAgg default flip, so it can be reviewed, merged, and backported on its own.

Why are the changes needed?

var_pop, covar_pop, regr_sxy, corr, and regr_r2 can return NaN for finite inputs when a partial/final aggregate merge encounters an empty buffer against a large-magnitude average. The correct results are finite (e.g. zero variance for equal values). The bug is only masked when adjacent aggregates are combined into Complete mode (which skips the merge), so the same query returns different results depending on whether the merge path runs.

Does this PR introduce any user-facing change?

Yes. Queries using corr, covar_pop/covar_samp, var_pop/var_samp, stddev_*, skewness, kurtosis, and the regr_* family that previously returned NaN for large finite inputs now return the correct finite result.

How was this patch tested?

Two new regression tests in DataFrameAggregateSuite:

  • SPARK-58291-covered var_pop/covar_pop/regr_sxy on two equal large values (expect 0.0).
  • corr/regr_r2 on two large-magnitude finite points (expect a finite result, not NaN).

Both assert results are stable across AQE on/off and combineAdjacentAggregation on/off, and both fail without the fix on the merge path. catalyst/compile and the new tests pass.

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

Generated-by: Claude Code (Claude Opus 4.8)

@cloud-fan cloud-fan left a comment

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.

0 blocking, 0 non-blocking, 1 nit.
The numerical fix is consistent across the affected aggregates; only the regression-test issue references need correction.

Nits: 1 minor item (see inline comments).

Verification

Traced each changed merge expression through its empty-left, empty-right, and non-empty paths. The guards preserve the existing formula for two non-empty buffers and reduce an empty-side merge to the non-empty buffer. Scanner coverage verified all eight changed behavioral claims.

Row(null, null, null, null, null))
}

test("SPARK-58210: empty-buffer merge must not overflow to NaN for statistical aggregates") {

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.

Please use SPARK-58291 in both new test names. SPARK-58210 points searches and test-failure reports at the wrong issue.

…ffer overflow

The Pearson correlation, covariance, and central-moment merge expressions
overflow to Infinity and then NaN via `Infinity * 0` when one side is an
empty buffer with a large average: `delta * deltaN` (and the `dx`/`dy`
variants) overflow before being multiplied by the zero count. Force the
delta terms to 0.0 and set the merged average directly to the non-empty
side when either buffer is empty, mirroring the guard each other
aggregate already applies.

Extracted from apache#57363 as a standalone correctness fix, independent of the
replaceHashWithSortAgg default flip, so it can be backported on its own.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ulysses-you
ulysses-you force-pushed the agg-merge-overflow-fix branch from 9b60bf8 to 8d5263e Compare July 23, 2026 13:01

@sunchao sunchao 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.

Reviewed commit 8d5263ec82f0a0f84428afa29987d8dd4dae637c across numerical correctness, distributed partial/final aggregation, regression coverage, generated-expression overhead, and aggregate-buffer/checkpoint compatibility.

The empty-buffer guards consistently prevent Infinity * 0 from contaminating central moments, covariance, and Pearson correlation; both empty-buffer directions preserve the populated buffer, while two populated buffers retain the existing merge formulas. The regression cases cover the affected variance, covariance, correlation, and regression functions with AQE and adjacent-aggregation enabled and disabled. Aggregate buffer schemas and public APIs are unchanged.

No blocking findings. I left one optional [P3] suggestion to explicitly exercise both empty-buffer merge orderings.

CI note: the exact-head Build was still in progress at approval time; this approval reflects the reviewed code and does not represent a completed CI run.

// This must hold with and without AQE, and with combining on and off.
Seq(true, false).foreach { aqe =>
withSQLConf(SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> aqe.toString) {
val df = Seq(1e155, 1e155).toDF("a").repartition(1)

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.

[P3][non-blocking] Consider directly exercising both empty-buffer merge directions.

These end-to-end cases reproduce the empty-left final-merge failure, but repartition(1) does not independently exercise the newly added isEmptyRight branch. A focused Catalyst regression using DeclarativeAggregateEvaluator.merge with a populated buffer and an empty buffer in both orders, ideally under both interpreted and generated execution, would protect both branches without changing the production fix.

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.

3 participants