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

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

commit 7c330d2c3611d5125a2ea838eab82fc58e7ab0e7
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Oct 15 09:30:27 2026 +0800

    branch-4.1: [fix](be) Preserve the sample triggering QuantileState digest 
conversion #69037 (#69049)
    
    Cherry-picked from #69037
    
    ---------
    
    Co-authored-by: TengJianPing <[email protected]>
---
 be/src/core/value/quantile_state.cpp               |  2 +-
 be/src/util/tdigest.h                              |  3 +-
 be/test/core/value/quantile_state_test.cpp         | 60 +++++++++++++++++++
 be/test/util/tdigest_test.cpp                      | 29 +++++++++
 .../test_aggregate_all_functions.out               |  2 +-
 .../test_aggregate_all_functions.out               |  2 +-
 .../test_quantile_state_explicit_boundary.out      | 22 +++++++
 .../test_quantile_state_explicit_boundary.groovy   | 70 ++++++++++++++++++++++
 8 files changed, 186 insertions(+), 4 deletions(-)

diff --git a/be/src/core/value/quantile_state.cpp 
b/be/src/core/value/quantile_state.cpp
index 9ca0f0a0ca8..47df852e2ad 100644
--- a/be/src/core/value/quantile_state.cpp
+++ b/be/src/core/value/quantile_state.cpp
@@ -411,10 +411,10 @@ void QuantileState::add_value(const double& value) {
             for (int i = 0; i < _explicit_data.size(); i++) {
                 _tdigest_ptr->digest.add((float)_explicit_data[i]);
             }
+            _tdigest_ptr->digest.add((float)value);
             _explicit_data.clear();
             _explicit_data.shrink_to_fit();
             _type = TDIGEST;
-
         } else {
             _explicit_data.emplace_back(value);
         }
diff --git a/be/src/util/tdigest.h b/be/src/util/tdigest.h
index 158dc748ce9..a37a606db01 100644
--- a/be/src/util/tdigest.h
+++ b/be/src/util/tdigest.h
@@ -544,7 +544,8 @@ private:
 
     Value _min = std::numeric_limits<Value>::max();
 
-    Value _max = std::numeric_limits<Value>::min();
+    // min() is the smallest positive value, so use lowest() for non-positive 
inputs.
+    Value _max = std::numeric_limits<Value>::lowest();
 
     Index _max_processed;
 
diff --git a/be/test/core/value/quantile_state_test.cpp 
b/be/test/core/value/quantile_state_test.cpp
index c025294caa3..e557fe2ce65 100644
--- a/be/test/core/value/quantile_state_test.cpp
+++ b/be/test/core/value/quantile_state_test.cpp
@@ -149,6 +149,66 @@ static long serialized_digest_weight(const QuantileState& 
state) {
     return digest.totalWeight();
 }
 
+static void expect_digest_samples(const QuantileState& state, int count, 
double minimum,
+                                  double maximum) {
+    ASSERT_EQ(TDIGEST, state._type);
+    EXPECT_EQ(count, serialized_digest_weight(state));
+    EXPECT_EQ(minimum, state.get_value_by_percentile(0));
+    EXPECT_EQ(maximum, state.get_value_by_percentile(1));
+}
+
+TEST(QuantileStateTest, AddValueRetainsSampleAtExplicitLimit) {
+    for (float compression : {QUANTILE_STATE_COMPRESSION_MIN, 
QUANTILE_STATE_COMPRESSION_MAX}) {
+        for (double value : {-100.0, 100.0}) {
+            QuantileState state(compression);
+            for (int i = 0; i < QUANTILE_STATE_EXPLICIT_NUM - 1; ++i) {
+                state.add_value(0);
+            }
+            ASSERT_EQ(EXPLICIT, state._type);
+            EXPECT_EQ(QUANTILE_STATE_EXPLICIT_NUM - 1, 
state._explicit_data.size());
+            state.add_value(0);
+            ASSERT_EQ(EXPLICIT, state._type);
+            EXPECT_EQ(QUANTILE_STATE_EXPLICIT_NUM, 
state._explicit_data.size());
+
+            state.add_value(value);
+            EXPECT_TRUE(state._explicit_data.empty());
+            expect_digest_samples(state, QUANTILE_STATE_EXPLICIT_NUM + 1, 
std::min(0.0, value),
+                                  std::max(0.0, value));
+
+            state.add_value(0);
+            expect_digest_samples(state, QUANTILE_STATE_EXPLICIT_NUM + 2, 
std::min(0.0, value),
+                                  std::max(0.0, value));
+        }
+    }
+}
+
+TEST(QuantileStateTest, MergeSingletonAndExplicitRetainsBoundarySample) {
+    for (double value : {-100.0, 100.0}) {
+        auto explicit_state = constant_state(0, QUANTILE_STATE_EXPLICIT_NUM);
+        auto singleton = constant_state(value, 1);
+
+        auto explicit_first = explicit_state;
+        explicit_first.merge(singleton);
+        auto singleton_first = singleton;
+        singleton_first.merge(explicit_state);
+        expect_digest_samples(explicit_first, QUANTILE_STATE_EXPLICIT_NUM + 1, 
std::min(0.0, value),
+                              std::max(0.0, value));
+        expect_digest_samples(singleton_first, QUANTILE_STATE_EXPLICIT_NUM + 1,
+                              std::min(0.0, value), std::max(0.0, value));
+        EXPECT_EQ(QUANTILE_STATE_EXPLICIT_NUM, 
explicit_state._explicit_data.size());
+        EXPECT_EQ(0, explicit_state.get_value_by_percentile(1));
+        EXPECT_EQ(value, singleton.get_value_by_percentile(1));
+    }
+}
+
+TEST(QuantileStateTest, ExplicitMergeGroupingRetainsAllSamples) {
+    auto state = constant_state(0, QUANTILE_STATE_EXPLICIT_NUM - 1);
+    auto other = constant_state(0, 1);
+    other.add_value(100);
+    state.merge(other);
+    expect_digest_samples(state, QUANTILE_STATE_EXPLICIT_NUM + 1, 0, 100);
+}
+
 TEST(QuantileStateTest, SelfMergeAndSharedSourceMerge) {
     for (int count : {2, 4096}) {
         auto state = constant_state(10, count);
diff --git a/be/test/util/tdigest_test.cpp b/be/test/util/tdigest_test.cpp
index 0804fa3ad7f..8d7c0408f5a 100644
--- a/be/test/util/tdigest_test.cpp
+++ b/be/test/util/tdigest_test.cpp
@@ -123,6 +123,35 @@ TEST_F(TDigestTest, EmptyDigest) {
     EXPECT_EQ(0, digest.processed().size());
 }
 
+TEST_F(TDigestTest, NonPositiveValuesHaveCorrectMaximum) {
+    for (const auto& values : {std::vector<Value> {-3, -2, -1}, 
std::vector<Value> {-3, -2, 0}}) {
+        SCOPED_TRACE(values.back());
+        const auto expect_extrema = [&](TDigest& digest) {
+            EXPECT_EQ(values.front(), digest.quantile(0));
+            EXPECT_EQ(values.back(), digest.quantile(1));
+        };
+        TDigest digest(2048);
+        for (auto value : values) {
+            digest.add(value);
+        }
+        expect_extrema(digest);
+
+        std::vector<uint8_t> bytes(digest.serialized_size());
+        ASSERT_EQ(bytes.size(), digest.serialize(bytes.data()));
+        TDigest restored(0);
+        restored.unserialize(bytes.data());
+        expect_extrema(restored);
+
+        TDigest merged(2048);
+        merged.merge(&digest);
+        expect_extrema(merged);
+
+        std::vector<Centroid> processed(digest.processed());
+        TDigest from_centroids(std::move(processed), {}, 2048, 0, 0);
+        expect_extrema(from_centroids);
+    }
+}
+
 TEST_F(TDigestTest, SingleValue) {
     TDigest digest(100);
     std::random_device gen;
diff --git 
a/regression-test/data/nereids_p0/sql_functions/aggregate_functions/test_aggregate_all_functions.out
 
b/regression-test/data/nereids_p0/sql_functions/aggregate_functions/test_aggregate_all_functions.out
index ac05bf4e2aa..45cdeea68ce 100644
--- 
a/regression-test/data/nereids_p0/sql_functions/aggregate_functions/test_aggregate_all_functions.out
+++ 
b/regression-test/data/nereids_p0/sql_functions/aggregate_functions/test_aggregate_all_functions.out
@@ -267,7 +267,7 @@ beijing chengdu shanghai
 -- !select49 --
 20220201       0       1.0
 20220201       1       1.0
-20220202       2       2500.0
+20220202       2       2499.5
 
 -- !select50 --
 20220201       0       1.0
diff --git 
a/regression-test/data/query_p0/sql_functions/aggregate_functions/test_aggregate_all_functions.out
 
b/regression-test/data/query_p0/sql_functions/aggregate_functions/test_aggregate_all_functions.out
index 55298776e38..8d7b0042dee 100644
--- 
a/regression-test/data/query_p0/sql_functions/aggregate_functions/test_aggregate_all_functions.out
+++ 
b/regression-test/data/query_p0/sql_functions/aggregate_functions/test_aggregate_all_functions.out
@@ -303,7 +303,7 @@ beijing chengdu shanghai
 -- !select49 --
 20220201       0       1.0
 20220201       1       1.0
-20220202       2       2500.0
+20220202       2       2499.5
 
 -- !select50 --
 20220201       0       1.0
diff --git 
a/regression-test/data/query_p0/sql_functions/quantile_state_functions/test_quantile_state_explicit_boundary.out
 
b/regression-test/data/query_p0/sql_functions/quantile_state_functions/test_quantile_state_explicit_boundary.out
new file mode 100644
index 00000000000..12546dfd2f2
--- /dev/null
+++ 
b/regression-test/data/query_p0/sql_functions/quantile_state_functions/test_quantile_state_explicit_boundary.out
@@ -0,0 +1,22 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !partial_extrema --
+0      0       0       0
+0      1       100     100
+1      0       100     100
+1      1       0       0
+2      0       0       0
+2      1       0       100
+
+-- !merged_extrema --
+0      0       100
+1      0       100
+2      0       100
+
+-- !ordered_merge_extrema --
+0      0       0       0
+0      1       0       100
+1      0       100     100
+1      1       0       100
+2      0       0       0
+2      1       0       100
+
diff --git 
a/regression-test/suites/query_p0/sql_functions/quantile_state_functions/test_quantile_state_explicit_boundary.groovy
 
b/regression-test/suites/query_p0/sql_functions/quantile_state_functions/test_quantile_state_explicit_boundary.groovy
new file mode 100644
index 00000000000..94fae7ff8fe
--- /dev/null
+++ 
b/regression-test/suites/query_p0/sql_functions/quantile_state_functions/test_quantile_state_explicit_boundary.groovy
@@ -0,0 +1,70 @@
+// 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_quantile_state_explicit_boundary") {
+    sql "DROP TABLE IF EXISTS test_quantile_state_boundary_partials"
+    sql """
+        CREATE TABLE test_quantile_state_boundary_partials (
+            layout_id INT NOT NULL,
+            part INT NOT NULL,
+            s QUANTILE_STATE QUANTILE_UNION NOT NULL
+        ) AGGREGATE KEY(layout_id, part)
+        DISTRIBUTED BY HASH(layout_id) BUCKETS 1
+        PROPERTIES("replication_num" = "1")
+    """
+
+    // The same 2048 zeros and singleton 100 are grouped three ways:
+    // EXPLICIT + SINGLE, SINGLE + EXPLICIT, and EXPLICIT + EXPLICIT.
+    sql """
+        INSERT INTO test_quantile_state_boundary_partials
+        SELECT 0, CAST(number = 2048 AS INT),
+               quantile_union(to_quantile_state(IF(number = 2048, 100.0, 0.0), 
2048))
+        FROM numbers("number" = "2049") GROUP BY 2
+    """
+    sql """
+        INSERT INTO test_quantile_state_boundary_partials
+        SELECT 1, CAST(number != 2048 AS INT),
+               quantile_union(to_quantile_state(IF(number = 2048, 100.0, 0.0), 
2048))
+        FROM numbers("number" = "2049") GROUP BY 2
+    """
+    sql """
+        INSERT INTO test_quantile_state_boundary_partials
+        SELECT 2, CAST(number >= 2047 AS INT),
+               quantile_union(to_quantile_state(IF(number = 2048, 100.0, 0.0), 
2048))
+        FROM numbers("number" = "2049") GROUP BY 2
+    """
+
+    qt_partial_extrema """
+        SELECT layout_id, part, quantile_percent(s, 0), quantile_percent(s, 1)
+        FROM test_quantile_state_boundary_partials ORDER BY layout_id, part
+    """
+    qt_merged_extrema """
+        SELECT layout_id, quantile_percent(quantile_union(s), 0),
+               quantile_percent(quantile_union(s), 1)
+        FROM test_quantile_state_boundary_partials GROUP BY layout_id ORDER BY 
layout_id
+    """
+    // Window ordering guarantees both directions of the boundary conversion.
+    qt_ordered_merge_extrema """
+        SELECT layout_id, part, quantile_percent(s, 0), quantile_percent(s, 1)
+        FROM (
+            SELECT layout_id, part, quantile_union(s) OVER (
+                PARTITION BY layout_id ORDER BY part
+                ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS s
+            FROM test_quantile_state_boundary_partials
+        ) t ORDER BY layout_id, part
+    """
+}


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

Reply via email to