fix: normalize noncanonical NaN literals in comparisons - #5472
Conversation
Thanks for tracking this down. The root cause is clear and the fix lines up with how Spark's own A few things I would like to understand better before this goes in. Constant-folding the literal instead of wrapping it In
Scope of
Test coverage question The test drives the comparison operators through |
|
Updated in 4a151c4c7. I addressed the membership issue in this PR rather than deferring it. The regressions preserve multiple candidates and pin the optimized expression to I also expanded the original comparison matrix: all seven comparison forms, both operand orders, and both widths now check surviving row identities through I kept the scope to the confirmed scalar comparison and membership paths. The literal guards are still needed for ordinary comparisons; constant folding was an optional optimization there, while folding the membership literals is necessary to retain the static IN filter. Existing join/window/grouping normalization is governed by its own Spark/planner paths, so this is not evidence that every such operator was affected by the original literal shortcut. Nested floating-point ordering is separate work, not silently covered by this scalar fix. Validation: full Spark 4.1.3 JVM reactor, Spotless, and Scalastyle passed; all four expanded tests passed against the rebuilt JVM code. Restoring only the old predicate implementation makes both new membership tests fail with the expected Boolean result mismatch. Local runs reused the existing OSS native library, and no full native rebuild is claimed. Fresh CI remains pending. |
|
Fixed the normalized-membership fallback issue in 9dd5c044c. The serializer now retains its normalized operands and copies failure reasons back to the original membership expression. Disabling The full Spark 4.1.3 JVM reactor passed all 37 selected tests (33 planner and four native expression tests), using a freshly built and verified native library. The four new tests cover 72 combinations; each fails when the previous membership serializer is restored. Spotless and the exact CI syntactic Scalafix check also pass. I updated the PR description with the scope and validation; CI for this new commit is pending. |
|
Addressed the pruning follow-up in d48800c93. Floating Validation: 39 tests passed on Spark 4.1 and 37 on Spark 3.4, with two expected version-based cancellations on 3.4. Both new serialization tests fail against the previous serializer. A fresh controlled Parquet probe confirms that the restored expression shape prunes 15 of 16 row groups instead of zero while returning the same two rows. |
|
Fixed the remaining infinity-list pruning regression in be281b7. The fast path now accepts infinity literals by checking for non-NaN, nonzero candidates. Lists containing NaN or either zero sign still normalize. The change is limited to the two guard checks, their comment, and extensions to the existing serialization and membership tests. Fresh full-reactor JVM runs, including formatting checks, passed: 39 tests on Spark 4.1.3 and 37 on Spark 3.4.3, with two expected version-related cancellations. The updated serialization tests fail with the previous guard and pass with this fix. In a fresh Spark 4.1 native scan, Native code is unchanged; these runs reused the matching native library and verified the loaded classes/library. CI for this commit is pending. |
Which issue does this PR close?
No linked issue. This fixes scalar floating-point comparisons and membership tests involving NaNs or signed zero.
Rationale for this change
Spark treats all NaNs as equal, regardless of their sign or payload bits, and orders them above every non-NaN value. Comet must preserve those rules when an application supplies a NaN literal with a different bit representation from the usual
Float.NaNorDouble.NaN.Comet already normalizes floating-point comparison operands, but its shortcut for literals skips everything except negative zero. That leaves unusual NaN literals unchanged even when the column on the other side has been normalized. Native comparisons can then distinguish values that Spark considers equal, producing incorrect Boolean results or silently dropping rows from a filter.
For example, suppose
readingsis a DataFrame read from Parquet whosevaluecolumn contains an ordinaryDouble.NaN:Spark retains that NaN row. Without this fix, Comet can drop it because the literal and column contain different NaN bits. Signed NaN literals can also produce incorrect ordering against finite values. The affected case specifically involves a literal with a noncanonical sign or payload, such as the application-supplied value above.
What changes are included in this PR?
The fix makes normalization consistent on both sides of a comparison. A NaN literal now goes through the same existing normalization path as a column operand, so differences in the literal's sign or payload no longer change the result of equality or ordering.
This reuses Comet's existing handling of NaNs and signed zero rather than introducing a new comparison algorithm. Ordinary numbers retain their comparison fast path, and negative zero keeps its existing normalization behavior.
The review also identified a pre-existing membership bug: DataFusion's static floating-point
INfilter hashes raw bits, so distinct NaN encodings and zero signs can fail to match.IN,InSet, and the fusedNOT INpath now normalize both the membership value and its candidates. Literal normalization is folded during serialization so constant lists remain scalar and retain the native static-filter optimization. Non-floating types and the existing collation/legacy-empty-list fallback rules are unchanged.When serialization rejects a normalized membership operand, the reason is now preserved on the original
INexpression, including the fusedNOT INpath. Disabling literal or floating-point normalization support therefore retains the specific EXPLAIN diagnostic and no longer trips the test-only strict fallback assertion. Successful serialization and native membership admission are unchanged.How are these changes tested?
Review fix at
9dd5c044c: a fresh default-release native build and the full Spark 4.1.3 JVM reactor passed, along with Spotless and CI's Scalafix 0.14.6 syntactic check. All 37 selected tests passed: 33 planner tests and four native comparison/membership tests. The actual loaded native library was checked against the rebuilt and bundled library.The four new serde/planner tests cover 72 FLOAT/DOUBLE combinations across both operand positions,
IN/NOT IN, disabled literals/normalizers, and strict mode on/off. Restoring only the previous membership serializer makes each of the four tests fail. A separate 18-case probe confirms unchanged native/fallback admission, includingInSet. These local runs use Spark 4.1.3; CI for the new commit is pending.New FLOAT and DOUBLE regressions construct NaNs with payload bits and either sign programmatically and compare them with stored column values. They exercise equality, inequality, null-safe equality, and ordering in both operand orders, alongside nulls, finite values, infinities, and signed zero. Every comparison also checks retained row identities through a native filter in both operand orders. The tests use the default floating-point mode.
The checks require native Comet projections and filters, with Parquet filter pushdown disabled so the predicates exercise native comparison execution. Comparing Boolean outputs also prevents Spark's NaN-aware answer checker from hiding an incorrect comparison.
CI previously passed for head
9b6f7c05. The Spark 4.1 expression job explicitly records both new regressions passing, with 1,268 tests passing overall.For the earlier review follow-up, the full Spark 4.1.3 / Scala 2.13 / JDK 17 JVM reactor and style checks passed. All four expanded regression tests passed against the rebuilt JVM classes. The membership cases force both
InandInSetwith multi-element lists, includeNOT IN, both zero signs, null candidates/input, and negative NaNs generated after the Parquet scan. Both new membership tests fail when only the previous predicate implementation is restored, confirming that they detect the bug.NOT INwith a null candidate is checked for its empty result without requiring a filter that Spark legitimately optimizes away.That earlier local execution reused a previously built OSS native library; it changed Scala serialization/tests and did not include a new native build. The earlier local Spark 4.0 attempt stopped at dependency resolution. The CI results above describe the prior head; CI for the new commit remains to be confirmed.