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
The following commit(s) were added to refs/heads/branch-4.2 by this push:
new dad6cd65a25 branch-4.2: [fix](be) Reset quantile union state for empty
window frames (#70239)
dad6cd65a25 is described below
commit dad6cd65a2599e755f6a8e3b8260a6d97f72eb1e
Author: Jerry Hu <[email protected]>
AuthorDate: Thu Nov 5 17:20:15 2026 +0800
branch-4.2: [fix](be) Reset quantile union state for empty window frames
(#70239)
### What problem does this PR solve?
Issue Number: None
Related PR: #69964
A bounded ROWS frame can become empty after a nonempty frame. The
`quantile_union` aggregate previously reset only its first-input flag,
leaving samples from the prior frame in `QuantileState`. Empty and
NULL-only frames could consequently return stale quantiles.
### What is changed?
- Reset the aggregate's `QuantileState` together with its first-input
flag.
- Add `QuantileState::reset()` to discard samples while retaining
explicit-vector capacity; preserve `clear()`'s capacity-release
behavior.
- Add focused unit and SQL regression coverage for empty, NULL-only, and
repeated frames.
### Release note
Fix `quantile_union` over empty window frames so `quantile_percent`
returns NaN rather than values from an earlier frame.
### Test
- ASAN BE+FE build, including FE Checkstyle: passed.
- Focused ASAN BE unit tests: 7/7 passed.
- Quantile SQL regression suites: 3/3 passed.
- clang-format, check-format, and `git diff --check`: passed.
---
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 | 185 +++++++++++++++++++++
.../test_quantile_union_empty_window.out | 28 ++++
.../test_quantile_union_empty_window.groovy | 91 ++++++++++
6 files changed, 315 insertions(+), 2 deletions(-)
diff --git a/be/src/core/value/quantile_state.cpp
b/be/src/core/value/quantile_state.cpp
index 47df852e2ad..c338b6e16b5 100644
--- a/be/src/core/value/quantile_state.cpp
+++ b/be/src/core/value/quantile_state.cpp
@@ -425,10 +425,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 e85bd4ec40b..192f73537a1 100644
--- a/be/src/exprs/aggregate/aggregate_function_quantile_state.h
+++ b/be/src/exprs/aggregate/aggregate_function_quantile_state.h
@@ -84,7 +84,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 d8497cba9cc..ad41f5821c2 100644
--- a/be/test/exprs/aggregate/agg_percentile_test.cpp
+++ b/be/test/exprs/aggregate/agg_percentile_test.cpp
@@ -17,11 +17,17 @@
#include <gtest/gtest.h>
+#include <cmath>
#include <memory>
#include <vector>
+#include "core/arena.h"
#include "core/column/column_complex.h"
+#include "core/column/column_nullable.h"
+#include "core/column/column_vector.h"
+#include "core/data_type/data_type_nullable.h"
#include "core/data_type/data_type_quantilestate.h"
+#include "core/value/quantile_state.h"
#include "exprs/aggregate/aggregate_function_quantile_state.h"
#include "util/defer_op.h"
#include "util/tdigest.h"
@@ -43,8 +49,187 @@ std::vector<double> expected_quantiles(const
std::vector<double>& values,
return result;
}
+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]