This is an automated email from the ASF dual-hosted git repository.

lxy-9602 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new feb511e8 fix: canonicalize floating-point NaN values (#255)
feb511e8 is described below

commit feb511e858ad858be39d79901780f5e105cb7f86
Author: Zhang Jiawei <[email protected]>
AuthorDate: Fri Aug 28 16:19:55 2026 +0800

    fix: canonicalize floating-point NaN values (#255)
---
 include/paimon/predicate/literal.h                 | 13 +++---
 .../common/data/variant/generic_variant_test.cpp   | 19 +++++++++
 src/paimon/common/data/variant/variant_builder.cpp |  7 ++--
 .../common/file_index/bloomfilter/fast_hash.cpp    | 23 ++---------
 .../file_index/bloomfilter/fast_hash_test.cpp      | 20 +++-------
 .../common/global_index/btree/key_serializer.cpp   | 11 ++----
 .../global_index/btree/key_serializer_test.cpp     | 28 +++++++++++++
 .../common/global_index/global_index_result.cpp    |  5 ++-
 .../global_index/global_index_result_test.cpp      | 26 ++++++++++++
 src/paimon/common/predicate/literal.cpp            |  6 +--
 src/paimon/common/utils/math.h                     | 46 ++++++++++++++++++++++
 src/paimon/common/utils/math_test.cpp              | 23 +++++++++++
 src/paimon/core/bucket/hive_bucket_function.cpp    | 11 ++----
 .../core/bucket/hive_bucket_function_test.cpp      | 33 ++++++----------
 .../core/global_index/indexed_split_test.cpp       | 31 +++++++++++++++
 src/paimon/core/table/source/split.cpp             |  5 ++-
 16 files changed, 218 insertions(+), 89 deletions(-)

diff --git a/include/paimon/predicate/literal.h 
b/include/paimon/predicate/literal.h
index ef586457..168d6a45 100644
--- a/include/paimon/predicate/literal.h
+++ b/include/paimon/predicate/literal.h
@@ -91,13 +91,12 @@ class PAIMON_EXPORT Literal {
     std::string ToString() const;
 
     /// Gets the hash code for this literal.
-    /// @note HashCode() hashes the exact bit representation (including 
Decimal scale), while
-    /// operator== delegates to CompareTo() which uses numeric equality (e.g. 
decimals with
-    /// different scales can compare equal). This means the hash-equality 
contract (equal objects
-    /// must have equal hashes) may be violated for Decimal literals with 
different scales. In
-    /// practice this is safe because all current std::unordered_map<Literal, 
...> usages (bitmap
-    /// file index) only store values from the same column, which guarantees a 
fixed precision and
-    /// scale.
+    /// @note HashCode() canonicalizes all floating-point NaNs so that values 
considered equal by
+    /// CompareTo() have the same hash. Decimal values include their scale in 
the hash, while
+    /// CompareTo() uses numeric equality, so Decimal literals with different 
scales can still
+    /// violate the hash-equality contract. In practice this is safe because 
all current
+    /// std::unordered_map<Literal, ...> usages only store values from the 
same column, which has a
+    /// fixed precision and scale.
     size_t HashCode() const;
 
     /// Compares this literal with another literal. The comparison follows SQL 
semantics for the
diff --git a/src/paimon/common/data/variant/generic_variant_test.cpp 
b/src/paimon/common/data/variant/generic_variant_test.cpp
index 11386a28..bc46a573 100644
--- a/src/paimon/common/data/variant/generic_variant_test.cpp
+++ b/src/paimon/common/data/variant/generic_variant_test.cpp
@@ -19,6 +19,7 @@
 
 #include "paimon/common/data/variant/generic_variant.h"
 
+#include <cstdint>
 #include <functional>
 #include <limits>
 #include <string>
@@ -27,6 +28,7 @@
 #include "gtest/gtest.h"
 #include "paimon/common/data/variant/variant_builder.h"
 #include "paimon/common/data/variant/variant_defs.h"
+#include "paimon/common/utils/math.h"
 #include "paimon/memory/memory_pool.h"
 #include "paimon/testing/utils/testharness.h"
 
@@ -350,6 +352,23 @@ TEST_F(GenericVariantTest, NonFiniteDoubleToJson) {
     ASSERT_EQ(json, "\"Infinity\"");
 }
 
+TEST_F(GenericVariantTest, CanonicalizesFloatingPointNaN) {
+    {
+        VariantBuilder builder(false);
+        
ASSERT_OK(builder.AppendFloat(FloatingPointFromBits<float>(0xffc12345U)));
+        ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> variant, 
builder.Build(pool_));
+        ASSERT_OK_AND_ASSIGN(std::string_view value, variant->Value());
+        ASSERT_EQ(ToHex(value), "380000c07f");
+    }
+    {
+        VariantBuilder builder(false);
+        
ASSERT_OK(builder.AppendDouble(FloatingPointFromBits<double>(0xfff8123456789abcULL)));
+        ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> variant, 
builder.Build(pool_));
+        ASSERT_OK_AND_ASSIGN(std::string_view value, variant->Value());
+        ASSERT_EQ(ToHex(value), "1c000000000000f87f");
+    }
+}
+
 TEST_F(GenericVariantTest, GetTypeInfoReturnsHeaderBits) {
     // GetTypeInfo exposes the primitive header's type-info bits; 42 is 
encoded as an int1.
     auto v = FromJson("42");
diff --git a/src/paimon/common/data/variant/variant_builder.cpp 
b/src/paimon/common/data/variant/variant_builder.cpp
index b3cf7ee5..51f70417 100644
--- a/src/paimon/common/data/variant/variant_builder.cpp
+++ b/src/paimon/common/data/variant/variant_builder.cpp
@@ -30,6 +30,7 @@
 
 #include "fmt/format.h"
 #include "paimon/common/data/variant/variant_defs.h"
+#include "paimon/common/utils/math.h"
 #include "rapidjson/error/en.h"
 #include "rapidjson/memorystream.h"
 #include "rapidjson/reader.h"
@@ -339,8 +340,7 @@ Status VariantBuilder::AppendLong(int64_t l) {
 Status VariantBuilder::AppendDouble(double d) {
     PAIMON_RETURN_NOT_OK(CheckCapacity(1 + 8));
     write_buffer_[write_pos_++] = 
VariantBinaryUtil::PrimitiveHeader(VariantDefs::kDouble);
-    int64_t bits;
-    memcpy(&bits, &d, sizeof(bits));
+    const int64_t bits = CanonicalizeDoubleToLongBits(d);
     VariantBinaryUtil::WriteLong(bits, 8, write_buffer_.data(), write_pos_);
     write_pos_ += 8;
     return Status::OK();
@@ -409,8 +409,7 @@ Status VariantBuilder::AppendTimestampNtz(int64_t 
micros_since_epoch) {
 Status VariantBuilder::AppendFloat(float f) {
     PAIMON_RETURN_NOT_OK(CheckCapacity(1 + 4));
     write_buffer_[write_pos_++] = 
VariantBinaryUtil::PrimitiveHeader(VariantDefs::kFloat);
-    int32_t bits;
-    memcpy(&bits, &f, sizeof(bits));
+    const int32_t bits = CanonicalizeFloatToIntBits(f);
     VariantBinaryUtil::WriteLong(bits, 4, write_buffer_.data(), write_pos_);
     write_pos_ += 4;
     return Status::OK();
diff --git a/src/paimon/common/file_index/bloomfilter/fast_hash.cpp 
b/src/paimon/common/file_index/bloomfilter/fast_hash.cpp
index b1d8784f..ef63f0fc 100644
--- a/src/paimon/common/file_index/bloomfilter/fast_hash.cpp
+++ b/src/paimon/common/file_index/bloomfilter/fast_hash.cpp
@@ -19,7 +19,6 @@
 #include "paimon/common/file_index/bloomfilter/fast_hash.h"
 
 #include <cassert>
-#include <cmath>
 #include <cstring>
 #include <string>
 #include <utility>
@@ -28,6 +27,7 @@
 #include "paimon/common/utils/checked_cast.h"
 #include "paimon/common/utils/date_time_utils.h"
 #include "paimon/common/utils/field_type_utils.h"
+#include "paimon/common/utils/math.h"
 #include "paimon/data/timestamp.h"
 #include "paimon/defs.h"
 #include "paimon/file_index/file_index_result.h"
@@ -35,11 +35,6 @@
 #include "xxhash.h"  // NOLINT(build/include_subdir)
 
 namespace paimon {
-namespace {
-constexpr int32_t kCanonicalFloatNaNBits = 0x7fc00000;
-constexpr int64_t kCanonicalDoubleNaNBits = 0x7ff8000000000000L;
-}  // namespace
-
 Result<FastHash::HashFunction> FastHash::GetHashFunction(
     const std::shared_ptr<arrow::DataType>& arrow_type) {
     PAIMON_ASSIGN_OR_RAISE(FieldType field_type,
@@ -64,23 +59,11 @@ Result<FastHash::HashFunction> FastHash::GetHashFunction(
             });
         case FieldType::FLOAT:
             return HashFunction([](const Literal& literal) -> int64_t {
-                const auto raw_value = literal.GetValue<float>();
-                if (std::isnan(raw_value)) {
-                    return GetLongHash(kCanonicalFloatNaNBits);
-                }
-                int32_t bits = 0;
-                std::memcpy(&bits, &raw_value, sizeof(raw_value));
-                return GetLongHash(bits);
+                return 
GetLongHash(CanonicalizeFloatToIntBits(literal.GetValue<float>()));
             });
         case FieldType::DOUBLE:
             return HashFunction([](const Literal& literal) -> int64_t {
-                const auto raw_value = literal.GetValue<double>();
-                if (std::isnan(raw_value)) {
-                    return GetLongHash(kCanonicalDoubleNaNBits);
-                }
-                int64_t bits;
-                std::memcpy(&bits, &raw_value, sizeof(raw_value));
-                return GetLongHash(bits);
+                return 
GetLongHash(CanonicalizeDoubleToLongBits(literal.GetValue<double>()));
             });
         case FieldType::TIMESTAMP: {
             auto ts_type = 
checked_pointer_cast<arrow::TimestampType>(arrow_type);
diff --git a/src/paimon/common/file_index/bloomfilter/fast_hash_test.cpp 
b/src/paimon/common/file_index/bloomfilter/fast_hash_test.cpp
index 8a528e44..6ffbc796 100644
--- a/src/paimon/common/file_index/bloomfilter/fast_hash_test.cpp
+++ b/src/paimon/common/file_index/bloomfilter/fast_hash_test.cpp
@@ -20,12 +20,12 @@
 
 #include <cmath>
 #include <cstdint>
-#include <cstring>
 #include <limits>
 #include <string>
 #include <vector>
 
 #include "gtest/gtest.h"
+#include "paimon/common/utils/math.h"
 #include "paimon/data/timestamp.h"
 #include "paimon/defs.h"
 #include "paimon/file_index/file_index_result.h"
@@ -168,26 +168,16 @@ TEST_F(FastHashTest, TestCompatibleWithJava) {
 }
 
 TEST_F(FastHashTest, TestNaNCompatibleWithJava) {
-    auto float_from_bits = [](uint32_t bits) {
-        float value;
-        std::memcpy(&value, &bits, sizeof(value));
-        return value;
-    };
-    const float float_nan = float_from_bits(0x7fc12345);
-    const float negative_float_nan = float_from_bits(0xffc54321);
+    const auto float_nan = FloatingPointFromBits<float>(0x7fc12345U);
+    const auto negative_float_nan = FloatingPointFromBits<float>(0xffc54321U);
     ASSERT_TRUE(std::isnan(float_nan));
     ASSERT_TRUE(std::isnan(negative_float_nan));
     ASSERT_OK_AND_ASSIGN(auto float_hash_function, 
FastHash::GetHashFunction(arrow::float32()));
     CheckResult(float_hash_function, {Literal(float_nan), 
Literal(negative_float_nan)},
                 {0x67c27c6d9936ae63, 0x67c27c6d9936ae63});
 
-    auto double_from_bits = [](uint64_t bits) {
-        double value;
-        std::memcpy(&value, &bits, sizeof(value));
-        return value;
-    };
-    const double double_nan = double_from_bits(0x7ff8123456789abc);
-    const double negative_double_nan = double_from_bits(0xfff8abcdef012345);
+    const auto double_nan = 
FloatingPointFromBits<double>(0x7ff8123456789abcULL);
+    const auto negative_double_nan = 
FloatingPointFromBits<double>(0xfff8abcdef012345ULL);
     ASSERT_TRUE(std::isnan(double_nan));
     ASSERT_TRUE(std::isnan(negative_double_nan));
     ASSERT_OK_AND_ASSIGN(auto double_hash_function, 
FastHash::GetHashFunction(arrow::float64()));
diff --git a/src/paimon/common/global_index/btree/key_serializer.cpp 
b/src/paimon/common/global_index/btree/key_serializer.cpp
index 464f37ea..4b66d1d1 100644
--- a/src/paimon/common/global_index/btree/key_serializer.cpp
+++ b/src/paimon/common/global_index/btree/key_serializer.cpp
@@ -27,6 +27,7 @@
 #include "paimon/common/utils/date_time_utils.h"
 #include "paimon/common/utils/field_type_utils.h"
 #include "paimon/common/utils/fields_comparator.h"
+#include "paimon/common/utils/math.h"
 #include "paimon/common/utils/preconditions.h"
 #include "paimon/common/utils/var_length_int_utils.h"
 #include "paimon/data/decimal.h"
@@ -164,19 +165,13 @@ Result<std::shared_ptr<Bytes>> 
KeySerializer::SerializeKey(
         case FieldType::FLOAT: {
             MemorySliceOutput output(4, pool);
             output.Reset();
-            auto fvalue = literal.GetValue<float>();
-            int32_t ivalue;
-            memcpy(&ivalue, &fvalue, sizeof(float));
-            output.WriteValue<int32_t>(ivalue);
+            
output.WriteValue<int32_t>(CanonicalizeFloatToIntBits(literal.GetValue<float>()));
             return output.ToSlice().CopyBytes(pool);
         }
         case FieldType::DOUBLE: {
             MemorySliceOutput output(8, pool);
             output.Reset();
-            auto dvalue = literal.GetValue<double>();
-            int64_t ivalue;
-            memcpy(&ivalue, &dvalue, sizeof(double));
-            output.WriteValue<int64_t>(ivalue);
+            
output.WriteValue<int64_t>(CanonicalizeDoubleToLongBits(literal.GetValue<double>()));
             return output.ToSlice().CopyBytes(pool);
         }
         case FieldType::STRING: {
diff --git a/src/paimon/common/global_index/btree/key_serializer_test.cpp 
b/src/paimon/common/global_index/btree/key_serializer_test.cpp
index e36e72ed..61322fde 100644
--- a/src/paimon/common/global_index/btree/key_serializer_test.cpp
+++ b/src/paimon/common/global_index/btree/key_serializer_test.cpp
@@ -19,7 +19,11 @@
 
 #include "paimon/common/global_index/btree/key_serializer.h"
 
+#include <cstdint>
+#include <string>
+
 #include "gtest/gtest.h"
+#include "paimon/common/utils/math.h"
 #include "paimon/data/decimal.h"
 #include "paimon/data/timestamp.h"
 #include "paimon/testing/utils/testharness.h"
@@ -208,6 +212,30 @@ TEST_F(KeySerializerTest, SerializeAndDeserializeAllTypes) 
{
     }
 }
 
+TEST_F(KeySerializerTest, CanonicalizesFloatingPointNaN) {
+    const auto float_nan = FloatingPointFromBits<float>(0xffc12345U);
+    const auto canonical_float_nan = 
FloatingPointFromBits<float>(kCanonicalFloatNaNBits);
+    ASSERT_OK_AND_ASSIGN(
+        std::shared_ptr<Bytes> float_bytes,
+        KeySerializer::SerializeKey(Literal(float_nan), arrow::float32(), 
pool_.get()));
+    ASSERT_OK_AND_ASSIGN(
+        std::shared_ptr<Bytes> canonical_float_bytes,
+        KeySerializer::SerializeKey(Literal(canonical_float_nan), 
arrow::float32(), pool_.get()));
+    ASSERT_EQ(std::string(float_bytes->data(), float_bytes->size()),
+              std::string(canonical_float_bytes->data(), 
canonical_float_bytes->size()));
+
+    const auto double_nan = 
FloatingPointFromBits<double>(0xfff8123456789abcULL);
+    const auto canonical_double_nan = 
FloatingPointFromBits<double>(kCanonicalDoubleNaNBits);
+    ASSERT_OK_AND_ASSIGN(
+        std::shared_ptr<Bytes> double_bytes,
+        KeySerializer::SerializeKey(Literal(double_nan), arrow::float64(), 
pool_.get()));
+    ASSERT_OK_AND_ASSIGN(
+        std::shared_ptr<Bytes> canonical_double_bytes,
+        KeySerializer::SerializeKey(Literal(canonical_double_nan), 
arrow::float64(), pool_.get()));
+    ASSERT_EQ(std::string(double_bytes->data(), double_bytes->size()),
+              std::string(canonical_double_bytes->data(), 
canonical_double_bytes->size()));
+}
+
 TEST_F(KeySerializerTest, RejectsMalformedSerializedKeys) {
     auto wrap = [this](const std::string& value) {
         return MemorySlice::Wrap(std::make_shared<Bytes>(value, pool_.get()));
diff --git a/src/paimon/common/global_index/global_index_result.cpp 
b/src/paimon/common/global_index/global_index_result.cpp
index f329b036..d2b94f21 100644
--- a/src/paimon/common/global_index/global_index_result.cpp
+++ b/src/paimon/common/global_index/global_index_result.cpp
@@ -22,6 +22,7 @@
 #include "fmt/format.h"
 #include "paimon/common/io/memory_segment_output_stream.h"
 #include "paimon/common/memory/memory_segment_utils.h"
+#include "paimon/common/utils/math.h"
 #include "paimon/global_index/bitmap_global_index_result.h"
 #include "paimon/global_index/bitmap_scored_global_index_result.h"
 #include "paimon/io/byte_array_input_stream.h"
@@ -37,8 +38,8 @@ void WriteBitmapAndScores(const RoaringBitmap64* bitmap, 
const std::vector<float
     out->WriteBytes(bitmap_bytes);
 
     out->WriteValue<int32_t>(scores.size());
-    for (auto score : scores) {
-        out->WriteValue<float>(score);
+    for (float score : scores) {
+        out->WriteValue<float>(CanonicalizeFloatingPoint(score));
     }
 }
 
diff --git a/src/paimon/common/global_index/global_index_result_test.cpp 
b/src/paimon/common/global_index/global_index_result_test.cpp
index 73c6d05e..3179e671 100644
--- a/src/paimon/common/global_index/global_index_result_test.cpp
+++ b/src/paimon/common/global_index/global_index_result_test.cpp
@@ -19,9 +19,12 @@
 
 #include "paimon/global_index/global_index_result.h"
 
+#include <cmath>
+#include <cstdint>
 #include <utility>
 
 #include "gtest/gtest.h"
+#include "paimon/common/utils/math.h"
 #include "paimon/global_index/bitmap_global_index_result.h"
 #include "paimon/global_index/bitmap_scored_global_index_result.h"
 #include "paimon/testing/utils/testharness.h"
@@ -144,6 +147,29 @@ TEST_F(GlobalIndexResultTest, 
TestSerializeAndDeserializeWithScore) {
                                                 serialize_bytes->data() + 
serialize_bytes->size()));
 }
 
+TEST_F(GlobalIndexResultTest, TestSerializeCanonicalizesNaNScore) {
+    auto pool = GetDefaultPool();
+    const auto payload_nan = FloatingPointFromBits<float>(0xffc12345U);
+    const auto canonical_nan = 
FloatingPointFromBits<float>(kCanonicalFloatNaNBits);
+    auto index_result = std::make_shared<BitmapScoredGlobalIndexResult>(
+        RoaringBitmap64::From({1}), std::vector<float>{payload_nan});
+    auto canonical_index_result = 
std::make_shared<BitmapScoredGlobalIndexResult>(
+        RoaringBitmap64::From({1}), std::vector<float>{canonical_nan});
+
+    ASSERT_OK_AND_ASSIGN(PAIMON_UNIQUE_PTR<Bytes> serialized,
+                         GlobalIndexResult::Serialize(index_result, pool));
+    ASSERT_OK_AND_ASSIGN(PAIMON_UNIQUE_PTR<Bytes> canonical_serialized,
+                         GlobalIndexResult::Serialize(canonical_index_result, 
pool));
+    ASSERT_EQ(*serialized, *canonical_serialized);
+
+    ASSERT_OK_AND_ASSIGN(
+        std::shared_ptr<GlobalIndexResult> deserialized,
+        GlobalIndexResult::Deserialize(serialized->data(), serialized->size(), 
pool));
+    auto scored_result = 
std::dynamic_pointer_cast<BitmapScoredGlobalIndexResult>(deserialized);
+    ASSERT_TRUE(scored_result);
+    ASSERT_TRUE(std::isnan(scored_result->GetScores()[0]));
+}
+
 TEST_F(GlobalIndexResultTest, TestInvalidSerialize) {
     auto pool = GetDefaultPool();
     auto result = 
std::make_shared<FakeGlobalIndexResult>(std::vector<int64_t>({1, 3, 5, 100}));
diff --git a/src/paimon/common/predicate/literal.cpp 
b/src/paimon/common/predicate/literal.cpp
index 3b2bcc0e..d679c2cc 100644
--- a/src/paimon/common/predicate/literal.cpp
+++ b/src/paimon/common/predicate/literal.cpp
@@ -18,7 +18,6 @@
 
 #include "paimon/predicate/literal.h"
 
-#include <cmath>
 #include <cstring>
 #include <functional>
 #include <sstream>
@@ -29,6 +28,7 @@
 #include "fmt/format.h"
 #include "paimon/common/utils/field_type_utils.h"
 #include "paimon/common/utils/fields_comparator.h"
+#include "paimon/common/utils/math.h"
 #include "paimon/data/decimal.h"
 #include "paimon/data/timestamp.h"
 #include "paimon/status.h"
@@ -63,9 +63,9 @@ class Literal::Impl {
             case FieldType::BIGINT:
                 return std::hash<int64_t>{}(value_.BigIntVal);
             case FieldType::FLOAT:
-                return std::hash<float>{}(value_.FloatVal);
+                return 
std::hash<float>{}(CanonicalizeFloatingPoint(value_.FloatVal));
             case FieldType::DOUBLE:
-                return std::hash<double>{}(value_.DoubleVal);
+                return 
std::hash<double>{}(CanonicalizeFloatingPoint(value_.DoubleVal));
             case FieldType::STRING:
             case FieldType::BINARY:
                 return 
std::hash<std::string_view>{}(std::string_view(value_.Buffer, size_));
diff --git a/src/paimon/common/utils/math.h b/src/paimon/common/utils/math.h
index 6aba6523..b9e2e310 100644
--- a/src/paimon/common/utils/math.h
+++ b/src/paimon/common/utils/math.h
@@ -28,6 +28,7 @@
 #pragma once
 
 #include <cassert>
+#include <cmath>
 #include <cstddef>
 #include <cstdint>
 #include <cstring>
@@ -41,6 +42,51 @@
 
 namespace paimon {
 
+inline constexpr uint32_t kCanonicalFloatNaNBits = 0x7fc00000;
+inline constexpr uint64_t kCanonicalDoubleNaNBits = 0x7ff8000000000000;
+
+template <typename FloatingPoint, typename Bits>
+inline FloatingPoint FloatingPointFromBits(Bits bits) {
+    static_assert(std::is_floating_point_v<FloatingPoint>);
+    static_assert(std::is_integral_v<Bits>);
+    static_assert(sizeof(FloatingPoint) == sizeof(Bits));
+    FloatingPoint value;
+    std::memcpy(&value, &bits, sizeof(value));
+    return value;
+}
+
+inline float CanonicalizeFloatingPoint(float value) {
+    if (std::isnan(value)) {
+        return FloatingPointFromBits<float>(kCanonicalFloatNaNBits);
+    }
+    return value;
+}
+
+inline double CanonicalizeFloatingPoint(double value) {
+    if (std::isnan(value)) {
+        return FloatingPointFromBits<double>(kCanonicalDoubleNaNBits);
+    }
+    return value;
+}
+
+inline int32_t CanonicalizeFloatToIntBits(float value) {
+    if (std::isnan(value)) {
+        return static_cast<int32_t>(kCanonicalFloatNaNBits);
+    }
+    int32_t bits;
+    std::memcpy(&bits, &value, sizeof(bits));
+    return bits;
+}
+
+inline int64_t CanonicalizeDoubleToLongBits(double value) {
+    if (std::isnan(value)) {
+        return static_cast<int64_t>(kCanonicalDoubleNaNBits);
+    }
+    int64_t bits;
+    std::memcpy(&bits, &value, sizeof(bits));
+    return bits;
+}
+
 template <typename To, typename From>
 constexpr bool InRange(From value) {
     static_assert(std::is_integral_v<To> && std::is_integral_v<From>,
diff --git a/src/paimon/common/utils/math_test.cpp 
b/src/paimon/common/utils/math_test.cpp
index 49d31d47..ce9379f6 100644
--- a/src/paimon/common/utils/math_test.cpp
+++ b/src/paimon/common/utils/math_test.cpp
@@ -28,6 +28,29 @@
 
 namespace paimon::test {
 
+TEST(MathTest, FloatingPointNaNCanonicalization) {
+    const auto float_nan = 
CanonicalizeFloatingPoint(FloatingPointFromBits<float>(0xffc12345U));
+    uint32_t float_nan_bits;
+    std::memcpy(&float_nan_bits, &float_nan, sizeof(float_nan_bits));
+    ASSERT_EQ(kCanonicalFloatNaNBits, float_nan_bits);
+    ASSERT_EQ(static_cast<int32_t>(kCanonicalFloatNaNBits),
+              
CanonicalizeFloatToIntBits(FloatingPointFromBits<float>(0x7fa12345U)));
+
+    const auto double_nan =
+        
CanonicalizeFloatingPoint(FloatingPointFromBits<double>(0xfff8123456789abcULL));
+    uint64_t double_nan_bits;
+    std::memcpy(&double_nan_bits, &double_nan, sizeof(double_nan_bits));
+    ASSERT_EQ(kCanonicalDoubleNaNBits, double_nan_bits);
+    ASSERT_EQ(static_cast<int64_t>(kCanonicalDoubleNaNBits),
+              
CanonicalizeDoubleToLongBits(FloatingPointFromBits<double>(0x7ff123456789abcdULL)));
+
+    const float negative_zero = CanonicalizeFloatingPoint(-0.0f);
+    uint32_t negative_zero_bits;
+    std::memcpy(&negative_zero_bits, &negative_zero, 
sizeof(negative_zero_bits));
+    ASSERT_EQ(0x80000000U, negative_zero_bits);
+    ASSERT_EQ(0x3ff0000000000000, CanonicalizeDoubleToLongBits(1.0));
+}
+
 // Test case: Test EndianSwapValue for different integral types
 TEST(MathTest, EndianSwapValue) {
     // Test 16-bit value
diff --git a/src/paimon/core/bucket/hive_bucket_function.cpp 
b/src/paimon/core/bucket/hive_bucket_function.cpp
index 913053c1..e87c292d 100644
--- a/src/paimon/core/bucket/hive_bucket_function.cpp
+++ b/src/paimon/core/bucket/hive_bucket_function.cpp
@@ -19,13 +19,12 @@
 #include "paimon/core/bucket/hive_bucket_function.h"
 
 #include <cassert>
-#include <cmath>
-#include <cstring>
 #include <limits>
 
 #include "fmt/format.h"
 #include "paimon/common/data/binary_row.h"
 #include "paimon/common/utils/field_type_utils.h"
+#include "paimon/common/utils/math.h"
 #include "paimon/core/bucket/hive_hasher.h"
 #include "paimon/status.h"
 
@@ -105,10 +104,8 @@ uint32_t HiveBucketFunction::ComputeHash(const BinaryRow& 
row, int32_t field_ind
             uint32_t bits;
             if (float_value == -0.0f) {
                 bits = 0;
-            } else if (std::isnan(float_value)) {
-                bits = 0x7FC00000U;
             } else {
-                std::memcpy(&bits, &float_value, sizeof(bits));
+                bits = 
static_cast<uint32_t>(CanonicalizeFloatToIntBits(float_value));
             }
             return HiveHasher::HashInt(bits);
         }
@@ -117,10 +114,8 @@ uint32_t HiveBucketFunction::ComputeHash(const BinaryRow& 
row, int32_t field_ind
             uint64_t bits;
             if (double_value == -0.0) {
                 bits = 0;
-            } else if (std::isnan(double_value)) {
-                bits = 0x7FF8000000000000ULL;
             } else {
-                std::memcpy(&bits, &double_value, sizeof(bits));
+                bits = 
static_cast<uint64_t>(CanonicalizeDoubleToLongBits(double_value));
             }
             return HiveHasher::HashLong(bits);
         }
diff --git a/src/paimon/core/bucket/hive_bucket_function_test.cpp 
b/src/paimon/core/bucket/hive_bucket_function_test.cpp
index 21f2a984..d97a0294 100644
--- a/src/paimon/core/bucket/hive_bucket_function_test.cpp
+++ b/src/paimon/core/bucket/hive_bucket_function_test.cpp
@@ -18,12 +18,12 @@
 
 #include "paimon/core/bucket/hive_bucket_function.h"
 
-#include <cstring>
 #include <limits>
 
 #include "gtest/gtest.h"
 #include "paimon/common/data/binary_row.h"
 #include "paimon/common/data/binary_row_writer.h"
+#include "paimon/common/utils/math.h"
 #include "paimon/core/bucket/hive_hasher.h"
 #include "paimon/memory/memory_pool.h"
 #include "paimon/testing/utils/binary_row_generator.h"
@@ -111,18 +111,6 @@ class HiveBucketFunctionTest : public ::testing::Test {
         auto pool = GetDefaultPool();
         return BinaryRowGenerator::GenerateRow({value}, pool.get());
     }
-
-    float FloatFromBits(uint32_t bits) {
-        float value;
-        std::memcpy(&value, &bits, sizeof(value));
-        return value;
-    }
-
-    double DoubleFromBits(uint64_t bits) {
-        double value;
-        std::memcpy(&value, &bits, sizeof(value));
-        return value;
-    }
 };
 
 /// Test matching Java: testHiveBucketFunction
@@ -235,11 +223,12 @@ TEST_F(HiveBucketFunctionTest, 
TestFloatNaNCanonicalizationCompatibleWithJava) {
     ASSERT_OK_AND_ASSIGN(auto func, HiveBucketFunction::Create(field_types));
 
     // Verified with Java HiveBucketFunction:
-    // Float.NaN, Float.intBitsToFloat(0x7fa12345), and 
Float.intBitsToFloat(0x7fc00000)
-    // all hash through Float.floatToIntBits(...) = 0x7fc00000.
+    // Float.NaN, a payload NaN, and the canonical NaN all hash through
+    // Float.floatToIntBits(...) to kCanonicalFloatNaNBits.
     ASSERT_EQ(344, 
func->Bucket(CreateFloatRow(std::numeric_limits<float>::quiet_NaN()), 1000));
-    ASSERT_EQ(344, func->Bucket(CreateFloatRow(FloatFromBits(0x7FA12345U)), 
1000));
-    ASSERT_EQ(344, func->Bucket(CreateFloatRow(FloatFromBits(0x7FC00000U)), 
1000));
+    ASSERT_EQ(344, 
func->Bucket(CreateFloatRow(FloatingPointFromBits<float>(0x7FA12345U)), 1000));
+    ASSERT_EQ(344, func->Bucket(
+                       
CreateFloatRow(FloatingPointFromBits<float>(kCanonicalFloatNaNBits)), 1000));
 }
 
 TEST_F(HiveBucketFunctionTest, 
TestDoubleNaNCanonicalizationCompatibleWithJava) {
@@ -248,10 +237,14 @@ TEST_F(HiveBucketFunctionTest, 
TestDoubleNaNCanonicalizationCompatibleWithJava)
 
     // Verified with Java HiveBucketFunction:
     // Double.NaN, Double.longBitsToDouble(0x7ff123456789abcd), and canonical 
NaN
-    // all hash through Double.doubleToLongBits(...) = 0x7ff8000000000000.
+    // All NaNs hash through Double.doubleToLongBits(...) to 
kCanonicalDoubleNaNBits.
     ASSERT_EQ(360, 
func->Bucket(CreateDoubleRow(std::numeric_limits<double>::quiet_NaN()), 1000));
-    ASSERT_EQ(360, 
func->Bucket(CreateDoubleRow(DoubleFromBits(0x7FF123456789ABCDULL)), 1000));
-    ASSERT_EQ(360, 
func->Bucket(CreateDoubleRow(DoubleFromBits(0x7FF8000000000000ULL)), 1000));
+    ASSERT_EQ(
+        360,
+        
func->Bucket(CreateDoubleRow(FloatingPointFromBits<double>(0x7FF123456789ABCDULL)),
 1000));
+    ASSERT_EQ(360,
+              
func->Bucket(CreateDoubleRow(FloatingPointFromBits<double>(kCanonicalDoubleNaNBits)),
+                           1000));
 }
 
 TEST_F(HiveBucketFunctionTest, TestTinyintNegativeValuesCompatibleWithJava) {
diff --git a/src/paimon/core/global_index/indexed_split_test.cpp 
b/src/paimon/core/global_index/indexed_split_test.cpp
index 7cd12125..03cd2976 100644
--- a/src/paimon/core/global_index/indexed_split_test.cpp
+++ b/src/paimon/core/global_index/indexed_split_test.cpp
@@ -17,6 +17,8 @@
  * under the License.
  */
 
+#include <cmath>
+#include <cstdint>
 #include <memory>
 #include <string>
 #include <utility>
@@ -26,6 +28,7 @@
 #include "gtest/gtest.h"
 #include "paimon/common/data/binary_row.h"
 #include "paimon/common/data/data_define.h"
+#include "paimon/common/utils/math.h"
 #include "paimon/core/global_index/indexed_split_impl.h"
 #include "paimon/core/table/source/data_split_impl.h"
 #include "paimon/fs/local/local_file_system.h"
@@ -155,6 +158,34 @@ TEST(IndexedSplitTest, TestIndexedSplitWithScore) {
         << roundtrip_indexed_split->ToString();
 }
 
+TEST(IndexedSplitTest, TestSerializeCanonicalizesNaNScore) {
+    auto pool = GetDefaultPool();
+    DataSplitImpl::Builder builder(
+        /*partition=*/BinaryRow::EmptyRow(),
+        /*bucket=*/0, /*bucket_path=*/"bucket-0",
+        /*data_files=*/{});
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<DataSplitImpl> data_split, 
builder.Build());
+
+    const auto payload_nan = FloatingPointFromBits<float>(0xffc12345U);
+    const auto canonical_nan = 
FloatingPointFromBits<float>(kCanonicalFloatNaNBits);
+    auto indexed_split = std::make_shared<IndexedSplitImpl>(
+        data_split, std::vector<Range>{Range(0, 0)}, 
std::vector<float>{payload_nan});
+    auto canonical_indexed_split = std::make_shared<IndexedSplitImpl>(
+        data_split, std::vector<Range>{Range(0, 0)}, 
std::vector<float>{canonical_nan});
+
+    ASSERT_OK_AND_ASSIGN(std::string serialized, 
Split::Serialize(indexed_split, pool));
+    ASSERT_OK_AND_ASSIGN(std::string canonical_serialized,
+                         Split::Serialize(canonical_indexed_split, pool));
+    ASSERT_EQ(serialized, canonical_serialized);
+
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<Split> roundtrip,
+                         Split::Deserialize(serialized.data(), 
serialized.size(), pool));
+    auto roundtrip_indexed_split = 
std::dynamic_pointer_cast<IndexedSplitImpl>(roundtrip);
+    ASSERT_TRUE(roundtrip_indexed_split);
+    ASSERT_EQ(roundtrip_indexed_split->Scores().size(), 1);
+    ASSERT_TRUE(std::isnan(roundtrip_indexed_split->Scores()[0]));
+}
+
 TEST(IndexedSplitTest, TestValidate) {
     auto meta = std::make_shared<DataFileMeta>(
         "file.orc", 1l, 200l, BinaryRow::EmptyRow(), BinaryRow::EmptyRow(),
diff --git a/src/paimon/core/table/source/split.cpp 
b/src/paimon/core/table/source/split.cpp
index 007df3cb..d6117a89 100644
--- a/src/paimon/core/table/source/split.cpp
+++ b/src/paimon/core/table/source/split.cpp
@@ -23,6 +23,7 @@
 #include "paimon/common/data/binary_row.h"
 #include "paimon/common/io/memory_segment_output_stream.h"
 #include "paimon/common/memory/memory_segment_utils.h"
+#include "paimon/common/utils/math.h"
 #include "paimon/common/utils/serialization_utils.h"
 #include "paimon/core/global_index/indexed_split_impl.h"
 #include "paimon/core/io/data_file_meta_serializer.h"
@@ -159,8 +160,8 @@ Result<std::string> Split::Serialize(const 
std::shared_ptr<Split>& split,
         if (!scores.empty()) {
             out.WriteValue<bool>(true);
             out.WriteValue<int32_t>(scores.size());
-            for (const auto& score : scores) {
-                out.WriteValue<float>(score);
+            for (float score : scores) {
+                out.WriteValue<float>(CanonicalizeFloatingPoint(score));
             }
         } else {
             out.WriteValue<bool>(false);

Reply via email to