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 2d76b67ff86 [branch-4.2][fix](function) Avoid duplicate zero-width 
regexp_extract_all matches (#70244)
2d76b67ff86 is described below

commit 2d76b67ff86e603f29927e54cd7accb4d3fd4dc8
Author: Jerry Hu <[email protected]>
AuthorDate: Fri Nov 6 11:17:57 2026 +0800

    [branch-4.2][fix](function) Avoid duplicate zero-width regexp_extract_all 
matches (#70244)
    
    ## Summary
    
    With extended regular expressions enabled, restarting Boost.Regex
    searches on a shortened input can repeat a zero-width match and change
    the context seen by lookbehind and anchors. Iterate over the complete
    input with `boost::cregex_iterator` instead, while retaining the
    existing capture-group behavior and leaving the RE2 path unchanged. This
    backports the fix from #68358.
    
    ## Tests
    
    - `./build.sh --be --fe -j 16` — passed.
    - `BUILD_TYPE=ASAN DISABLE_BE_JAVA_EXTENSIONS=OFF ./build.sh --be -j 32`
    — passed.
    - `./run-be-ut.sh --run
    --filter=FunctionLikeTest.regexp_extract_all_boost_zero_width_match -j
    32` — passed (1 test).
    - Local SQL regression: `query_p0/load` — passed (8 suites);
    `query_p0/sql_functions/string_functions/test_string_function_regexp` —
    passed (1 suite).
    - `build-support/clang-format.sh`, `build-support/check-format.sh`, and
    committed diff whitespace check — passed.
---
 be/src/exprs/function/function_regexp.cpp          | 21 +++----
 be/test/exprs/function/function_like_test.cpp      | 64 ++++++++++++++++++++++
 .../test_string_function_regexp.out                | 18 ++++++
 .../test_string_function_regexp.groovy             |  7 +++
 4 files changed, 97 insertions(+), 13 deletions(-)

diff --git a/be/src/exprs/function/function_regexp.cpp 
b/be/src/exprs/function/function_regexp.cpp
index 257209bbe6a..53147a63a1a 100644
--- a/be/src/exprs/function/function_regexp.cpp
+++ b/be/src/exprs/function/function_regexp.cpp
@@ -169,22 +169,17 @@ struct RegexpExtractEngine {
                 pos += offset + matches[0].size();
             }
         } else if (is_boost()) {
-            const char* search_start = data;
-            const char* search_end = data + size;
-            boost::match_results<const char*> matches;
-
-            while (boost::regex_search(search_start, search_end, matches, 
*boost_regex)) {
+            // Iterate over the whole input instead of re-running regex_search 
on a
+            // shrinking range: the iterator keeps the preceding text visible 
to
+            // lookbehind/`^`/`\b` and steps past zero-width matches (e.g. 
lookahead)
+            // without matching the same position twice.
+            boost::cregex_iterator it(data, data + size, *boost_regex);
+            boost::cregex_iterator end;
+            for (; it != end; ++it) {
+                const auto& matches = *it;
                 if (matches.size() > 1 && matches[1].matched) {
                     results.emplace_back(matches[1].str());
                 }
-                if (matches[0].length() == 0) {
-                    if (search_start == search_end) {
-                        break;
-                    }
-                    search_start += 1;
-                } else {
-                    search_start = matches[0].second;
-                }
             }
         }
     }
diff --git a/be/test/exprs/function/function_like_test.cpp 
b/be/test/exprs/function/function_like_test.cpp
index 9430882e4a3..ee3f0869710 100644
--- a/be/test/exprs/function/function_like_test.cpp
+++ b/be/test/exprs/function/function_like_test.cpp
@@ -15,6 +15,8 @@
 // specific language governing permissions and limitations
 // under the License.
 
+#include <gen_cpp/PaloInternalService_types.h>
+
 #include <cstdint>
 #include <memory>
 #include <string>
@@ -584,6 +586,68 @@ TEST(FunctionLikeTest, regexp_extract_all) {
     }
 }
 
+// Extended regex falls back to Boost for lookaround patterns. The iterator 
must
+// advance through zero-width matches without losing the original input 
context.
+TEST(FunctionLikeTest, regexp_extract_all_boost_zero_width_match) {
+    auto str_type = std::make_shared<DataTypeString>();
+    auto return_type = make_nullable(std::make_shared<DataTypeString>());
+
+    auto run_case = [&](const std::string& str, const std::string& pattern,
+                        const std::string& expected) {
+        auto col_str = ColumnString::create();
+        col_str->insert_data(str.data(), str.size());
+        auto col_pattern = ColumnString::create();
+        col_pattern->insert_data(pattern.data(), pattern.size());
+
+        Block block;
+        block.insert({std::move(col_str), str_type, "str"});
+        block.insert({ColumnConst::create(std::move(col_pattern), 1), 
str_type, "pattern"});
+        block.insert({nullptr, return_type, "result"});
+
+        ColumnsWithTypeAndName arg_cols = {block.get_by_position(0), 
block.get_by_position(1)};
+        auto func = 
SimpleFunctionFactory::instance().get_function("regexp_extract_all", arg_cols,
+                                                                   
return_type);
+        ASSERT_TRUE(func != nullptr);
+
+        std::vector<DataTypePtr> arg_types = {str_type, str_type};
+        FunctionUtils fn_utils({}, arg_types, false);
+        auto* fn_ctx = fn_utils.get_fn_ctx();
+        TQueryOptions query_options = fn_ctx->state()->query_options();
+        query_options.__set_enable_extended_regex(true);
+        fn_ctx->state()->set_query_options(query_options);
+        fn_ctx->set_constant_cols(
+                {nullptr, 
std::make_shared<ColumnPtrWrapper>(block.get_by_position(1).column)});
+
+        ASSERT_EQ(Status::OK(), func->open(fn_ctx, 
FunctionContext::FRAGMENT_LOCAL));
+        ASSERT_EQ(Status::OK(), func->open(fn_ctx, 
FunctionContext::THREAD_LOCAL));
+        ASSERT_EQ(Status::OK(), func->execute(fn_ctx, block, {0, 1}, 2, 1));
+
+        auto result_col = block.get_by_position(2).column;
+        ASSERT_TRUE(result_col.get() != nullptr);
+        ASSERT_FALSE(result_col->is_null_at(0));
+        EXPECT_EQ(expected, return_type->to_string(*result_col, 0))
+                << "input: '" << str << "', pattern: '" << pattern << "'";
+
+        static_cast<void>(func->close(fn_ctx, FunctionContext::THREAD_LOCAL));
+        static_cast<void>(func->close(fn_ctx, 
FunctionContext::FRAGMENT_LOCAL));
+    };
+
+    // The lookahead matches once, at the last position; it must not be 
extracted twice.
+    run_case("ba", "(?=(a))", "['a']");
+    // Zero-width matches at distinct positions are all still found, exactly 
once each.
+    run_case("aba", "(?=(a))", "['a','a']");
+    run_case("aaa", "(?=(a))", "['a','a','a']");
+    // Non-zero-width Boost matches keep advancing past the whole match.
+    run_case("foo123bar456baz", "(\\d{3})(?=bar|baz)", "['123','456']");
+    // The text before a zero-width match stays visible to lookbehind at the 
next position.
+    run_case("aa", "(?<=(a))", "['a','a']");
+    // After an empty match, a consuming match at the same position is still 
tried.
+    run_case("xa", "((?<=x)|a)", "['','a']");
+    // Resuming after a zero-width match must not let `^` anchor in the middle 
of the input
+    // (the string form renders "no match" as an empty string).
+    run_case("xxa?b", "(?<=a)|^(b)", "");
+}
+
 TEST(FunctionLikeTest, regexp_replace) {
     std::string func_name = "regexp_replace";
 
diff --git 
a/regression-test/data/query_p0/sql_functions/string_functions/test_string_function_regexp.out
 
b/regression-test/data/query_p0/sql_functions/string_functions/test_string_function_regexp.out
index 5b21c2a1bcc..e5427af7ccd 100644
--- 
a/regression-test/data/query_p0/sql_functions/string_functions/test_string_function_regexp.out
+++ 
b/regression-test/data/query_p0/sql_functions/string_functions/test_string_function_regexp.out
@@ -247,6 +247,24 @@ aXb
 -- !sql_regexp_extract_all_5 --
 ['Case1','Case2','Case3']
 
+-- !sql_regexp_extract_all_zero_width_1 --
+['a']
+
+-- !sql_regexp_extract_all_zero_width_2 --
+['a','a']
+
+-- !sql_regexp_extract_all_zero_width_3 --
+['a','a','a']
+
+-- !sql_regexp_extract_all_zero_width_4 --
+['a','a']
+
+-- !sql_regexp_extract_all_zero_width_5 --
+['','a']
+
+-- !sql_regexp_extract_all_zero_width_6 --
+
+
 -- !sql_regexp_extract_all_6 --
 ['foo\nbar']
 
diff --git 
a/regression-test/suites/query_p0/sql_functions/string_functions/test_string_function_regexp.groovy
 
b/regression-test/suites/query_p0/sql_functions/string_functions/test_string_function_regexp.groovy
index c5163875b58..3739fba44e6 100644
--- 
a/regression-test/suites/query_p0/sql_functions/string_functions/test_string_function_regexp.groovy
+++ 
b/regression-test/suites/query_p0/sql_functions/string_functions/test_string_function_regexp.groovy
@@ -120,6 +120,13 @@ suite("test_string_function_regexp") {
     qt_sql_regexp_extract_all_3 'SELECT 
REGEXP_EXTRACT_ALL(\'foo123bar456baz\', \'(\\\\d{3})(?=bar|baz)\');'
     qt_sql_regexp_extract_all_4 'SELECT 
REGEXP_EXTRACT_ALL(\'ID:AA-1,ID:BB-2,ID:CC-3\', \'(?<=ID:)([A-Z]{2}-\\\\d)\');'
     qt_sql_regexp_extract_all_5 'SELECT 
REGEXP_EXTRACT_ALL(\'EdgeCase1EdgeCase2EdgeCase3\', 
\'(?<=Edge)(Case\\\\d)(?=Edge|$)\');'
+    // zero-width Boost matches must be extracted once per position
+    qt_sql_regexp_extract_all_zero_width_1 "SELECT REGEXP_EXTRACT_ALL('ba', 
'(?=(a))');"
+    qt_sql_regexp_extract_all_zero_width_2 "SELECT REGEXP_EXTRACT_ALL('aba', 
'(?=(a))');"
+    qt_sql_regexp_extract_all_zero_width_3 "SELECT REGEXP_EXTRACT_ALL('aaa', 
'(?=(a))');"
+    qt_sql_regexp_extract_all_zero_width_4 "SELECT REGEXP_EXTRACT_ALL('aa', 
'(?<=(a))');"
+    qt_sql_regexp_extract_all_zero_width_5 "SELECT REGEXP_EXTRACT_ALL('xa', 
'((?<=x)|a)');"
+    qt_sql_regexp_extract_all_zero_width_6 "SELECT REGEXP_EXTRACT_ALL('xxa?b', 
'(?<=a)|^(b)');"
     sql "set enable_extended_regex = false;"
 
     qt_sql_regexp_extract_all_6 "SELECT REGEXP_EXTRACT_ALL(concat('foo', 
char(10), 'bar'), '(foo.bar)');"


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

Reply via email to