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 5fb13937573 [fix](be) Treat non-positive topn expansion rates as
unlimited (#68246)
5fb13937573 is described below
commit 5fb13937573c538e718b821fa1fceaed521e9118
Author: HappenLee <[email protected]>
AuthorDate: Tue Sep 22 11:43:45 2026 +0800
[fix](be) Treat non-positive topn expansion rates as unlimited (#68246)
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary:
With a zero `space_expand_rate`, `topn` and `topn_array` can return a
value in one-stage aggregation but an empty result in two-stage
aggregation. The BE computes a zero candidate capacity, so serializing
the partial state discards every candidate. For example, `topn(s, 1, 0)`
over `['a', 'b', 'a']` should return `{"a":2}` in both aggregation
phases.
Treat non-positive expansion rates as unlimited candidate capacity in
the shared BE state used by `topn`, `topn_array`, and `topn_weighted`.
Retain all intermediate candidates and exact counts through
serialization and merging; `top_num` still limits the final result.
Positive expansion rates and the default rate of 50 keep their existing
behavior.
Add coverage for zero, negative and minimum INT rates, multiple
serialization/merge stages, global winners outside each local top N,
weighted values, empty/NULL input, and state reuse. Update the existing
aggregate-state parameter tests: non-positive rates describe compatible
unlimited states, while unlimited and finite-capacity states are
incompatible. The empty-state regression cases now use NULL input.
### Release note
`topn`, `topn_array`, and `topn_weighted` now interpret a non-positive
`space_expand_rate` as unlimited intermediate candidate retention. The
result remains limited by `top_num`. Retaining all distinct candidates
can increase intermediate-state memory and network traffic.
### Check List (For Author)
- Test
- [x] Regression test
- [x] Unit Test
- [x] Manual test
- [ ] No need to test or manual test.
Validation before rebasing onto current master:
- FE and BE ASAN builds passed.
- 10 TOPN BE unit tests passed.
- Both `topn` and `topn_unlimited` regression suites passed; expected
output was generated by the regression runner.
- SQL checks returned `{"a":2}` and `["a"]` for non-positive rates in
both aggregation phases; EXPLAIN confirmed partial serialization
followed by merge/finalization.
- clang-format 16, build hygiene, and clang-tidy passed.
Validation on the PR branch:
- Updated the reset/reuse test and existing aggregate-state tests for
current master.
- clang-format 16, build hygiene, and clang-tidy passed.
- ASAN unit tests passed on the PR branch: 10 TOPN tests and all 16
`AggregateStateParametersTest` tests (26 total).
- Commands:
- `./run-be-ut.sh -j 48 --run
--filter='AggregateFunctionTopN*.*:*/AggregateFunctionTopN*.*:AggTest.topn*'`
- `./run-be-ut.sh -j 48 --run --filter='AggregateStateParametersTest.*'`
- The newly adjusted `test_agg_state_parameters` regression suite has
not been rerun locally.
- Behavior changed:
- [ ] No.
- [x] Yes. Non-positive expansion rates explicitly mean unlimited
candidate retention.
- Does this need documentation?
- [ ] No.
- [x] Yes. The SQL function docs should describe unlimited candidate
retention for non-positive expansion rates; a documentation PR is not
included.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
be/src/exprs/aggregate/aggregate_function_topn.h | 3 +-
.../exprs/aggregate/agg_state_parameters_test.cpp | 63 +++--------
be/test/exprs/aggregate/agg_topn_test.cpp | 125 +++++++++++++++++++++
.../agg_function/topn/topn_unlimited.out | 91 +++++++++++++++
.../agg_state/test_agg_state_parameters.groovy | 12 +-
.../agg_function/topn/topn_unlimited.groovy | 76 +++++++++++++
6 files changed, 320 insertions(+), 50 deletions(-)
diff --git a/be/src/exprs/aggregate/aggregate_function_topn.h
b/be/src/exprs/aggregate/aggregate_function_topn.h
index 501b1af198b..062b0e8eefe 100644
--- a/be/src/exprs/aggregate/aggregate_function_topn.h
+++ b/be/src/exprs/aggregate/aggregate_function_topn.h
@@ -59,7 +59,8 @@ struct AggregateFunctionTopNData {
using DataType = typename PrimitiveTypeTraits<T>::CppType;
void set_paramenters(int input_top_num, int space_expand_rate = 50) {
top_num = input_top_num;
- capacity = (uint64_t)top_num * space_expand_rate;
+ // Non-positive expansion rates retain all candidates during
serialization and merging.
+ capacity = space_expand_rate <= 0 ? UINT64_MAX : (uint64_t)top_num *
space_expand_rate;
}
void add(const StringRef& value, const UInt64& increment = 1) {
diff --git a/be/test/exprs/aggregate/agg_state_parameters_test.cpp
b/be/test/exprs/aggregate/agg_state_parameters_test.cpp
index b1c500e846f..aa8a3a8cda4 100644
--- a/be/test/exprs/aggregate/agg_state_parameters_test.cpp
+++ b/be/test/exprs/aggregate/agg_state_parameters_test.cpp
@@ -17,6 +17,7 @@
#include <gtest/gtest.h>
+#include <cstdint>
#include <limits>
#include "common/exception.h"
@@ -88,12 +89,6 @@ public:
});
}
- void check_direct_mismatch(const Arguments& first, const Arguments&
second) {
- auto* destination = create(first);
- auto* source = create(second);
- expect_incompatible([&] { _function->merge(destination, source,
_arena); });
- }
-
void check_merge_result(const Arguments& initial, const Arguments&
incoming, bool serialized) {
auto* destination = create();
auto* source = create();
@@ -147,21 +142,6 @@ public:
EXPECT_TRUE(ColumnHelper::column_equal(result(destination),
result(expected)));
}
- void check_configured_empty_payload(const Arguments& arguments) {
- // TopN with zero capacity keeps counters in memory but serializes no
counters.
- // A decoded empty state must not change compatible counters.
- auto serialized = serialize(create(arguments));
- auto* configured_empty = create();
- _function->deserialize_and_merge_from_column(configured_empty,
*serialized, _arena);
- auto* populated = create(arguments);
- EXPECT_NO_THROW(
- _function->deserialize_and_merge_from_column(populated,
*serialized, _arena));
- EXPECT_TRUE(ColumnHelper::column_equal(result(populated),
result(create(arguments))));
- EXPECT_NO_THROW(_function->merge(configured_empty, create(arguments),
_arena));
- EXPECT_TRUE(
- ColumnHelper::column_equal(result(configured_empty),
result(create(arguments))));
- }
-
void check_compatible(const Arguments& arguments) {
auto* destination = create();
auto serialized = serialize(create(arguments));
@@ -394,33 +374,24 @@ TEST(AggregateStateParametersTest, Histogram) {
{value, argument<DataTypeInt32>(3)});
}
-TEST(AggregateStateParametersTest, TopNZeroCapacityParameters) {
+TEST(AggregateStateParametersTest, TopNUnlimitedParameters) {
for (const auto& name : {"topn", "topn_array", "topn_weighted"}) {
- SCOPED_TRACE(name);
- Arguments zero_capacity {argument<DataTypeString>("a")};
- if (std::string(name) == "topn_weighted") {
- zero_capacity.push_back(argument<DataTypeInt64>(1));
- }
- zero_capacity.push_back(argument<DataTypeInt32>(1));
- zero_capacity.push_back(argument<DataTypeInt32>(0));
- auto positive_capacity = zero_capacity;
- positive_capacity.back() = argument<DataTypeInt32>(2);
- DataTypes types;
- for (const auto& arg : zero_capacity) {
- types.push_back(arg.type);
+ for (int rate : {0, -1, INT32_MIN}) {
+ SCOPED_TRACE(rate);
+ Arguments unlimited {argument<DataTypeString>("a")};
+ if (std::string(name) == "topn_weighted") {
+ unlimited.push_back(argument<DataTypeInt64>(1));
+ }
+ unlimited.push_back(argument<DataTypeInt32>(1));
+ unlimited.push_back(argument<DataTypeInt32>(rate));
+ auto finite = unlimited;
+ finite.back() = argument<DataTypeInt32>(2);
+ check_parameters(name, unlimited, finite);
+
+ auto zero_rate = unlimited;
+ zero_rate.back() = argument<DataTypeInt32>(0);
+ check_compatible_states(name, unlimited, zero_rate);
}
- auto function = AggregateFunctionSimpleFactory::instance().get(
- name, types, nullptr, false,
BeExecVersionManager::get_newest_version());
- ASSERT_NE(function, nullptr);
- StateParameterChecks checks(function);
- // Only the serialized zero-capacity state is empty; in-memory
counters contribute.
- checks.check_direct_mismatch(zero_capacity, positive_capacity);
- checks.check_direct_mismatch(positive_capacity, zero_capacity);
- check_ignored_parameters(name, zero_capacity, positive_capacity, true);
- checks.check_merge_result({}, zero_capacity, false);
- checks.check_merge_result(zero_capacity, {}, false);
- checks.check_merge_result(zero_capacity, {}, true);
- checks.check_configured_empty_payload(zero_capacity);
}
}
diff --git a/be/test/exprs/aggregate/agg_topn_test.cpp
b/be/test/exprs/aggregate/agg_topn_test.cpp
new file mode 100644
index 00000000000..324bb7272d9
--- /dev/null
+++ b/be/test/exprs/aggregate/agg_topn_test.cpp
@@ -0,0 +1,125 @@
+// 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.
+
+#include <gtest/gtest.h>
+
+#include <cstdint>
+#include <string>
+
+#include "core/column/column_string.h"
+#include "core/column/column_vector.h"
+#include "core/string_buffer.hpp"
+#include "exprs/aggregate/aggregate_function_topn.h"
+
+namespace doris {
+namespace {
+
+template <PrimitiveType T>
+AggregateFunctionTopNData<T> round_trip(const AggregateFunctionTopNData<T>&
state) {
+ auto column = ColumnString::create();
+ BufferWritable writer(*column);
+ state.write(writer);
+ writer.commit();
+ BufferReadable reader(column->get_data_at(0));
+ AggregateFunctionTopNData<T> result;
+ result.read(reader);
+ return result;
+}
+
+class AggregateFunctionTopNUnlimitedTest : public testing::TestWithParam<int>
{};
+
+TEST_P(AggregateFunctionTopNUnlimitedTest, SerializeAndMergeStrings) {
+ AggregateFunctionTopNData<TYPE_STRING> lhs;
+ AggregateFunctionTopNData<TYPE_STRING> rhs;
+ lhs.set_paramenters(1, GetParam());
+ rhs.set_paramenters(1, GetParam());
+ // The global winner is not the most frequent value in either partial
state.
+ lhs.add(std::string("a"), 3);
+ lhs.add(std::string("winner"), 2);
+ rhs.add(std::string("b"), 3);
+ rhs.add(std::string("winner"), 2);
+
+ auto partial = round_trip(lhs);
+ EXPECT_EQ(partial.counter_map, lhs.counter_map);
+ AggregateFunctionTopNData<TYPE_STRING> merged;
+ merged.merge(partial);
+ merged = round_trip(merged);
+ merged.merge(round_trip(rhs));
+ merged = round_trip(merged);
+
+ ASSERT_EQ(merged.counter_map.size(), 3);
+ EXPECT_EQ(merged.counter_map.at("a"), 3);
+ EXPECT_EQ(merged.counter_map.at("b"), 3);
+ EXPECT_EQ(merged.counter_map.at("winner"), 4);
+ EXPECT_EQ(merged.get(), R"({"winner":4})");
+
+ AggregateFunctionTopNData<TYPE_STRING> empty;
+ merged.merge(round_trip(empty));
+ EXPECT_EQ(merged.get(), R"({"winner":4})");
+
+ merged.reset();
+ EXPECT_EQ(round_trip(merged).get(), "{}");
+ merged.set_paramenters(1, GetParam());
+ merged.add(std::string("new"), 7);
+ EXPECT_EQ(round_trip(merged).get(), R"({"new":7})");
+}
+
+TEST_P(AggregateFunctionTopNUnlimitedTest, SerializeAndMergeWeightedIntegers) {
+ AggregateFunctionTopNData<TYPE_INT> lhs;
+ AggregateFunctionTopNData<TYPE_INT> rhs;
+ lhs.set_paramenters(2, GetParam());
+ rhs.set_paramenters(2, GetParam());
+ lhs.add(1, 10);
+ lhs.add(2, 7);
+ lhs.add(3, 6);
+ rhs.add(4, 11);
+ rhs.add(5, 8);
+ rhs.add(3, 6);
+
+ AggregateFunctionTopNData<TYPE_INT> merged;
+ merged.merge(round_trip(lhs));
+ merged.merge(round_trip(rhs));
+ merged = round_trip(merged);
+ ASSERT_EQ(merged.counter_map.size(), 5);
+ EXPECT_EQ(merged.counter_map.at(3), 12);
+ auto result = ColumnInt32::create();
+ merged.insert_result_into(*result);
+ ASSERT_EQ(result->size(), 2);
+ EXPECT_EQ(result->get_element(0), 3);
+ EXPECT_EQ(result->get_element(1), 4);
+}
+
+INSTANTIATE_TEST_SUITE_P(NonPositiveRates, AggregateFunctionTopNUnlimitedTest,
+ testing::Values(0, -1, INT32_MIN));
+
+TEST(AggregateFunctionTopNTest, PositiveRateStillLimitsSerializedCandidates) {
+ AggregateFunctionTopNData<TYPE_INT> state;
+ state.set_paramenters(1, 2);
+ state.add(1, 3);
+ state.add(2, 2);
+ state.add(3, 1);
+ auto partial = round_trip(state);
+ ASSERT_EQ(partial.counter_map.size(), 2);
+ EXPECT_EQ(partial.counter_map.at(1), 3);
+ EXPECT_EQ(partial.counter_map.at(2), 2);
+
+ state.set_paramenters(1);
+ EXPECT_EQ(round_trip(state).counter_map, state.counter_map);
+}
+
+} // namespace
+} // namespace doris
diff --git
a/regression-test/data/nereids_function_p0/agg_function/topn/topn_unlimited.out
b/regression-test/data/nereids_function_p0/agg_function/topn/topn_unlimited.out
new file mode 100644
index 00000000000..888ec8e5112
--- /dev/null
+++
b/regression-test/data/nereids_function_p0/agg_function/topn/topn_unlimited.out
@@ -0,0 +1,91 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !unlimited --
+{"a":3,"x":2} ["a", "x"] [1, 4] ["b", "y"] [2, 5]
+
+-- !grouped --
+1 {"a":3,"b":2} ["a", "b"] [1, 2] ["b", "a"] [2, 1]
+2 {"x":2,"y":1} ["x", "y"] [4, 5] ["y", "x"] [5, 4]
+3 \N \N \N \N \N
+
+-- !empty --
+\N \N \N
+
+-- !constant_input --
+{"a":2} ["a"]
+
+-- !unlimited --
+{"a":3,"x":2} ["a", "x"] [1, 4] ["b", "y"] [2, 5]
+
+-- !grouped --
+1 {"a":3,"b":2} ["a", "b"] [1, 2] ["b", "a"] [2, 1]
+2 {"x":2,"y":1} ["x", "y"] [4, 5] ["y", "x"] [5, 4]
+3 \N \N \N \N \N
+
+-- !empty --
+\N \N \N
+
+-- !constant_input --
+{"a":2} ["a"]
+
+-- !unlimited --
+{"a":3,"x":2} ["a", "x"] [1, 4] ["b", "y"] [2, 5]
+
+-- !grouped --
+1 {"a":3,"b":2} ["a", "b"] [1, 2] ["b", "a"] [2, 1]
+2 {"x":2,"y":1} ["x", "y"] [4, 5] ["y", "x"] [5, 4]
+3 \N \N \N \N \N
+
+-- !empty --
+\N \N \N
+
+-- !constant_input --
+{"a":2} ["a"]
+
+-- !positive_and_default --
+{"a":3,"x":2} {"a":3,"x":2} [1, 4] [1, 4] [2, 5] [2, 5]
+
+-- !unlimited --
+{"a":3,"x":2} ["a", "x"] [1, 4] ["b", "y"] [2, 5]
+
+-- !grouped --
+1 {"a":3,"b":2} ["a", "b"] [1, 2] ["b", "a"] [2, 1]
+2 {"x":2,"y":1} ["x", "y"] [4, 5] ["y", "x"] [5, 4]
+3 \N \N \N \N \N
+
+-- !empty --
+\N \N \N
+
+-- !constant_input --
+{"a":2} ["a"]
+
+-- !unlimited --
+{"a":3,"x":2} ["a", "x"] [1, 4] ["b", "y"] [2, 5]
+
+-- !grouped --
+1 {"a":3,"b":2} ["a", "b"] [1, 2] ["b", "a"] [2, 1]
+2 {"x":2,"y":1} ["x", "y"] [4, 5] ["y", "x"] [5, 4]
+3 \N \N \N \N \N
+
+-- !empty --
+\N \N \N
+
+-- !constant_input --
+{"a":2} ["a"]
+
+-- !unlimited --
+{"a":3,"x":2} ["a", "x"] [1, 4] ["b", "y"] [2, 5]
+
+-- !grouped --
+1 {"a":3,"b":2} ["a", "b"] [1, 2] ["b", "a"] [2, 1]
+2 {"x":2,"y":1} ["x", "y"] [4, 5] ["y", "x"] [5, 4]
+3 \N \N \N \N \N
+
+-- !empty --
+\N \N \N
+
+-- !constant_input --
+{"a":2} ["a"]
+
+-- !positive_and_default --
+{"a":3,"x":2} {"a":3,"x":2} [1, 4] [1, 4] [2, 5] [2, 5]
+
diff --git
a/regression-test/suites/datatype_p0/agg_state/test_agg_state_parameters.groovy
b/regression-test/suites/datatype_p0/agg_state/test_agg_state_parameters.groovy
index 88dbde8ee63..883678bcbe5 100644
---
a/regression-test/suites/datatype_p0/agg_state/test_agg_state_parameters.groovy
+++
b/regression-test/suites/datatype_p0/agg_state/test_agg_state_parameters.groovy
@@ -62,9 +62,9 @@ suite("test_agg_state_parameters") {
def ignoredCases = [
["percentile_approx_weighted", "7, 0, 0.25", "7, 1, 0.75"],
["percentile_array", "7, cast([] as array<double>)", "7, [0.25]"],
- ["topn_weighted", "1, 1, 1, 0", "1, 1, 1, 2"],
- ["topn_array", "1, 1, 0", "1, 1, 2"],
- ["topn", "'a', 1, 0", "'a', 1, 2"]
+ ["topn_weighted", "cast(NULL as int), 1, 1, 0", "nullable(cast(1 as
int)), 1, 1, 2"],
+ ["topn_array", "cast(NULL as int), 1, 0", "nullable(cast(1 as int)),
1, 2"],
+ ["topn", "cast(NULL as string), 1, 0", "nullable(cast('a' as string)),
1, 2"]
]
def cases = [
["topn", "'a', 1", "'a', 3"],
@@ -105,6 +105,12 @@ suite("test_agg_state_parameters") {
["sequence_count", "'(?1)', non_nullable(cast('2024-01-01' as
datetime)), true, false",
"'(?2)', non_nullable(cast('2024-01-01' as
datetime)), true, false"]
]
+ // Unlimited states retain samples, so they cannot merge with
finite-capacity states.
+ for (def rate : [0, -1, -2147483648]) {
+ cases.add(["topn", "'a', 1, ${rate}", "'a', 1, 2"])
+ cases.add(["topn_array", "1, 1, ${rate}", "1, 1, 2"])
+ cases.add(["topn_weighted", "1, 1, 1, ${rate}", "1, 1, 1, 2"])
+ }
// States without contributing samples ignore their parameter values.
// Keep AggState argument nullability identical across both sides of the
UNION.
for (def quantile : ["0.0", "0.25", "1.0"]) {
diff --git
a/regression-test/suites/nereids_function_p0/agg_function/topn/topn_unlimited.groovy
b/regression-test/suites/nereids_function_p0/agg_function/topn/topn_unlimited.groovy
new file mode 100644
index 00000000000..1942f1157b4
--- /dev/null
+++
b/regression-test/suites/nereids_function_p0/agg_function/topn/topn_unlimited.groovy
@@ -0,0 +1,76 @@
+// 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("topn_unlimited") {
+ sql "DROP TABLE IF EXISTS test_topn_unlimited"
+ sql """
+ CREATE TABLE test_topn_unlimited (
+ g INT,
+ s STRING,
+ v INT,
+ w BIGINT
+ ) DISTRIBUTED BY HASH(v) BUCKETS 3
+ PROPERTIES ("replication_num" = "1")
+ """
+ sql """
+ INSERT INTO test_topn_unlimited VALUES
+ (1, 'a', 1, 1), (1, 'b', 2, 10), (1, 'a', 1, 2),
+ (1, 'c', 3, 3), (1, 'a', 1, 1), (1, 'b', 2, 2),
+ (2, 'x', 4, 1), (2, 'y', 5, 5), (2, 'x', 4, 2),
+ (3, NULL, NULL, 1)
+ """
+
+ sql "SET enable_bucketed_hash_agg = false"
+ sql "SET parallel_pipeline_task_num = 1"
+ for (def phase : [1, 2]) {
+ sql "SET agg_phase = ${phase}"
+ // Non-positive rates retain every candidate, including after partial
serialization.
+ for (def rate : [0, -1, -2147483648]) {
+ order_qt_unlimited """
+ SELECT topn(s, 2, ${rate}),
+ topn_array(s, 2, ${rate}),
+ topn_array(v, 2, ${rate}),
+ topn_weighted(s, w, 2, ${rate}),
+ topn_weighted(v, w, 2, ${rate})
+ FROM test_topn_unlimited
+ """
+ order_qt_grouped """
+ SELECT g, topn(s, 2, ${rate}),
+ topn_array(s, 2, ${rate}),
+ topn_array(v, 2, ${rate}),
+ topn_weighted(s, w, 2, ${rate}),
+ topn_weighted(v, w, 2, ${rate})
+ FROM test_topn_unlimited GROUP BY g
+ """
+ order_qt_empty """
+ SELECT topn(s, 1, ${rate}), topn_array(v, 1, ${rate}),
+ topn_weighted(v, w, 1, ${rate})
+ FROM test_topn_unlimited WHERE g = 4
+ """
+ order_qt_constant_input """
+ SELECT topn(s, 1, ${rate}), topn_array(s, 1, ${rate})
+ FROM (SELECT 'a' AS s UNION ALL SELECT 'b' UNION ALL SELECT
'a') t
+ """
+ }
+ order_qt_positive_and_default """
+ SELECT topn(s, 2), topn(s, 2, 50),
+ topn_array(v, 2), topn_array(v, 2, 50),
+ topn_weighted(v, w, 2), topn_weighted(v, w, 2, 50)
+ FROM test_topn_unlimited
+ """
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]