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

HappenLee 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 b79d65203ce [fix](be) Preserve the sample triggering QuantileState 
digest conversion (#69037)
b79d65203ce is described below

commit b79d65203cef65c0c1a5a4e39f7f85772cac0949
Author: TengJianPing <[email protected]>
AuthorDate: Wed Oct 14 10:11:55 2026 +0800

    [fix](be) Preserve the sample triggering QuantileState digest conversion 
(#69037)
    
    ### What problem does this PR solve?
    
    Issue Number: N/A
    
    Related PR: N/A
    
    Problem Summary: Adding a sample to a QuantileState with 2048 explicit
    values converted the existing values to a TDigest but omitted the
    incoming sample. Merging 2048 zeros with a singleton 100 consequently
    returned a maximum of 0 and total weight 2048, while a different
    grouping retained all 2049 inputs. Add the triggering sample before
    clearing the explicit buffer. The shared add_value path fixes direct
    additions and both EXPLICIT/SINGLE merge orders. Add unit coverage for
    boundary counts, both extrema, compression limits, merge orders and the
    EXPLICIT/EXPLICIT control grouping, plus SQL regression coverage for
    stored partial states and ordered window merges.
    
    ### Release note
    
    Fix lost samples and incorrect quantile results when QUANTILE_STATE
    crosses the 2048-sample explicit representation limit.
    
    ### Check List (For Author)
    
    - Test:
    - Unit Test: ASAN run-be-ut.sh, all 48 QuantileState-related tests
    passed. Reverting the fix makes both new boundary tests fail; the
    control passes.
    - Regression test: run-regression-test.sh,
    test_quantile_state_explicit_boundary and test_quantile_state_function
    passed; expected output was generated by the runner.
    - Manual test: the reported SQL changes from maximum 0 / weight 2048 to
    maximum 100 / weight 2049 using the same persisted partial states.
    - ASAN BE build, clang-format 16, clang-tidy and build hygiene checks
    passed.
    - Behavior changed: Yes, retain the sample triggering
    EXPLICIT-to-TDIGEST conversion.
    - Does this need documentation: No
    
    ### What problem does this PR solve?
    
    Issue Number: close #xxx
    
    Related PR: #xxx
    
    Problem Summary:
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [ ] Regression test
        - [ ] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason <!-- Add your reason?  -->
    
    - Behavior changed:
        - [ ] No.
        - [ ] Yes. <!-- Explain the behavior change -->
    
    - Does this need documentation?
        - [ ] No.
    - [ ] Yes. <!-- Add document PR link here. eg:
    https://github.com/apache/doris-website/pull/1214 -->
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label <!-- Add branch pick label that this PR
    should merge into -->
---
 be/src/core/value/quantile_state.cpp               |  2 +-
 be/test/core/value/quantile_state_test.cpp         | 60 +++++++++++++++++++
 .../test_quantile_state_explicit_boundary.out      | 22 +++++++
 .../test_quantile_state_explicit_boundary.groovy   | 70 ++++++++++++++++++++++
 4 files changed, 153 insertions(+), 1 deletion(-)

diff --git a/be/src/core/value/quantile_state.cpp 
b/be/src/core/value/quantile_state.cpp
index c5008f59616..c73ce277c2a 100644
--- a/be/src/core/value/quantile_state.cpp
+++ b/be/src/core/value/quantile_state.cpp
@@ -410,10 +410,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/test/core/value/quantile_state_test.cpp 
b/be/test/core/value/quantile_state_test.cpp
index 80ac1aa1ccf..defb69a62da 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.total_weight();
 }
 
+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/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