fix: order -NaN below +NaN in Literal comparison - #861
Conversation
CompareFloat returned lhs_is_negative <=> rhs_is_negative for the both-NaN case, so -NaN compared as greater than +NaN. That contradicts the adjacent "-NAN < NAN" comment and the FloatSpecialValuesComparison / DoubleSpecialValuesComparison tests, which assert the total ordering -NaN < -Infinity < ... < +Infinity < +NaN. Swap the operands so a negative sign bit sorts below a positive one. The existing NaN tests only covered same-sign pairs (qNaN vs sNaN), so the mixed-sign case was unexercised; add FloatSignedNaNComparison and DoubleSignedNaNComparison to cover it.
|
cc @wgtmac FYI |
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Fixes NaN total-ordering behavior in Literal::operator<=> so that -NaN sorts below +NaN, aligning implementation with the documented and tested ordering and adding missing mixed-sign NaN coverage (Fixes #860).
Changes:
- Corrected NaN sign-bit comparison in
CompareFloatto order-NaN < +NaN. - Added new float/double tests covering mixed-sign NaN comparisons.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/iceberg/test/literal_test.cc | Adds regression tests for signed NaN ordering for float and double. |
| src/iceberg/expression/literal.cc | Fixes NaN ordering logic to sort negative-sign NaNs below positive-sign NaNs. |
| auto neg_nan = Literal::Float(-std::numeric_limits<float>::quiet_NaN()); | ||
| auto pos_nan = Literal::Float(std::numeric_limits<float>::quiet_NaN()); |
| // A negative sign bit sorts below a positive one (-NaN < +NaN), so a | ||
| // negative operand must compare as less. | ||
| return rhs_is_negative <=> lhs_is_negative; |
There was a problem hiding this comment.
Keeping rhs <=> lhs: it's the idiom for reversing the order relation, and the comment above states the intent. The SignedNaNComparison tests pin the direction either way.
There was a problem hiding this comment.
Can we directly use function proposed by https://parquet.apache.org/blog/2026/05/29/taming-floating-point-statistics-in-apache-parquet-ieee-754-total-order-and-nan-counts/ for IEEE754 total order?
pub fn totalOrder(x: f64, y: f64) -> bool {
let mut x_int = x.to_bits() as i64;
let mut y_int = y.to_bits() as i64;
x_int ^= (((x_int >> 63) as u64) >> 1) as i64;
y_int ^= (((y_int >> 63) as u64) >> 1) as i64;
return x_int <= y_int;
}
It is a piece of Rust code for f64 but would be easy to be adapted to f32.
std::numeric_limits<T>::quiet_NaN() does not guarantee a sign bit, so build the mixed-sign NaN operands with std::copysign to keep the test deterministic across platforms.
std::strong_order implements the IEEE 754 totalOrder predicate on IEC 559 types, which is exactly the ordering CompareFloat hand-rolled. Replace the manual NaN sign handling with a direct call. This distinguishes NaNs by bit pattern (a signaling NaN sorts below a quiet NaN of the same sign) instead of collapsing same-sign NaNs to equivalent; update the NaN comparison tests accordingly.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/iceberg/expression/literal.cc:439
- The PR description/issue discussion describes fixing only the
-NaNvs+NaNordering by swapping sign-bit operands, but the implementation here switches tostd::strong_orderfor all float comparisons, which also changes semantics for NaNs with different payloads/quiet-vs-signaling bits (and thereforeLiteral::operator==). Please confirm this broader behavior change is intended and align the PR description accordingly; also consider clarifying it in the comment to prevent future confusion.
// Template function for floating point comparison following the Iceberg total
// ordering: -NaN < -Infinity < ... < +Infinity < +NaN. std::strong_order
// implements the IEEE 754 totalOrder predicate on IEC 559 types, which matches
// this requirement (and orders -0 below +0).
template <std::floating_point T>
std::strong_ordering CompareFloat(T lhs, T rhs) {
return std::strong_order(lhs, rhs);
src/iceberg/test/literal_test.cc:219
FloatNaNComparisonassumes (1) signaling NaNs are supported and (2) that a signaling NaN sorts below the quiet NaN returned bynumeric_limits.has_signaling_NaNmay be false on some platforms, and even when true the relative totalOrder between the library’s chosen signaling/quiet bit patterns is not guaranteed. This can make the test non-portable/flaky.
// Identical NaN bit patterns are equivalent under the total ordering.
EXPECT_EQ(nan1 <=> nan2, std::partial_ordering::equivalent);
// Total ordering distinguishes NaNs by bit pattern; a signaling NaN sorts
// below a quiet NaN of the same sign.
EXPECT_EQ(signaling_nan <=> nan1, std::partial_ordering::less);
src/iceberg/test/literal_test.cc:284
DoubleNaNComparisonassumes signaling NaNs exist and thatsignaling_NaN()sorts belowquiet_NaN()under the total ordering.has_signaling_NaNcan be false, and the relative ordering of the implementation-chosen NaN payloads isn’t guaranteed, so the test may be non-portable.
// Identical NaN bit patterns are equivalent under the total ordering.
EXPECT_EQ(nan1 <=> nan2, std::partial_ordering::equivalent);
// Total ordering distinguishes NaNs by bit pattern; a signaling NaN sorts
// below a quiet NaN of the same sign.
EXPECT_EQ(signaling_nan <=> nan1, std::partial_ordering::less);
What
Literal::operator<=>orders a negative NaN as greater than a positive NaN, the opposite of the total ordering documented and tested in this file.CompareFloatreturnslhs_is_negative <=> rhs_is_negativefor the both-NaN case, so-NaN <=> +NaNistrue <=> false=greater. The adjacent comment says "-NAN < NAN", andFloatSpecialValuesComparison/DoubleSpecialValuesComparisonassert-NaN < -Infinity < ... < +Infinity < +NaN, both of which this branch contradicts.Fixes #860.
How
Swap the operands so a negative sign bit sorts below a positive one:
return rhs_is_negative <=> lhs_is_negative;Testing
The existing
FloatNaNComparison/DoubleNaNComparisontests only cover same-sign NaN pairs (qNaN vs sNaN, which are equivalent), so the mixed-sign case was unexercised. AddedFloatSignedNaNComparisonandDoubleSignedNaNComparisonasserting-NaN < +NaNand the reverse. Verified fail-without (the new tests reportgreater/lessswapped) / pass-with. Fullexpression_testpasses (495 tests).