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

csun5285 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 5fbe3600024 [fix](zonemap) Treat unparsable zone map as invalid 
(#67341)
5fbe3600024 is described below

commit 5fbe3600024aae89a4d32323f42b87fe6a24324f
Author: Chenyang Sun <[email protected]>
AuthorDate: Tue Sep 1 19:09:37 2026 +0800

    [fix](zonemap) Treat unparsable zone map as invalid (#67341)
    
    ### What problem does this PR solve?
    
    A zone map bound that fails to parse is unknown, not null -- older
    versions wrote DOUBLE bounds with digits10+1 precision, so DBL_MAX reads
    back as infinity and is rejected. Mark such a zone map pass_all so it
    prunes nothing, instead of failing every scan that loads it.
---
 .../data_type_serde/data_type_nullable_serde.cpp   |  9 ++--
 be/src/storage/index/zone_map/zone_map_index.cpp   | 14 +++++--
 be/src/storage/segment/column_reader.cpp           | 39 +++++++++--------
 be/src/storage/segment/segment.cpp                 | 49 ++++++++++++++++++++--
 .../data_type_serde_number_test.cpp                | 14 +++++++
 be/test/storage/segment/zone_map_index_test.cpp    | 38 +++++++++++++++++
 6 files changed, 132 insertions(+), 31 deletions(-)

diff --git a/be/src/core/data_type_serde/data_type_nullable_serde.cpp 
b/be/src/core/data_type_serde/data_type_nullable_serde.cpp
index c06bcf0af82..1a2befda5ab 100644
--- a/be/src/core/data_type_serde/data_type_nullable_serde.cpp
+++ b/be/src/core/data_type_serde/data_type_nullable_serde.cpp
@@ -585,13 +585,10 @@ Status DataTypeNullableSerDe::from_string(StringRef& str, 
IColumn& column,
     return Status::OK();
 }
 
+// A zone map bound is never a null Field -- nullness lives in 
has_null/has_not_null, not in
+// min/max -- so there is no null to fall back to here: defer to the nested 
serde as is.
 Status DataTypeNullableSerDe::from_zonemap_string(const std::string& str, 
Field& field) const {
-    if (!nested_serde->from_zonemap_string(str, field).ok()) {
-        // fill null if fail
-        field = Field();
-        return Status::OK();
-    }
-    return Status::OK();
+    return nested_serde->from_zonemap_string(str, field);
 }
 
 Status DataTypeNullableSerDe::from_fe_string(const std::string& str, Field& 
field) const {
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 23e2e5a4a0f..70eb29987b1 100644
--- a/be/src/storage/index/zone_map/zone_map_index.cpp
+++ b/be/src/storage/index/zone_map/zone_map_index.cpp
@@ -54,6 +54,14 @@ Status ZoneMap::from_proto(const ZoneMapPB& zone_map, const 
DataTypePtr& data_ty
     zone_map_info.has_positive_inf = zone_map.has_positive_inf();
     zone_map_info.has_nan = zone_map.has_nan();
 
+    // A bound that fails to parse makes the zone map invalid: mark it 
pass_all so it prunes
+    // nothing, instead of failing the scan that loads it.
+    auto parse_bound = [&](const std::string& bound, Field& value) {
+        if (!data_type->get_serde()->from_zonemap_string(bound, value).ok()) {
+            zone_map_info.pass_all = true;
+        }
+    };
+
     auto field_type = data_type->get_storage_field_type();
     // min value and max value are valid if has_not_null is true
     if (zone_map.has_not_null()) {
@@ -69,8 +77,7 @@ Status ZoneMap::from_proto(const ZoneMapPB& zone_map, const 
DataTypePtr& data_ty
             }
         } else {
             if (!zone_map_info.pass_all) {
-                RETURN_IF_ERROR(data_type->get_serde()->from_zonemap_string(
-                        zone_map.min(), zone_map_info.min_value));
+                parse_bound(zone_map.min(), zone_map_info.min_value);
             }
         }
 
@@ -96,8 +103,7 @@ Status ZoneMap::from_proto(const ZoneMapPB& zone_map, const 
DataTypePtr& data_ty
             }
         } else {
             if (!zone_map_info.pass_all) {
-                RETURN_IF_ERROR(data_type->get_serde()->from_zonemap_string(
-                        zone_map.max(), zone_map_info.max_value));
+                parse_bound(zone_map.max(), zone_map_info.max_value);
             }
         }
     }
diff --git a/be/src/storage/segment/column_reader.cpp 
b/be/src/storage/segment/column_reader.cpp
index 2dcb761940b..d11a3798012 100644
--- a/be/src/storage/segment/column_reader.cpp
+++ b/be/src/storage/segment/column_reader.cpp
@@ -357,7 +357,8 @@ void ColumnReader::check_data_by_zone_map_for_test(const 
MutableColumnPtr& dst)
     ZoneMap zone_map;
     THROW_IF_ERROR(ZoneMap::from_proto(*_segment_zone_map, _data_type, 
zone_map));
 
-    if (zone_map.has_null) {
+    // pass_all leaves min/max unset, so there is nothing to check the data 
against.
+    if (zone_map.has_null || zone_map.pass_all) {
         return;
     }
 
@@ -477,6 +478,9 @@ Status ColumnReader::next_batch_of_zone_map(size_t* n, 
MutableColumnPtr& dst) co
     // TODO: this work to get min/max value seems should only do once
     ZoneMap zone_map;
     RETURN_IF_ERROR(ZoneMap::from_proto(*_segment_zone_map, _data_type, 
zone_map));
+    // Segment::new_iterator does not build this iterator on an invalid zone 
map, whose min/max
+    // are unset and would be reported below as if they were data.
+    DORIS_CHECK(!zone_map.pass_all);
 
     dst->reserve(*n);
     if (!zone_map.has_not_null) {
@@ -559,26 +563,25 @@ Status ColumnReader::_get_filtered_pages(
     const std::vector<ZoneMapPB>& zone_maps = 
_zone_map_index->page_zone_maps();
     size_t page_size = _zone_map_index->num_pages();
     for (size_t i = 0; i < page_size; ++i) {
-        if (zone_maps[i].pass_all()) {
+        segment_v2::ZoneMap zone_map;
+        RETURN_IF_ERROR(ZoneMap::from_proto(zone_maps[i], _data_type, 
zone_map));
+        // from_proto also sets pass_all when the zone map it parsed is 
invalid.
+        if (zone_map.pass_all) {
             page_indexes->push_back(cast_set<uint32_t>(i));
-        } else {
-            segment_v2::ZoneMap zone_map;
-            RETURN_IF_ERROR(ZoneMap::from_proto(zone_maps[i], _data_type, 
zone_map));
-            if (_zone_map_match_condition(zone_map, col_predicates)) {
-                bool should_read = true;
-                if (delete_predicates != nullptr) {
-                    for (auto del_pred : *delete_predicates) {
-                        // TODO: Both `min_value` and `max_value` should be 0 
or neither should be 0.
-                        //  So nullable only need to judge once.
-                        if (del_pred->evaluate_del(zone_map)) {
-                            should_read = false;
-                            break;
-                        }
+        } else if (_zone_map_match_condition(zone_map, col_predicates)) {
+            bool should_read = true;
+            if (delete_predicates != nullptr) {
+                for (auto del_pred : *delete_predicates) {
+                    // TODO: Both `min_value` and `max_value` should be 0 or 
neither should be 0.
+                    //  So nullable only need to judge once.
+                    if (del_pred->evaluate_del(zone_map)) {
+                        should_read = false;
+                        break;
                     }
                 }
-                if (should_read) {
-                    page_indexes->push_back(cast_set<uint32_t>(i));
-                }
+            }
+            if (should_read) {
+                page_indexes->push_back(cast_set<uint32_t>(i));
             }
         }
     }
diff --git a/be/src/storage/segment/segment.cpp 
b/be/src/storage/segment/segment.cpp
index bf61ab34dd0..a637b060961 100644
--- a/be/src/storage/segment/segment.cpp
+++ b/be/src/storage/segment/segment.cpp
@@ -143,6 +143,40 @@ Status build_segment_zonemap_context(Segment* segment, 
const ReadSchema& schema,
     return Status::OK();
 }
 
+// The statistics iterator answers pushed-down aggregates from the segment 
zone maps alone. An
+// invalid zone map has no min/max to answer with, so the caller has to read 
the data instead.
+Status segment_zone_maps_can_answer_agg(Segment* segment, const ReadSchema& 
schema,
+                                        const StorageReadOptions& 
read_options, bool* usable) {
+    *usable = true;
+    for (size_t ordinal = 0; ordinal < schema.num_block_columns(); ++ordinal) {
+        // The commit-tso column is only served correctly once its reader is 
created with the
+        // rowset's commit_tso as a const value. Creating it here without one 
would cache a reader
+        // that hands every later read the on-disk placeholder instead.
+        if (static_cast<int32_t>(ordinal) == schema.commit_tso_ordinal()) {
+            continue;
+        }
+        std::shared_ptr<ColumnReader> reader;
+        Status st = segment->get_column_reader(*schema.column(ordinal), 
&reader, read_options.stats,
+                                               &read_options.io_ctx);
+        if (st.is<ErrorCode::NOT_FOUND>()) {
+            continue;
+        }
+        RETURN_IF_ERROR(st);
+        // Columns without a zone map keep the existing behaviour: the 
statistics iterator reports
+        // the missing zone map itself.
+        if (reader == nullptr || !reader->has_zone_map()) {
+            continue;
+        }
+        ZoneMap zone_map;
+        RETURN_IF_ERROR(reader->get_segment_zone_map(&zone_map));
+        if (zone_map.pass_all) {
+            *usable = false;
+            return Status::OK();
+        }
+    }
+    return Status::OK();
+}
+
 void fill_missing_decimal_precision(const TabletColumn& column, ColumnMetaPB* 
meta) {
     auto meta_type = static_cast<FieldType>(meta->type());
     if (meta_type != column.type()) {
@@ -469,9 +503,18 @@ Status Segment::new_iterator(ReadSchemaSPtr schema, const 
StorageReadOptions& re
         RETURN_IF_ERROR(load_index(read_options.stats, &read_options.io_ctx));
     }
 
-    if (read_options.delete_condition_predicates->num_of_column_predicate() == 
0 &&
-        read_options.push_down_agg_type_opt != TPushAggOp::NONE &&
-        read_options.push_down_agg_type_opt != TPushAggOp::COUNT_ON_INDEX) {
+    bool use_statistics_iterator =
+            
read_options.delete_condition_predicates->num_of_column_predicate() == 0 &&
+            read_options.push_down_agg_type_opt != TPushAggOp::NONE &&
+            read_options.push_down_agg_type_opt != TPushAggOp::COUNT_ON_INDEX;
+    // COUNT only fills defaults, every other pushed-down aggregate reads 
min/max out of the
+    // segment zone maps.
+    if (use_statistics_iterator && read_options.push_down_agg_type_opt != 
TPushAggOp::COUNT) {
+        bool usable = false;
+        RETURN_IF_ERROR(segment_zone_maps_can_answer_agg(this, *schema, 
read_options, &usable));
+        use_statistics_iterator = usable;
+    }
+    if (use_statistics_iterator) {
         iter->reset(new_vstatistics_iterator(this->shared_from_this(), 
*schema));
     } else {
         *iter = std::make_unique<SegmentIterator>(this->shared_from_this(), 
schema);
diff --git a/be/test/core/data_type_serde/data_type_serde_number_test.cpp 
b/be/test/core/data_type_serde/data_type_serde_number_test.cpp
index 41724fd3b33..e9256791f59 100644
--- a/be/test/core/data_type_serde/data_type_serde_number_test.cpp
+++ b/be/test/core/data_type_serde/data_type_serde_number_test.cpp
@@ -39,6 +39,7 @@
 #include "core/data_type_serde/data_type_date_or_datetime_serde.h"
 #include "core/data_type_serde/data_type_datetimev2_serde.h"
 #include "core/data_type_serde/data_type_datev2_serde.h"
+#include "core/data_type_serde/data_type_nullable_serde.h"
 #include "core/field.h"
 #include "core/types.h"
 #include "testutil/test_util.h"
@@ -793,4 +794,17 @@ TEST_F(DataTypeNumberSerDeTest, 
OlapStringRoundTripFloatExtremes) {
     check_float(0.0f);
 }
 
+TEST_F(DataTypeNumberSerDeTest, NullableZonemapStringPropagatesParseError) {
+    DataTypeNullableSerDe nullable_serde(serde_float64);
+
+    Field field;
+    EXPECT_FALSE(nullable_serde.from_zonemap_string("1.797693134862316e+308", 
field).ok());
+    EXPECT_FALSE(nullable_serde.from_zonemap_string("-1.797693134862316e+308", 
field).ok());
+    EXPECT_FALSE(nullable_serde.from_zonemap_string("not-a-double", 
field).ok());
+
+    Field parsed;
+    ASSERT_TRUE(nullable_serde.from_zonemap_string("1.7976931348623157e+308", 
parsed).ok());
+    EXPECT_EQ(parsed.get<TYPE_DOUBLE>(), std::numeric_limits<double>::max());
+}
+
 } // namespace doris
diff --git a/be/test/storage/segment/zone_map_index_test.cpp 
b/be/test/storage/segment/zone_map_index_test.cpp
index db4b692b1f1..66ad4129f66 100644
--- a/be/test/storage/segment/zone_map_index_test.cpp
+++ b/be/test/storage/segment/zone_map_index_test.cpp
@@ -943,6 +943,44 @@ TEST_F(ColumnZoneMapTest, DoubleFiniteExtremesRoundTrip) {
     EXPECT_EQ(pzm.max_value.get<TYPE_DOUBLE>(), 
std::numeric_limits<double>::max());
 }
 
+TEST_F(ColumnZoneMapTest, LegacyUnparsableDoubleBoundDegradesToPassAll) {
+    auto make_zone_map = [](const std::string& min, const std::string& max) {
+        ZoneMapPB pb;
+        pb.set_min(min);
+        pb.set_max(max);
+        pb.set_has_null(false);
+        pb.set_has_not_null(true);
+        pb.set_pass_all(false);
+        return pb;
+    };
+    // 16g renderings of ±DBL_MAX, both of which read back as ∓inf.
+    const std::string legacy_min = "-1.797693134862316e+308";
+    const std::string legacy_max = "1.797693134862316e+308";
+    const std::string exact_min = "-1.7976931348623157e+308";
+    const std::string exact_max = "1.7976931348623157e+308";
+
+    for (bool nullable : {false, true}) {
+        auto data_type = 
DataTypeFactory::instance().create_data_type(TYPE_DOUBLE, nullable);
+
+        for (const auto& pb :
+             {make_zone_map(legacy_min, legacy_max), make_zone_map(legacy_min, 
exact_max),
+              make_zone_map(exact_min, legacy_max)}) {
+            ZoneMap zm;
+            ASSERT_TRUE(ZoneMap::from_proto(pb, data_type, zm).ok()) << 
"nullable=" << nullable;
+            EXPECT_TRUE(zm.pass_all) << "nullable=" << nullable << ", min='" 
<< pb.min()
+                                     << "', max='" << pb.max() << "'";
+            EXPECT_TRUE(zm.has_not_null);
+            EXPECT_FALSE(zm.has_null);
+        }
+
+        ZoneMap zm;
+        ASSERT_TRUE(ZoneMap::from_proto(make_zone_map(exact_min, exact_max), 
data_type, zm).ok());
+        EXPECT_FALSE(zm.pass_all) << "nullable=" << nullable;
+        EXPECT_EQ(zm.min_value.get<TYPE_DOUBLE>(), 
std::numeric_limits<double>::lowest());
+        EXPECT_EQ(zm.max_value.get<TYPE_DOUBLE>(), 
std::numeric_limits<double>::max());
+    }
+}
+
 TabletColumnPtr create_timestamptz_column(int32_t id, bool is_nullable) {
     auto column = std::make_shared<TabletColumn>();
     column->_unique_id = id;


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

Reply via email to