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

yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 2709af1d507 branch-4.1: [fix](zonemap) Do not treat a legacy float or 
double zone map as NaN-free #67779 (#68102)
2709af1d507 is described below

commit 2709af1d5075b9ce3c23a7a9eae4433b71e17fbb
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Sep 17 21:15:46 2026 +0800

    branch-4.1: [fix](zonemap) Do not treat a legacy float or double zone map 
as NaN-free #67779 (#68102)
    
    Cherry-picked from #67779
    
    Co-authored-by: YangJie <[email protected]>
---
 be/src/storage/index/zone_map/zone_map_index.cpp | 14 +++++
 be/test/storage/segment/zone_map_index_test.cpp  | 80 +++++++++++++++++++++++-
 2 files changed, 92 insertions(+), 2 deletions(-)

diff --git a/be/src/storage/index/zone_map/zone_map_index.cpp 
b/be/src/storage/index/zone_map/zone_map_index.cpp
index ef4f77fe97f..2ee6f828d1e 100644
--- a/be/src/storage/index/zone_map/zone_map_index.cpp
+++ b/be/src/storage/index/zone_map/zone_map_index.cpp
@@ -79,6 +79,20 @@ Status ZoneMap::from_proto(const ZoneMapPB& zone_map, const 
DataTypePtr& data_ty
     };
 
     auto field_type = data_type->get_storage_field_type();
+
+    // has_nan arrived with NaN-aware float/double zone maps, so its absence 
means the writer could
+    // not report NaN and the bounds came from a comparison that never selects 
one: a hidden NaN
+    // cannot be ruled out. Doris orders NaN above every other value, so `x > 
c` can be true for a
+    // row that these bounds say cannot exist. Treat such a zone map as 
covering everything instead
+    // of as NaN-free. A zone with no non-null value never received one, so it 
has no NaN to hide;
+    // leaving it usable keeps the three ColumnPredicate checks that start 
from has_not_null: the
+    // null predicates, each comparison predicate's early return, and the 
in-list one's.
+    if ((field_type == FieldType::OLAP_FIELD_TYPE_FLOAT ||
+         field_type == FieldType::OLAP_FIELD_TYPE_DOUBLE) &&
+        zone_map.has_not_null() && !zone_map.has_has_nan()) {
+        zone_map_info.pass_all = true;
+    }
+
     // min value and max value are valid if has_not_null is true
     if (zone_map.has_not_null()) {
         if (!zone_map_info.pass_all) {
diff --git a/be/test/storage/segment/zone_map_index_test.cpp 
b/be/test/storage/segment/zone_map_index_test.cpp
index 2ea9503b0b9..36a4883f7c1 100644
--- a/be/test/storage/segment/zone_map_index_test.cpp
+++ b/be/test/storage/segment/zone_map_index_test.cpp
@@ -1014,6 +1014,74 @@ TEST_F(ColumnZoneMapTest, DoubleFiniteExtremesRoundTrip) 
{
     EXPECT_EQ(pzm.max_value.get<TYPE_DOUBLE>(), 
std::numeric_limits<double>::max());
 }
 
+TEST_F(ColumnZoneMapTest, LegacyFloatZoneMapWithoutHasNanDegradesToPassAll) {
+    // A float or double zone map written before has_nan existed says nothing 
about NaN, and its
+    // bounds were produced by a comparison that never picks a NaN. Since 
Doris sorts NaN above
+    // every other value, trusting such bounds drops NaN rows from `>` and 
`>=`.
+    auto legacy_pb = [](double min_value, double max_value) {
+        ZoneMapPB pb;
+        pb.set_min(std::to_string(min_value));
+        pb.set_max(std::to_string(max_value));
+        pb.set_has_null(false);
+        pb.set_has_not_null(true);
+        pb.set_pass_all(false);
+        // Deliberately no set_has_nan / set_has_positive_inf / 
set_has_negative_inf.
+        return pb;
+    };
+
+    for (const auto primitive_type : {TYPE_FLOAT, TYPE_DOUBLE}) {
+        for (bool nullable : {false, true}) {
+            auto data_type = 
DataTypeFactory::instance().create_data_type(primitive_type, nullable);
+            ZoneMap zm;
+            ASSERT_TRUE(ZoneMap::from_proto(legacy_pb(1.0, 2.0), data_type, 
zm).ok());
+            EXPECT_TRUE(zm.pass_all) << "type=" << primitive_type << ", 
nullable=" << nullable;
+            // The null flags survive, and so does the pruning that reads only 
them:
+            // eval_null_zonemap never consults pass_all. The ColumnPredicate 
path in
+            // ColumnReader::_get_filtered_pages does return early on 
pass_all, so it loses its
+            // IS NULL pruning for this zone map.
+            EXPECT_TRUE(zm.has_not_null);
+            EXPECT_FALSE(zm.has_null);
+
+            // The same bounds from a writer that does report the flag stay 
usable.
+            auto pb = legacy_pb(1.0, 2.0);
+            pb.set_has_nan(false);
+            ZoneMap current;
+            ASSERT_TRUE(ZoneMap::from_proto(pb, data_type, current).ok());
+            EXPECT_FALSE(current.pass_all)
+                    << "type=" << primitive_type << ", nullable=" << nullable;
+            if (primitive_type == TYPE_DOUBLE) {
+                EXPECT_EQ(1.0, current.min_value.get<TYPE_DOUBLE>());
+                EXPECT_EQ(2.0, current.max_value.get<TYPE_DOUBLE>());
+            } else {
+                EXPECT_EQ(1.0F, current.min_value.get<TYPE_FLOAT>());
+                EXPECT_EQ(2.0F, current.max_value.get<TYPE_FLOAT>());
+            }
+
+            // A legacy zone with no non-null value never received one, so no 
NaN can hide in it.
+            // The bound text is irrelevant here: from_proto only parses it 
when has_not_null.
+            auto all_null = legacy_pb(1.0, 2.0);
+            all_null.set_has_null(true);
+            all_null.set_has_not_null(false);
+            ZoneMap all_null_zm;
+            ASSERT_TRUE(ZoneMap::from_proto(all_null, data_type, 
all_null_zm).ok());
+            EXPECT_FALSE(all_null_zm.pass_all)
+                    << "type=" << primitive_type << ", nullable=" << nullable;
+        }
+    }
+
+    // Non-floating columns never carried the flag and are unaffected.
+    auto int_type = DataTypeFactory::instance().create_data_type(TYPE_INT, 
false);
+    ZoneMapPB int_pb;
+    int_pb.set_min("1");
+    int_pb.set_max("9");
+    int_pb.set_has_null(false);
+    int_pb.set_has_not_null(true);
+    int_pb.set_pass_all(false);
+    ZoneMap int_zm;
+    ASSERT_TRUE(ZoneMap::from_proto(int_pb, int_type, int_zm).ok());
+    EXPECT_FALSE(int_zm.pass_all);
+}
+
 TEST_F(ColumnZoneMapTest, LegacyUnparsableDoubleBoundDegradesToPassAll) {
     auto make_zone_map = [](const std::string& min, const std::string& max) {
         ZoneMapPB pb;
@@ -1022,6 +1090,9 @@ TEST_F(ColumnZoneMapTest, 
LegacyUnparsableDoubleBoundDegradesToPassAll) {
         pb.set_has_null(false);
         pb.set_has_not_null(true);
         pb.set_pass_all(false);
+        // The current writer always reports this flag; leaving it out would 
mark the zone map as
+        // pre-NaN-tracking legacy metadata, which is a different test.
+        pb.set_has_nan(false);
         return pb;
     };
     // 16g renderings of ±DBL_MAX, both of which read back as ∓inf.
@@ -1162,10 +1233,15 @@ TEST_F(ColumnZoneMapTest, 
ReversedBoundsDegradeToPassAll) {
         pb.set_has_null(false);
         pb.set_has_not_null(true);
         pb.set_pass_all(false);
+        // The current writer always reports this flag; leaving it out would 
mark the zone map as
+        // pre-NaN-tracking legacy metadata, which is a different test.
+        pb.set_has_nan(false);
         return pb;
     };
-    // What a page of only NaN leaves behind before 4.0: bounds that never 
moved off the values
-    // the writer starts from, and that round-trip exactly, so only the 
reversal gives them away.
+    // add_values() starts each call from numeric_limits::max() and 
::lowest(), and a page whose
+    // only non-null values are NaN or infinity leaves them there, so the 
stored pair comes back
+    // reversed. These strings round-trip exactly, and the reversal is the 
first signal either way:
+    // is_reversed runs before the flag overrides.
     const std::string double_lowest = "-1.7976931348623157e+308";
     const std::string double_highest = "1.7976931348623157e+308";
     const std::string float_lowest = "-3.4028235e+38";


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to