This is an automated email from the ASF dual-hosted git repository.
mrhhsg 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 dac77e06562 [fix](be) Report sequence pattern iteration exhaustion as
an error (#69834)
dac77e06562 is described below
commit dac77e06562bdb35690b49c02475822e45dd0d43
Author: Jerry Hu <[email protected]>
AuthorDate: Thu Nov 5 14:39:26 2026 +0800
[fix](be) Report sequence pattern iteration exhaustion as an error (#69834)
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: For time-constrained `sequence_match` and
`sequence_count`, both the deterministic prefilter and the backtracking
search returned `false` when their one-million-iteration budget was
exceeded. A valid event can still appear later in the sequence, so a
completed query could incorrectly return `false`, `0`, or a partial
count. Report `INVALID_ARGUMENT` from both exhaustion paths instead of
returning an incomplete result. The existing iteration limit, comparison
boundary, and results for searches that complete within the budget are
unchanged.
### Release note
Time-constrained `sequence_match` and `sequence_count` now return an
explicit error when pattern evaluation exceeds one million iterations
instead of silently returning an incomplete result.
### Check List (For Author)
- Test:
- Unit Test: `./run-be-ut.sh --run --filter='VSequenceMatchTest.*' -j
16` (13/13 passed).
- Regression test: `test_sequence_match_iteration_limit` in
`query_p0/sql_functions/aggregate_functions`; generated expected output
and reran the suite (1/1 passed). The suite checks both SQL errors and
normal match/no-match results.
- Build and checks: ASAN BE+FE build with FE Checkstyle; BE
clang-format, check-format, build hygiene, clang-tidy, and
committed-diff checks passed.
- Behavior changed: Yes. Iteration-budget exhaustion now fails the query
with `INVALID_ARGUMENT` rather than returning an incomplete result.
- Does this need documentation: No
---
.../aggregate/aggregate_function_sequence_match.h | 20 ++++---
.../exprs/aggregate/vec_sequence_match_test.cpp | 68 ++++++++++++++++++++++
.../test_sequence_match_iteration_limit.out | 6 ++
.../test_sequence_match_iteration_limit.groovy | 61 +++++++++++++++++++
4 files changed, 146 insertions(+), 9 deletions(-)
diff --git a/be/src/exprs/aggregate/aggregate_function_sequence_match.h
b/be/src/exprs/aggregate/aggregate_function_sequence_match.h
index 0b9f82d5a7a..086cd4e7ce7 100644
--- a/be/src/exprs/aggregate/aggregate_function_sequence_match.h
+++ b/be/src/exprs/aggregate/aggregate_function_sequence_match.h
@@ -68,7 +68,7 @@ struct ComparePairFirst final {
static constexpr size_t MAX_EVENTS = 32;
-/// Max number of iterations to match the pattern against a sequence,
exception thrown when exceeded
+/// Max number of iterations to match the pattern against a sequence;
exceeding it is an error.
constexpr auto sequence_match_max_iterations = 1000000l;
template <PrimitiveType T, typename Derived>
@@ -482,10 +482,11 @@ public:
}
if (++i > sequence_match_max_iterations) {
- LOG(WARNING)
- << "Pattern application proves too difficult,
exceeding max iterations (" +
-
std::to_string(sequence_match_max_iterations) + ")";
- return false;
+ // The only matching event may follow the budget (e.g. a
million earlier events).
+ // Exhausting the search is not proof that the pattern does
not match.
+ throw Exception(ErrorCode::INVALID_ARGUMENT,
+ "sequence pattern evaluation exceeded max
iterations ({})",
+ sequence_match_max_iterations);
}
}
@@ -540,10 +541,11 @@ public:
}
if (limit_iterations && ++events_processed >
sequence_match_max_iterations) {
- LOG(WARNING) << "Pattern application proves too difficult,
exceeding max "
- "iterations are " +
-
std::to_string(sequence_match_max_iterations);
- return false;
+ // A deterministic event may still occur after the
inspected prefix.
+ // Report the exhausted budget instead of treating it as a
missing event.
+ throw Exception(ErrorCode::INVALID_ARGUMENT,
+ "sequence pattern evaluation exceeded max
iterations ({})",
+ sequence_match_max_iterations);
}
}
diff --git a/be/test/exprs/aggregate/vec_sequence_match_test.cpp
b/be/test/exprs/aggregate/vec_sequence_match_test.cpp
index 5a8a35f422a..8908dc8d51c 100644
--- a/be/test/exprs/aggregate/vec_sequence_match_test.cpp
+++ b/be/test/exprs/aggregate/vec_sequence_match_test.cpp
@@ -19,6 +19,7 @@
#include <gtest/gtest-test-part.h>
#include <memory>
+#include <string>
#include <vector>
#include "core/column/column_nullable.h"
@@ -94,6 +95,42 @@ public:
agg_function->destroy(place);
}
+ void assert_time_condition_result(const AggregateFunctionPtr& agg_function,
+ const IColumn* columns[], int
event1_count) {
+ std::unique_ptr<char[]> memory(new char[agg_function->size_of_data()]);
+ AggregateDataPtr place = memory.get();
+ agg_function->create(place);
+ for (int i = 0; i < event1_count; ++i) {
+ agg_function->add(place, columns, 0, arena);
+ }
+ agg_function->add(place, columns, 1, arena);
+
+ bool limit_exceeded = false;
+ try {
+ if (agg_function == agg_function_sequence_match) {
+ ColumnUInt8 result;
+ agg_function->insert_result_into(place, result);
+ if (event1_count < 999998) {
+ EXPECT_EQ(result.get_data()[0], event1_count == 0 ? 0 : 1)
+ << "event1_count=" << event1_count;
+ }
+ } else {
+ ColumnInt64 result;
+ agg_function->insert_result_into(place, result);
+ if (event1_count < 999998) {
+ EXPECT_EQ(result.get_data()[0], event1_count == 0 ? 0 : 1)
+ << "event1_count=" << event1_count;
+ }
+ }
+ } catch (const Exception& e) {
+ limit_exceeded = true;
+ EXPECT_EQ(e.code(), ErrorCode::INVALID_ARGUMENT);
+ EXPECT_NE(e.message().find("exceeded max iterations (1000000)"),
std::string::npos);
+ }
+ EXPECT_EQ(limit_exceeded, event1_count >= 999998) << "event1_count="
<< event1_count;
+ agg_function->destroy(place);
+ }
+
Arena arena;
};
@@ -663,4 +700,35 @@ TEST_F(VSequenceMatchTest,
testMatchRejectsInvalidPatternRegardlessOfNullTimesta
}
}
+TEST_F(VSequenceMatchTest, testTimeConditionIterationLimitReportsError) {
+ auto column_pattern = ColumnString::create();
+
column_pattern->insert(Field::create_field<TYPE_STRING>("(?1)(?t>=1)(?2)"));
+
+ auto column_timestamp = ColumnDateTimeV2::create();
+ for (int second = 0; second < 2; ++second) {
+ DateV2Value<DateTimeV2ValueType> time_value;
+ time_value.unchecked_set_time(2026, 1, 1, 0, 0, second);
+ column_timestamp->insert_data((char*)&time_value, 0);
+ }
+
+ auto column_event1 = ColumnUInt8::create();
+ column_event1->insert_value(1);
+ column_event1->insert_value(0);
+ auto column_event2 = ColumnUInt8::create();
+ column_event2->insert_value(0);
+ column_event2->insert_value(1);
+ auto column_event3 = ColumnUInt8::create();
+ column_event3->insert_value(0);
+ column_event3->insert_value(0);
+
+ const IColumn* columns[] = {column_pattern.get(), column_timestamp.get(),
column_event1.get(),
+ column_event2.get(), column_event3.get()};
+ for (int event1_count : {0, 1000, 999997, 999998, 1000001}) {
+ for (const auto& agg_function :
+ {agg_function_sequence_match, agg_function_sequence_count}) {
+ assert_time_condition_result(agg_function, columns, event1_count);
+ }
+ }
+}
+
} // namespace doris
diff --git
a/regression-test/data/query_p0/sql_functions/aggregate_functions/test_sequence_match_iteration_limit.out
b/regression-test/data/query_p0/sql_functions/aggregate_functions/test_sequence_match_iteration_limit.out
new file mode 100644
index 00000000000..75dff20dfe6
--- /dev/null
+++
b/regression-test/data/query_p0/sql_functions/aggregate_functions/test_sequence_match_iteration_limit.out
@@ -0,0 +1,6 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !small_match --
+true 1
+
+-- !small_no_match --
+false 0
diff --git
a/regression-test/suites/query_p0/sql_functions/aggregate_functions/test_sequence_match_iteration_limit.groovy
b/regression-test/suites/query_p0/sql_functions/aggregate_functions/test_sequence_match_iteration_limit.groovy
new file mode 100644
index 00000000000..2bfab137d96
--- /dev/null
+++
b/regression-test/suites/query_p0/sql_functions/aggregate_functions/test_sequence_match_iteration_limit.groovy
@@ -0,0 +1,61 @@
+// 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_sequence_match_iteration_limit") {
+ sql "SET query_timeout = 120"
+ sql "DROP TABLE IF EXISTS sequence_match_iteration_limit_test"
+ sql """
+ CREATE TABLE sequence_match_iteration_limit_test (
+ id BIGINT NOT NULL,
+ ts DATETIMEV2(0) NOT NULL,
+ evt TINYINT NOT NULL
+ ) DUPLICATE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES ("replication_num" = "1")
+ """
+ sql """
+ INSERT INTO sequence_match_iteration_limit_test
+ SELECT number,
+ IF(number < 1000001, CAST('2026-01-01 00:00:00' AS
DATETIMEV2(0)),
+ CAST('2026-01-01 00:00:01' AS
DATETIMEV2(0))),
+ CAST(IF(number < 1000001, 1, 2) AS TINYINT)
+ FROM numbers('number' = '1000002')
+ """
+
+ // A single early event 1 and the final event 2 form one valid match.
+ qt_small_match """
+ SELECT sequence_match('(?1)(?t>=1)(?2)', ts, evt = 1, evt = 2),
+ sequence_count('(?1)(?t>=1)(?2)', ts, evt = 1, evt = 2)
+ FROM sequence_match_iteration_limit_test WHERE id IN (0, 1000001)
+ """
+ qt_small_no_match """
+ SELECT sequence_match('(?1)(?t>=2)(?2)', ts, evt = 1, evt = 2),
+ sequence_count('(?1)(?t>=2)(?2)', ts, evt = 1, evt = 2)
+ FROM sequence_match_iteration_limit_test WHERE id IN (0, 1000001)
+ """
+
+ // The final event must not be reported as absent when the search budget
is exhausted.
+ test {
+ sql """SELECT sequence_match('(?1)(?t>=1)(?2)', ts, evt = 1, evt = 2)
+ FROM sequence_match_iteration_limit_test"""
+ exception "exceeded max iterations"
+ }
+ test {
+ sql """SELECT sequence_count('(?1)(?t>=1)(?2)', ts, evt = 1, evt = 2)
+ FROM sequence_match_iteration_limit_test"""
+ exception "exceeded max iterations"
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]