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 40883594296 [fix](be) Reset quantile union state for empty window 
frames (#69964)
40883594296 is described below

commit 4088359429618d31bee87b5f42e25c75935ee2af
Author: Jerry Hu <[email protected]>
AuthorDate: Tue Nov 3 11:46:56 2026 +0800

    [fix](be) Reset quantile union state for empty window frames (#69964)
    
    ### What problem does this PR solve?
    
    Issue Number: N/A
    
    Related PR: N/A
    
    Problem Summary: A bounded ROWS window can become empty after a nonempty
    frame. The `quantile_union` reset previously restored only the
    first-input flag and retained the preceding `QuantileState`. Without a
    subsequent value to merge, an empty or NULL-only frame returned the
    preceding frame's quantiles. Reset both the first-input flag and the
    stored samples before each frame. The logical reset retains explicit
    sample-vector capacity for reuse, while `clear()` preserves its
    storage-release behavior.
    
    ### Release note
    
    Fix `quantile_union` over empty window frames: `quantile_percent` now
    returns NaN instead of values from the preceding frame.
    
    ### Check List (For Author)
    
    - Test: ASAN BE unit tests (47 passed); ASAN BE+FE build (including FE
    Checkstyle); three isolated SQL regression suites (1 suite passed each);
    clang-format, format check, build hygiene, changed-line clang-tidy (no
    warnings), and diff check. No performance benchmark was run.
    - Behavior changed: Yes (empty window frames no longer inherit the
    preceding quantile state).
    - Does this need documentation: No.
---
 be/src/core/value/quantile_state.cpp               |   6 +-
 be/src/core/value/quantile_state.h                 |   2 +
 .../aggregate/aggregate_function_quantile_state.h  |   5 +-
 be/test/exprs/aggregate/agg_percentile_test.cpp    | 183 +++++++++++++++++++++
 .../test_quantile_union_empty_window.out           |  28 ++++
 .../test_quantile_union_empty_window.groovy        |  91 ++++++++++
 6 files changed, 313 insertions(+), 2 deletions(-)

diff --git a/be/src/core/value/quantile_state.cpp 
b/be/src/core/value/quantile_state.cpp
index c73ce277c2a..d432a44e352 100644
--- a/be/src/core/value/quantile_state.cpp
+++ b/be/src/core/value/quantile_state.cpp
@@ -424,10 +424,14 @@ void QuantileState::add_value(const double& value) {
     }
 }
 
-void QuantileState::clear() {
+void QuantileState::reset() {
     _type = EMPTY;
     _tdigest_ptr.reset();
     _explicit_data.clear();
+}
+
+void QuantileState::clear() {
+    reset();
     _explicit_data.shrink_to_fit();
 }
 
diff --git a/be/src/core/value/quantile_state.h 
b/be/src/core/value/quantile_state.h
index fc540848c26..82e842a010c 100644
--- a/be/src/core/value/quantile_state.h
+++ b/be/src/core/value/quantile_state.h
@@ -60,6 +60,8 @@ public:
     size_t serialize(uint8_t* dst) const;
     void merge(const QuantileState& other);
     void add_value(const double& value);
+    // Clear samples while retaining explicit capacity for the next aggregate 
frame.
+    void reset();
     void clear();
     bool is_valid(const Slice& slice);
     size_t get_serialized_size() const;
diff --git a/be/src/exprs/aggregate/aggregate_function_quantile_state.h 
b/be/src/exprs/aggregate/aggregate_function_quantile_state.h
index 7f5f09710bd..2b43653f6bb 100644
--- a/be/src/exprs/aggregate/aggregate_function_quantile_state.h
+++ b/be/src/exprs/aggregate/aggregate_function_quantile_state.h
@@ -83,7 +83,10 @@ struct AggregateFunctionQuantileStateData {
 
     void read(BufferReadable& buf) { 
DataTypeQuantileState::deserialize_as_stream(value, buf); }
 
-    void reset() { is_first = true; }
+    void reset() {
+        is_first = true;
+        value.reset();
+    }
 
     DataType& get() { return value; }
 
diff --git a/be/test/exprs/aggregate/agg_percentile_test.cpp 
b/be/test/exprs/aggregate/agg_percentile_test.cpp
index 7a1030e837c..3912d543b23 100644
--- a/be/test/exprs/aggregate/agg_percentile_test.cpp
+++ b/be/test/exprs/aggregate/agg_percentile_test.cpp
@@ -17,11 +17,13 @@
 
 #include <gtest/gtest.h>
 
+#include <cmath>
 #include <limits>
 #include <memory>
 #include <optional>
 #include <vector>
 
+#include "core/arena.h"
 #include "core/column/column_array.h"
 #include "core/column/column_complex.h"
 #include "core/column/column_nullable.h"
@@ -32,9 +34,11 @@
 #include "core/data_type/data_type_number.h"
 #include "core/data_type/data_type_quantilestate.h"
 #include "core/string_buffer.hpp"
+#include "core/value/quantile_state.h"
 #include "exprs/aggregate/aggregate_function_percentile.h"
 #include "exprs/aggregate/aggregate_function_quantile_state.h"
 #include "exprs/aggregate/aggregate_function_simple_factory.h"
+#include "util/defer_op.h"
 #include "util/tdigest.h"
 
 namespace doris {
@@ -111,8 +115,187 @@ void expect_results_equal(const std::vector<double>& 
actual, const std::vector<d
     }
 }
 
+void expect_reset_quantile_state(size_t count, bool is_window) {
+    auto type = std::make_shared<DataTypeQuantileState>();
+    auto function = create_aggregate_function_quantile_state_union(
+            "quantile_union", {type}, type, false,
+            {.is_window_function = is_window, .column_names = {}});
+    std::unique_ptr<char[]> memory(new char[function->size_of_data()]);
+    auto* place = memory.get();
+    function->create(place);
+    Defer destroy([&] { function->destroy(place); });
+    Arena arena;
+    auto input = ColumnQuantileState::create();
+    QuantileState seed(10000);
+    for (size_t i = 0; i < count; ++i) {
+        seed.add_value(20);
+    }
+    input->insert_value(seed);
+    QuantileState next(2048);
+    next.add_value(30);
+    input->insert_value(next);
+    const IColumn* columns[] = {input.get()};
+
+    auto results = ColumnQuantileState::create();
+    function->insert_result_into(place, *results);
+    function->add(place, columns, 0, arena);
+    function->insert_result_into(place, *results);
+    function->reset(place);
+    function->insert_result_into(place, *results);
+    function->reset(place);
+    function->insert_result_into(place, *results);
+    function->add(place, columns, 1, arena);
+    function->insert_result_into(place, *results);
+
+    ASSERT_EQ(5, results->size());
+    for (double percentile : {0.0, 0.5, 1.0}) {
+        
EXPECT_TRUE(std::isnan(results->get_element(0).get_value_by_percentile(percentile)));
+        EXPECT_DOUBLE_EQ(20, 
results->get_element(1).get_value_by_percentile(percentile));
+        
EXPECT_TRUE(std::isnan(results->get_element(2).get_value_by_percentile(percentile)));
+        
EXPECT_TRUE(std::isnan(results->get_element(3).get_value_by_percentile(percentile)));
+        EXPECT_DOUBLE_EQ(30, 
results->get_element(4).get_value_by_percentile(percentile));
+        EXPECT_DOUBLE_EQ(20, 
input->get_element(0).get_value_by_percentile(percentile));
+    }
+    EXPECT_EQ(sizeof(float) + 1, 
results->get_element(2).get_serialized_size());
+    EXPECT_EQ(sizeof(float) + 1, 
results->get_element(3).get_serialized_size());
+}
+
+void append_explicit_frame(const AggregateFunctionPtr& function, 
AggregateDataPtr place,
+                           const IColumn** columns, size_t row, size_t 
frame_size, Arena& arena,
+                           ColumnQuantileState& results, QuantileState& 
accumulator) {
+    for (size_t sample = 0; sample < frame_size; ++sample) {
+        function->add(place, columns, row, arena);
+    }
+    ASSERT_EQ(EXPLICIT, accumulator._type);
+    const size_t capacity = accumulator._explicit_data.capacity();
+    ASSERT_GE(capacity, frame_size);
+    function->insert_result_into(place, results);
+    function->reset(place);
+    EXPECT_EQ(EMPTY, accumulator._type);
+    EXPECT_TRUE(accumulator._explicit_data.empty());
+    // Bounded ROWS frames reset on every row; the warmed vector must survive 
each reset.
+    EXPECT_EQ(capacity, accumulator._explicit_data.capacity());
+    function->insert_result_into(place, results);
+    function->add(place, columns, row, arena);
+    EXPECT_EQ(capacity, accumulator._explicit_data.capacity());
+    function->reset(place);
+}
+
+void expect_repeated_explicit_frames_reuse_capacity(size_t frame_size) {
+    auto type = std::make_shared<DataTypeQuantileState>();
+    auto function = create_aggregate_function_quantile_state_union(
+            "quantile_union", {type}, type, false,
+            {.is_window_function = true, .column_names = {}});
+    std::unique_ptr<char[]> memory(new char[function->size_of_data()]);
+    auto* place = memory.get();
+    function->create(place);
+    Defer destroy([&] { function->destroy(place); });
+    Arena arena;
+    auto input = ColumnQuantileState::create();
+    for (double value : {20.0, 30.0}) {
+        QuantileState state;
+        state.add_value(value);
+        input->insert_value(state);
+    }
+    const IColumn* columns[] = {input.get()};
+    using Data = 
AggregateFunctionQuantileStateData<AggregateFunctionQuantileStateUnionOp>;
+    auto& accumulator = reinterpret_cast<Data*>(place)->value;
+
+    auto results = ColumnQuantileState::create();
+    for (size_t frame = 0; frame < 3; ++frame) {
+        append_explicit_frame(function, place, columns, frame % 2, frame_size, 
arena, *results,
+                              accumulator);
+    }
+    ASSERT_EQ(6, results->size());
+    for (size_t frame = 0; frame < 3; ++frame) {
+        const double expected = frame % 2 == 0 ? 20 : 30;
+        EXPECT_DOUBLE_EQ(expected, results->get_element(2 * 
frame).get_value_by_percentile(0.5));
+        EXPECT_TRUE(std::isnan(results->get_element(2 * frame + 
1).get_value_by_percentile(0.5)));
+    }
+}
+
 } // namespace
 
+TEST(AggregateFunctionQuantileStateTest, EmptyWindowAfterNonemptyWindow) {
+    auto type = std::make_shared<DataTypeQuantileState>();
+    auto function = create_aggregate_function_quantile_state_union(
+            "quantile_union", {type}, type, false,
+            {.is_window_function = true, .column_names = {}});
+    std::unique_ptr<char[]> memory(new char[function->size_of_data()]);
+    auto* place = memory.get();
+    function->create(place);
+    Defer destroy([&] { function->destroy(place); });
+    Arena arena;
+    auto input = ColumnQuantileState::create();
+    QuantileState state;
+    state.add_value(20);
+    input->insert_value(state);
+    const IColumn* columns[] = {input.get()};
+
+    auto results = ColumnQuantileState::create();
+    function->insert_result_into(place, *results);
+    function->add(place, columns, 0, arena);
+    function->insert_result_into(place, *results);
+    function->reset(place);
+    function->insert_result_into(place, *results);
+
+    ASSERT_EQ(3, results->size());
+    for (double percentile : {0.0, 0.5, 1.0}) {
+        
EXPECT_TRUE(std::isnan(results->get_element(0).get_value_by_percentile(percentile)));
+        EXPECT_DOUBLE_EQ(20, 
results->get_element(1).get_value_by_percentile(percentile));
+        
EXPECT_TRUE(std::isnan(results->get_element(2).get_value_by_percentile(percentile)));
+    }
+}
+
+TEST(AggregateFunctionQuantileStateTest, ResetClearsAllRepresentations) {
+    for (bool is_window : {false, true}) {
+        for (size_t count : {size_t(1), size_t(3), 
size_t(QUANTILE_STATE_EXPLICIT_NUM + 1)}) {
+            expect_reset_quantile_state(count, is_window);
+        }
+    }
+}
+
+TEST(AggregateFunctionQuantileStateTest, RepeatedExplicitFramesReuseCapacity) {
+    for (size_t frame_size : {size_t(2), size_t(3), 
size_t(QUANTILE_STATE_EXPLICIT_NUM)}) {
+        expect_repeated_explicit_frames_reuse_capacity(frame_size);
+    }
+}
+
+TEST(AggregateFunctionQuantileStateTest, NullOnlyWindowAfterResetIsEmpty) {
+    auto type = std::make_shared<DataTypeQuantileState>();
+    auto function = create_aggregate_function_quantile_state_union(
+            "quantile_union", {make_nullable(type)}, type, false,
+            {.is_window_function = true, .column_names = {}});
+    std::unique_ptr<char[]> memory(new char[function->size_of_data()]);
+    auto* place = memory.get();
+    function->create(place);
+    Defer destroy([&] { function->destroy(place); });
+    Arena arena;
+    auto nested = ColumnQuantileState::create();
+    QuantileState state;
+    state.add_value(20);
+    nested->insert_value(state);
+    nested->insert_default();
+    auto null_map = ColumnUInt8::create();
+    null_map->insert_value(0);
+    null_map->insert_value(1);
+    auto input = ColumnNullable::create(std::move(nested), 
std::move(null_map));
+    const IColumn* columns[] = {input.get()};
+
+    auto results = ColumnQuantileState::create();
+    function->add(place, columns, 0, arena);
+    function->insert_result_into(place, *results);
+    function->reset(place);
+    function->add(place, columns, 1, arena);
+    function->insert_result_into(place, *results);
+
+    ASSERT_EQ(2, results->size());
+    for (double percentile : {0.0, 0.5, 1.0}) {
+        EXPECT_DOUBLE_EQ(20, 
results->get_element(0).get_value_by_percentile(percentile));
+        
EXPECT_TRUE(std::isnan(results->get_element(1).get_value_by_percentile(percentile)));
+    }
+}
+
 TEST(AggregateFunctionQuantileStateTest, GrowingWindowKeepsResultsCompact) {
     auto type = std::make_shared<DataTypeQuantileState>();
     auto function = create_aggregate_function_quantile_state_union(
diff --git 
a/regression-test/data/query_p0/sql_functions/quantile_state_functions/test_quantile_union_empty_window.out
 
b/regression-test/data/query_p0/sql_functions/quantile_state_functions/test_quantile_union_empty_window.out
new file mode 100644
index 00000000000..8fd70eee474
--- /dev/null
+++ 
b/regression-test/data/query_p0/sql_functions/quantile_state_functions/test_quantile_union_empty_window.out
@@ -0,0 +1,28 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !initial_empty --
+0      NaN     NaN     NaN
+
+-- !nonempty_then_empty --
+0      20      20      20
+1      NaN     NaN     NaN
+
+-- !consecutive_nonempty_then_empty --
+0      20      20      20
+1      30      30      30
+2      NaN     NaN     NaN
+
+-- !multi_value_frame_then_empty --
+0      20      25      30
+1      30      30      30
+2      NaN     NaN     NaN
+
+-- !current_row_control --
+0      10      10      10
+1      20      20      20
+2      30      30      30
+
+-- !partition_reset --
+0      0       NaN     NaN     NaN
+0      1       10      10      10
+1      0       NaN     NaN     NaN
+1      1       30      30      30
diff --git 
a/regression-test/suites/query_p0/sql_functions/quantile_state_functions/test_quantile_union_empty_window.groovy
 
b/regression-test/suites/query_p0/sql_functions/quantile_state_functions/test_quantile_union_empty_window.groovy
new file mode 100644
index 00000000000..3bed8233f84
--- /dev/null
+++ 
b/regression-test/suites/query_p0/sql_functions/quantile_state_functions/test_quantile_union_empty_window.groovy
@@ -0,0 +1,91 @@
+// 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_union_empty_window") {
+    qt_initial_empty """
+        SELECT id, quantile_percent(q, 0) AS q_min,
+               quantile_percent(q, 0.5) AS q_median, quantile_percent(q, 1) AS 
q_max
+        FROM (
+            SELECT number AS id,
+                   quantile_union(to_quantile_state(10 + 10 * number, 2048)) 
OVER (
+                       ORDER BY number ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING
+                   ) AS q
+            FROM numbers("number" = "1")
+        ) w ORDER BY id
+    """
+
+    qt_nonempty_then_empty """
+        SELECT id, quantile_percent(q, 0) AS q_min,
+               quantile_percent(q, 0.5) AS q_median, quantile_percent(q, 1) AS 
q_max
+        FROM (
+            SELECT number AS id,
+                   quantile_union(to_quantile_state(10 + 10 * number, 2048)) 
OVER (
+                       ORDER BY number ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING
+                   ) AS q
+            FROM numbers("number" = "2")
+        ) w ORDER BY id
+    """
+
+    qt_consecutive_nonempty_then_empty """
+        SELECT id, quantile_percent(q, 0) AS q_min,
+               quantile_percent(q, 0.5) AS q_median, quantile_percent(q, 1) AS 
q_max
+        FROM (
+            SELECT number AS id,
+                   quantile_union(to_quantile_state(10 + 10 * number, 2048)) 
OVER (
+                       ORDER BY number ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING
+                   ) AS q
+            FROM numbers("number" = "3")
+        ) w ORDER BY id
+    """
+
+    qt_multi_value_frame_then_empty """
+        SELECT id, quantile_percent(q, 0) AS q_min,
+               quantile_percent(q, 0.5) AS q_median, quantile_percent(q, 1) AS 
q_max
+        FROM (
+            SELECT number AS id,
+                   quantile_union(to_quantile_state(10 + 10 * number, 2048)) 
OVER (
+                       ORDER BY number ROWS BETWEEN 1 FOLLOWING AND 2 FOLLOWING
+                   ) AS q
+            FROM numbers("number" = "3")
+        ) w ORDER BY id
+    """
+
+    qt_current_row_control """
+        SELECT id, quantile_percent(q, 0) AS q_min,
+               quantile_percent(q, 0.5) AS q_median, quantile_percent(q, 1) AS 
q_max
+        FROM (
+            SELECT number AS id,
+                   quantile_union(to_quantile_state(10 + 10 * number, 2048)) 
OVER (
+                       ORDER BY number ROWS BETWEEN CURRENT ROW AND CURRENT ROW
+                   ) AS q
+            FROM numbers("number" = "3")
+        ) w ORDER BY id
+    """
+
+    qt_partition_reset """
+        SELECT p, id, quantile_percent(q, 0) AS q_min,
+               quantile_percent(q, 0.5) AS q_median, quantile_percent(q, 1) AS 
q_max
+        FROM (
+            SELECT number DIV 2 AS p, number % 2 AS id,
+                   quantile_union(to_quantile_state(10 + 10 * number, 2048)) 
OVER (
+                       PARTITION BY number DIV 2 ORDER BY number % 2
+                       ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING
+                   ) AS q
+            FROM numbers("number" = "4")
+        ) w ORDER BY p, id
+    """
+}


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

Reply via email to