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

mrhhsg 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 49f122698de [fix](aggregate) Keep embedded NUL bytes in the JSON keys 
emitted by topn() (#68243)
49f122698de is described below

commit 49f122698deff5b0a31f8033afd5ac8fb6f38cf7
Author: Jerry Hu <[email protected]>
AuthorDate: Thu Oct 8 15:14:27 2026 +0800

    [fix](aggregate) Keep embedded NUL bytes in the JSON keys emitted by topn() 
(#68243)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Problem Summary: A Doris STRING value is binary, so it may contain a NUL
    byte. `topn()` keeps the whole value in its state, but writes the result
    JSON with `writer.Key(element.second.c_str())`, which has no length
    argument and therefore stops at the first NUL. `topn(concat('a',
    unhex('00'), 'b'), 1)` returns `{"a":1}` instead of the real key, and
    two values that only differ after the NUL collapse into the same
    duplicated key, e.g. `{"a":1,"a":1}`, so the caller cannot recover the
    top-N values at all.
    
    Pass the key together with its length so rapidjson escapes the NUL as
    `\u0000` and emits the whole key.
    
    Before:
    
    ```
    mysql> SELECT topn(s, 2) FROM (SELECT concat('a', unhex('00'), 'b') AS s
        ->                        UNION ALL SELECT concat('a', unhex('00'), 
'c')) t;
    +---------------------+
    | {"a":1,"a":1}       |
    +---------------------+
    ```
    
    After:
    
    ```
    +-------------------------------+
    | {"a\u0000b":1,"a\u0000c":1}   |
    +-------------------------------+
    ```
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test:
    - Unit Test: Yes, `AggTopNTest.test_string_key_with_embedded_nul` covers
    a string key with an embedded NUL and two keys that only differ after
    it.
    - Regression test: Yes, `test_topn_embedded_nul` checks `topn()` on such
    keys and keeps `topn_array()` as a reference.
    - Behavior changed: Yes, `topn()` now emits the complete key with the
    NUL escaped as `\u0000` instead of a key truncated at the NUL.
    - Does this need documentation: No
    
    https://claude.ai/code/session_01RZ36Pij3o8fGnYYg33PKnq
---
 be/src/exprs/aggregate/aggregate_function_topn.h   |  3 +-
 be/test/exprs/aggregate/agg_topn_test.cpp          | 60 ++++++++++++++++++++--
 .../aggregate_functions/test_topn_embedded_nul.out | 10 ++++
 .../test_topn_embedded_nul.groovy                  | 39 ++++++++++++++
 4 files changed, 108 insertions(+), 4 deletions(-)

diff --git a/be/src/exprs/aggregate/aggregate_function_topn.h 
b/be/src/exprs/aggregate/aggregate_function_topn.h
index 062b0e8eefe..82216314cbc 100644
--- a/be/src/exprs/aggregate/aggregate_function_topn.h
+++ b/be/src/exprs/aggregate/aggregate_function_topn.h
@@ -30,6 +30,7 @@
 #include <utility>
 #include <vector>
 
+#include "common/cast_set.h"
 #include "common/exception.h"
 #include "core/assert_cast.h"
 #include "core/column/column.h"
@@ -184,7 +185,7 @@ struct AggregateFunctionTopNData {
         writer.StartObject();
         for (int i = 0; i < std::min((int)counter_vector.size(), top_num); 
i++) {
             const auto& element = counter_vector[i];
-            writer.Key(element.second.c_str());
+            writer.Key(element.second.data(), 
cast_set<rapidjson::SizeType>(element.second.size()));
             writer.Uint64(element.first);
         }
         writer.EndObject();
diff --git a/be/test/exprs/aggregate/agg_topn_test.cpp 
b/be/test/exprs/aggregate/agg_topn_test.cpp
index 324bb7272d9..f8c5c758459 100644
--- a/be/test/exprs/aggregate/agg_topn_test.cpp
+++ b/be/test/exprs/aggregate/agg_topn_test.cpp
@@ -18,15 +18,71 @@
 #include <gtest/gtest.h>
 
 #include <cstdint>
+#include <memory>
 #include <string>
 
+#include "core/arena.h"
 #include "core/column/column_string.h"
 #include "core/column/column_vector.h"
+#include "core/data_type/data_type.h"
+#include "core/data_type/data_type_number.h"
+#include "core/data_type/data_type_string.h"
 #include "core/string_buffer.hpp"
+#include "exprs/aggregate/aggregate_function.h"
+#include "exprs/aggregate/aggregate_function_simple_factory.h"
 #include "exprs/aggregate/aggregate_function_topn.h"
 
 namespace doris {
-namespace {
+
+void register_aggregate_function_topn(AggregateFunctionSimpleFactory& factory);
+
+class AggTopNTest : public testing::Test {
+public:
+    void SetUp() override {
+        AggregateFunctionSimpleFactory factory = 
AggregateFunctionSimpleFactory::instance();
+        register_aggregate_function_topn(factory);
+    }
+
+protected:
+    Arena _agg_arena_pool;
+};
+
+// String keys are binary, so an embedded NUL must not terminate the JSON key.
+TEST_F(AggTopNTest, test_string_key_with_embedded_nul) {
+    DataTypes data_types = {std::make_shared<DataTypeString>(), 
std::make_shared<DataTypeInt32>()};
+    auto agg_function =
+            AggregateFunctionSimpleFactory::instance().get("topn", data_types, 
nullptr, false, -1);
+    ASSERT_NE(agg_function, nullptr);
+
+    const std::string frequent("a\0b", 3);
+    const std::string rare("a\0c", 3);
+
+    auto value_column = ColumnString::create();
+    value_column->insert_data(frequent.data(), frequent.size());
+    value_column->insert_data(frequent.data(), frequent.size());
+    value_column->insert_data(rare.data(), rare.size());
+
+    auto top_num_column = ColumnInt32::create();
+    for (size_t i = 0; i < value_column->size(); ++i) {
+        top_num_column->insert_value(2);
+    }
+
+    const IColumn* columns[2] = {value_column.get(), top_num_column.get()};
+
+    std::unique_ptr<char[]> memory(new char[agg_function->size_of_data()]);
+    AggregateDataPtr place = memory.get();
+    agg_function->create(place);
+    for (size_t i = 0; i < value_column->size(); ++i) {
+        agg_function->add(place, columns, i, _agg_arena_pool);
+    }
+
+    auto result_column = ColumnString::create();
+    agg_function->insert_result_into(place, *result_column);
+    agg_function->destroy(place);
+
+    ASSERT_EQ(result_column->size(), 1);
+    EXPECT_EQ(result_column->get_data_at(0).to_string(), 
R"({"a\u0000b":2,"a\u0000c":1})");
+}
 
 template <PrimitiveType T>
 AggregateFunctionTopNData<T> round_trip(const AggregateFunctionTopNData<T>& 
state) {
@@ -120,6 +176,4 @@ TEST(AggregateFunctionTopNTest, 
PositiveRateStillLimitsSerializedCandidates) {
     state.set_paramenters(1);
     EXPECT_EQ(round_trip(state).counter_map, state.counter_map);
 }
-
-} // namespace
 } // namespace doris
diff --git 
a/regression-test/data/query_p0/sql_functions/aggregate_functions/test_topn_embedded_nul.out
 
b/regression-test/data/query_p0/sql_functions/aggregate_functions/test_topn_embedded_nul.out
new file mode 100644
index 00000000000..0c5edd72fcf
--- /dev/null
+++ 
b/regression-test/data/query_p0/sql_functions/aggregate_functions/test_topn_embedded_nul.out
@@ -0,0 +1,10 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !topn_single_key --
+{"a\\u0000b":1}
+
+-- !topn_distinct_keys --
+{"a\\u0000b":2,"a\\u0000c":1}
+
+-- !topn_array_element_hex --
+610062
+
diff --git 
a/regression-test/suites/query_p0/sql_functions/aggregate_functions/test_topn_embedded_nul.groovy
 
b/regression-test/suites/query_p0/sql_functions/aggregate_functions/test_topn_embedded_nul.groovy
new file mode 100644
index 00000000000..19bd0628404
--- /dev/null
+++ 
b/regression-test/suites/query_p0/sql_functions/aggregate_functions/test_topn_embedded_nul.groovy
@@ -0,0 +1,39 @@
+// 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.
+
+suite("test_topn_embedded_nul") {
+    // A STRING value is binary, so a NUL byte inside a key must not truncate
+    // the JSON key that topn() emits, and two keys differing after the NUL
+    // must stay distinct in the result.
+    qt_topn_single_key """
+        SELECT topn(s, 1) FROM (SELECT concat('a', unhex('00'), 'b') AS s) t
+    """
+
+    qt_topn_distinct_keys """
+        SELECT topn(s, 2) FROM (
+            SELECT concat('a', unhex('00'), 'b') AS s
+            UNION ALL SELECT concat('a', unhex('00'), 'b')
+            UNION ALL SELECT concat('a', unhex('00'), 'c')
+        ) t
+    """
+
+    qt_topn_array_element_hex """
+        SELECT hex(element_at(topn_array(s, 1), 1)) FROM (
+            SELECT concat('a', unhex('00'), 'b') AS s
+        ) t
+    """
+}


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

Reply via email to