github-actions[bot] commented on code in PR #67642:
URL: https://github.com/apache/doris/pull/67642#discussion_r4003745689


##########
be/src/storage/segment/segment.cpp:
##########
@@ -169,10 +175,26 @@ Status segment_zone_maps_can_answer_agg(Segment* segment, 
const ReadSchema& sche
         }
         ZoneMap zone_map;
         RETURN_IF_ERROR(reader->get_segment_zone_map(&zone_map));
+
+        // The zone map gave up its range, so it has no min/max left to answer 
with.
         if (zone_map.pass_all) {
             *usable = false;
             return Status::OK();
         }
+
+        // Only a string bound is cut at MAX_ZONE_MAP_INDEX_SIZE, and a column 
of nothing but
+        // nulls stored no bound to look at.
+        if (!is_string_type(schema.column(ordinal)->type()) || 
!zone_map.has_not_null) {
+            continue;
+        }
+
+        // A cut bound is not a value the column holds: the min is a prefix of 
the smallest value
+        // and the max was raised past the largest one. Neither can answer 
MIN()/MAX().
+        if (zone_map.min_value.as_string_view().size() >= 
MAX_ZONE_MAP_INDEX_SIZE ||
+            zone_map.max_value.as_string_view().size() >= 
MAX_ZONE_MAP_INDEX_SIZE) {

Review Comment:
   [P1] Reject lossy embedded-NUL STRING bounds
   
   This check sees an already-deserialized Field. STRING writes preserve the 
full `ColumnString` byte length and zone maps serialize those raw bytes, but 
`from_olap_string` uses `strnlen` for all string types. Thus a short five-byte 
STRING containing an embedded NUL is shortened at that byte, remains 
`pass_all=false`, passes the `< 512` checks here, and is returned by 
`VStatisticsIterator`. Since this PR removes the default FE gate for 
`PrimitiveType.STRING`, exact user MIN/MAX now reaches that wrong-result path 
without opting in. Please preserve raw bytes for STRING/VARCHAR (trimming only 
CHAR padding), or reject such lossy bounds before selecting the statistics 
iterator, with an end-to-end embedded-NUL test.



##########
be/src/storage/segment/segment.cpp:
##########
@@ -503,16 +525,17 @@ Status Segment::new_iterator(ReadSchemaSPtr schema, const 
StorageReadOptions& re
         RETURN_IF_ERROR(load_index(read_options.stats, &read_options.io_ctx));
     }
 
+    // COUNT and MIX report the segment row count, which a delete predicate 
makes wrong whatever
+    // the zone map bounds hold, so they keep the guard below even when the 
switch is on.
+    const auto agg = read_options.push_down_agg_type_opt;
+    const bool forced = pushdown_zonemap_minmax_forced(read_options);
     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;
+            agg != TPushAggOp::NONE && agg != TPushAggOp::COUNT_ON_INDEX &&
+            (forced || 
read_options.delete_condition_predicates->num_of_column_predicate() == 0);
+    // COUNT only fills defaults, every other aggregate reads min/max out of 
the zone maps.
+    if (use_statistics_iterator && !forced && agg != TPushAggOp::COUNT) {
+        RETURN_IF_ERROR(segment_zone_maps_can_answer_agg(this, *schema, 
read_options,

Review Comment:
   [P1] Do not force unreadable `pass_all` bounds into MIN/MAX
   
   `forced` skips the validity helper for every `pass_all` reason, including 
bounds that are not safe approximations. For the new test's 512-byte value 
ending in `0xff`, the writer stores a max ending in NUL; `from_olap_string` 
applies `strnlen` before `from_proto` sets `pass_all`, so the emitted max is 
only the preceding 511-byte prefix—strictly below the real value. The test 
checks only `next_batch().ok()`, not that value. Please distinguish 
deliberately inexact-but-materialized cut/delete bounds from wrapped or 
unparsable maps, and keep the latter on the row-scan fallback.



##########
be/src/storage/index/zone_map/zone_map_index.cpp:
##########
@@ -85,6 +87,17 @@ Status ZoneMap::from_proto(const ZoneMapPB& zone_map, const 
DataTypePtr& data_ty
             parse_bound(zone_map.max(), zone_map_info.max_value);
         }
 
+        // Lower the raised byte back to what the data held, then run the 
writer's check on it.
+        // A max that came from 0xff wrapped to 0x00, and old segments still 
carry such a max.
+        if (!zone_map_info.pass_all && is_string_type(field_type) &&
+            zone_map.max().size() == MAX_ZONE_MAP_INDEX_SIZE) {
+            std::string max_before_raise = zone_map.max();
+            max_before_raise.back() -= 1;
+            if (!validate_utf8(max_before_raise.data(), 
max_before_raise.size())) {
+                zone_map_info.pass_all = true;

Review Comment:
   [P2] Detect byte wrap without rejecting split UTF-8 prefixes
   
   Invalid UTF-8 after decrementing the last byte is not specific to the `0xff` 
wrap. A valid long value whose 512-byte cut ends midway through a multibyte 
character reconstructs to invalid UTF-8, while the stored non-wrapping raised 
byte is still a sound upper bound under Doris's bytewise comparison. Marking it 
`pass_all` makes segment and page pruning retain every such zone (and the added 
test explicitly pins this safe split as unusable). Please detect the actual 
wrap—for example, the raised stored byte becoming `0x00`, or explicit writer 
metadata—and add a pruning test for a split multibyte prefix.



##########
fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java:
##########
@@ -2380,10 +2380,13 @@ public boolean isEnableHboNonStrictMatchingMode() {
             + "pushdown minmax on unique table.")
     public boolean enablePushDownMinMaxOnUnique = false;
 
-    // Whether enable push down string type minmax to scan node.
-    @VarAttrDef.VarAttr(name = ENABLE_PUSHDOWN_STRING_MINMAX, needForward = 
true, description = "Set whether to enable "
-            + "push down string type minmax.")
-    public boolean enablePushDownStringMinMax = false;
+    // Whether to force MIN/MAX onto the zone map when its bound is not a 
value the data holds now:
+    // a cut string bound, or one covering rows a delete predicate removed. 
The alias is the old
+    // name, from when this only governed string bounds.
+    @VarAttrDef.VarAttr(name = FORCE_PUSHDOWN_ZONEMAP_MINMAX, alias = 
{"enable_pushdown_string_minmax"},
+            needForward = true, description = "Set whether to force a pushed 
down minmax onto the zone map when its "

Review Comment:
   [P1] Include the expanded force flag in result-cache identity
   
   This flag now changes execution results for numeric and short-string MIN/MAX 
too: with a delete predicate, `true` reads zone-map extrema that may belong to 
deleted rows, while `false` falls back to live rows. Yet the annotation omits 
`affectQueryResultInExecution`, so both SQL-cache variable checks and the 
fragment query-cache digest ignore it. A result cached with force enabled can 
therefore be replayed after the session disables forcing, despite the table 
version and plan being unchanged. Please mark this as 
execution-result-affecting and cover cache population under one value followed 
by lookup under the other.



##########
fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java:
##########
@@ -794,7 +794,7 @@ public String toString() {
 
     public static final String KEEP_CARRIAGE_RETURN = "keep_carriage_return";
 
-    public static final String ENABLE_PUSHDOWN_STRING_MINMAX = 
"enable_pushdown_string_minmax";
+    public static final String FORCE_PUSHDOWN_ZONEMAP_MINMAX = 
"force_pushdown_zonemap_minmax";
 

Review Comment:
   [P2] Preserve the old name for persistence and forwarding
   
   Making `enable_pushdown_string_minmax` only an alias does not migrate stored 
or forwarded state. `SessionVariable.toJson`/`readFromJson` and the 
forwarded-variable map use only `VarAttr.name()`; aliases are registered only 
for SQL lookup. Consequently, an FE image containing an old global value of 
`true` reloads this renamed field as its default `false`, and old/new FEs 
ignore each other's forwarding key. Please retain the old name as the 
persisted/forwarded primary (with the new spelling as an alias), or make both 
replay paths alias-aware and cover old-image and mixed-FE cases.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to