ctagard opened a new pull request, #59403:
URL: https://github.com/apache/spark/pull/59403

   ### What changes were proposed in this pull request?
   
   `PushPredicateThroughNonJoin` pushes a filter on grouping keys below an 
`Aggregate`. This PR keeps a conjunct above the `Aggregate` when it can tell 
apart values that the `Aggregate` merges by normalizing float/double grouping 
keys.
   
   Float/double grouping keys are normalized during physical planning 
(`SparkStrategies`): `-0.0` becomes `0.0` and NaNs are canonicalized. The 
normalized key keeps the original `ExprId`, so everything above the `Aggregate` 
sees the normalized value, while a filter pushed below it sees the raw value.
   
   The change, in the `Aggregate` case of `PushPredicateThroughNonJoin`:
   - Collect the child attributes behind grouping expressions whose type needs 
normalization (`NormalizeFloatingNumbers.needNormalize`).
   - Push a conjunct only if `sensitiveToFloatNormalization(replaced, 
normalizedKeys)` is false. The check runs on the conjunct after alias 
expansion, which is the expression that actually runs below the `Aggregate`. So 
an alias like `cast(x AS STRING) AS s` is caught.
   - `sensitiveToFloatNormalization` walks the expression:
     - A normalized key used as a direct float/double operand of a comparison 
(`BinaryComparison`, `In`, `IsNull`, `IsNotNull`, `IsNaN`) is safe, because 
these already treat `-0.0`/`0.0` and all NaNs as equal.
     - Any other use of a key is treated as sensitive.
     - Boolean connectives such as `OR` are sensitive if any child is.
   
   Common filters such as `x = 1.0`, `x > 5` or `NOT isnan(x)` are still pushed 
down. Filters such as `cast(x AS STRING) = '-0.0'` stay above the `Aggregate`.
   
   ### Why are the changes needed?
   
   The rule assumes a grouping key has the same value above and below the 
`Aggregate`, and normalization breaks that assumption for float/double keys. A 
pushed filter can then return a row for which the predicate is false:
   
   ```python
   spark.createDataFrame([(-0.0,)], "x double").createOrReplaceTempView("t")
   
   spark.sql("SELECT x, cast(x AS STRING) = '-0.0' AS p FROM (SELECT DISTINCT x 
FROM t)").collect()
   # [Row(x=0.0, p=False)]
   
   spark.sql("SELECT * FROM (SELECT DISTINCT x FROM t) WHERE cast(x AS STRING) 
= '-0.0'").collect()
   # expected: []
   # before:   [Row(x=0.0)]
   ```
   
   The same happens with `GROUP BY`, `INTERSECT ALL`, `EXCEPT`/`subtract` and 
`dropDuplicates`. Pushing such a filter can also split a group and change its 
aggregates:
   
   ```sql
   -- t.x = [-0.0, 0.0]
   SELECT x, count(*) FROM t GROUP BY x HAVING cast(x AS STRING) = '0.0'
   -- expected: (0.0, 2)
   -- before:   (0.0, 1)
   ```
   
   This is the same assumption as in SPARK-60093 (`RemoveRedundantAggregates`), 
in a different rule.
   
   ### Does this PR introduce _any_ user-facing change?
   
   Yes, it fixes a correctness bug. Queries that filter an aggregate's 
float/double grouping key with a predicate that can tell `-0.0` from `0.0`, or 
one NaN from another, now return results consistent with the predicate. Such 
predicates are no longer pushed below the aggregate. Other filters are pushed 
down as before.
   
   ### How was this patch tested?
   
   New tests:
   - `FilterPushdownSuite`, three plan tests:
     - the ticket's filter stays above the `Aggregate`;
     - comparison filters on double/float keys are still pushed down, while an 
`OR` with a sensitive branch stays up as a whole, and only the sensitive 
conjunct of an `AND` stays up;
     - aliases computed from a key (`cast(x AS STRING) AS s`, `length(cast(x AS 
STRING)) AS k`) are checked after alias expansion, while a plain rename is 
still pushed down.
   - `DataFrameAggregateSuite`, one end-to-end test:
     - `distinct`, `groupBy().count()`, `dropDuplicates`, `intersectAll`, 
`except`, a float key, the ticket's SQL and an aliased key all return the 
correct rows;
     - the split-group `count` case returns `(0.0, 2)`;
     - a pushed comparison still gives the right answer.
   
   All new tests fail without the fix.
   
   Existing suites:
   - `build/sbt 'catalyst/testOnly org.apache.spark.sql.catalyst.optimizer.*'`
   - `build/sbt 'sql/testOnly *DataFrameAggregateSuite'`
   - `build/sbt 'sql/testOnly *PlanStabilitySuite 
*PlanStabilityWithStatsSuite'`. No golden plan changes.
   
   ### Was this patch authored or co-authored using generative AI tooling?
   
   Generated-by: Claude Code (Claude Opus 5.5)
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)
   


-- 
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