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


##########
be/src/core/data_type_serde/data_type_string_serde.cpp:
##########
@@ -847,12 +847,15 @@ Status 
DataTypeStringSerDeBase<ColumnType>::from_string(StringRef& str, IColumn&
 template <typename ColumnType>
 Status DataTypeStringSerDeBase<ColumnType>::from_olap_string(const 
std::string& str, Field& field,
                                                              const 
FormatOptions& options) const {
-    // CHAR(N) writes through OlapColumnDataConvertorChar are zero-padded to
-    // the declared schema length, so the serialized OLAP string carries
-    // trailing '\0' bytes. strnlen() drops that padding to surface the
-    // logical character content in the Field. VARCHAR / STRING never write
-    // trailing '\0' through this path, so strnlen is a no-op for them.
-    size_t len = strnlen(str.data(), str.size());
+    // CHAR(N) is zero-padded to the declared schema length before it is 
written, so its
+    // stored bytes carry trailing '\0' and stop at the first one. The page 
read path cuts
+    // CHAR values the same way (see BinaryPlainPageCharStripPreDecoder), so a 
bound built
+    // like this stays comparable with the rows it describes.
+    //
+    // VARCHAR and STRING keep every byte they were given, '\0' included. 
Cutting such a
+    // value at an embedded '\0' would give a bound the data never held, and a 
zone map
+    // built from it prunes rows that match.
+    size_t len = _type == TYPE_CHAR ? strnlen(str.data(), str.size()) : 
str.size();

Review Comment:
   [P1] Handle saturated max prefixes before trusting all bytes
   
   Preserving `str.size()` fixes short embedded-NUL bounds, but a 513+ byte 
STRING/VARCHAR can still get a max smaller than its rows. 
`_update_page_zonemap()` truncates to 512 bytes and 
`modify_index_before_flush()` increments byte 512; Doris accepts arbitrary 
bytes (including `UNHEX('FF')`), so `0xff` wraps to `0x00`. For a row `61 00 + 
509*78 + ff 7a` and a `GT` bound with the same first 511 bytes followed by 
`01`, the stored max ends in `00` and both segment/page pruning reject the row 
even though it matches. Please use a carry-aware bytewise prefix successor (or 
`pass_all` when none exists), conservatively handle already-persisted wrapped 
bounds, and test >512-byte embedded-NUL/`ff` values at both map levels.



##########
regression-test/suites/datatype_p0/string/test_string_embedded_nul_zonemap.groovy:
##########
@@ -0,0 +1,79 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// A STRING / VARCHAR value may hold a 0x00 byte. The zone map bound used to be
+// parsed back with C string semantics and stopped at that byte, so a 
comparison pushed into
+// the scan read a bound the data never held and dropped pages that hold 
matching rows.
+//
+// Each table below holds 32 copies of one value, so the segment min and max 
are that value.
+// The SUM(...) row is the per-row answer, which never goes through the zone 
map; the three
+// COUNT(*) rows run the same predicates through the scan and have to agree 
with it.
+suite("test_string_embedded_nul_zonemap") {
+    def load_one_value = { String table, String type, String hex ->
+        sql "DROP TABLE IF EXISTS ${table}"
+        sql """
+            CREATE TABLE ${table} (
+                id INT,
+                s ${type}
+            )
+            DUPLICATE KEY(id)
+            DISTRIBUTED BY HASH(id) BUCKETS 1
+            PROPERTIES (
+                "replication_num" = "1",
+                "disable_auto_compaction" = "true"
+            )
+        """
+        def rows = (1..32).collect { "(${it}, UNHEX('${hex}'))" }.join(", ")
+        sql "INSERT INTO ${table} VALUES ${rows}"
+    }
+
+    // 'a' 0x00 'b' in a STRING column -- the 0x00 sits in the middle.
+    load_one_value("nul_inside_string", "STRING", "610062")
+    qt_inside_string_data "SELECT COUNT(*), MIN(LENGTH(s)), MAX(LENGTH(s)), 
MIN(HEX(s)), MAX(HEX(s)) FROM nul_inside_string"
+    qt_inside_string_oracle "SELECT SUM(CAST(s > 'a' AS INT)), SUM(CAST(s != 
'a' AS INT)), SUM(CAST(s <= 'a' AS INT)) FROM nul_inside_string"
+    qt_inside_string_gt "SELECT COUNT(*) FROM nul_inside_string WHERE s > 'a'"
+    qt_inside_string_ne "SELECT COUNT(*) FROM nul_inside_string WHERE s != 'a'"
+    qt_inside_string_le "SELECT COUNT(*) FROM nul_inside_string WHERE s <= 'a'"
+    qt_inside_string_minmax "SELECT HEX(MIN(s)), HEX(MAX(s)) FROM 
nul_inside_string"
+
+    // The same value in a VARCHAR column, which shares the zone map bound 
path.
+    load_one_value("nul_inside_varchar", "VARCHAR(20)", "610062")
+    qt_inside_varchar_data "SELECT COUNT(*), MIN(LENGTH(s)), MAX(LENGTH(s)), 
MIN(HEX(s)), MAX(HEX(s)) FROM nul_inside_varchar"
+    qt_inside_varchar_oracle "SELECT SUM(CAST(s > 'a' AS INT)), SUM(CAST(s != 
'a' AS INT)), SUM(CAST(s <= 'a' AS INT)) FROM nul_inside_varchar"
+    qt_inside_varchar_gt "SELECT COUNT(*) FROM nul_inside_varchar WHERE s > 
'a'"
+    qt_inside_varchar_ne "SELECT COUNT(*) FROM nul_inside_varchar WHERE s != 
'a'"
+    qt_inside_varchar_le "SELECT COUNT(*) FROM nul_inside_varchar WHERE s <= 
'a'"
+    qt_inside_varchar_minmax "SELECT HEX(MIN(s)), HEX(MAX(s)) FROM 
nul_inside_varchar"

Review Comment:
   [P1] Keep exact 512-byte maxima out of synthetic statistics
   
   This three-byte VARCHAR case misses a wrong-result boundary in the same 
path. `_update_page_zonemap()` retains an exact 512-byte max, but 
`modify_index_before_flush()` increments every retained max of size 512 even 
when nothing was truncated. `VARCHAR(512)` is still eligible for default 
MIN/MAX pushdown, and `next_batch_of_zone_map()` emits that widened bound as 
data, so a table whose sole value is `61 00 + 510*78` can return a `MAX(s)` 
ending in `79` although the stored value ends in `78`. Please track whether 
widening was actually needed, keep exact maxima exact, make aggregate pushdown 
fall back for approximate/legacy bounds, and add an exact-512 embedded-NUL 
regression here.



-- 
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