Skip to content

Commit d4e78d5

Browse files
mergify[bot]Copilotstdpain
authored
[BugFix] Fix regexp_replace hyperscan_vec processing multiple rows incorrectly (backport #67380) (#67470)
Signed-off-by: stdpain <34912776+stdpain@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: stdpain <34912776+stdpain@users.noreply.github.com>
1 parent b1df511 commit d4e78d5

5 files changed

Lines changed: 139 additions & 26 deletions

File tree

be/src/exprs/string_functions.cpp

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
namespace starrocks {
5555
// A regex to match any regex pattern is equivalent to a substring search.
5656
static const RE2 SUBSTRING_RE(R"((?:\.\*)*([^\.\^\{\[\(\|\)\]\}\+\*\?\$\\]+)(?:\.\*)*)", re2::RE2::Quiet);
57+
// A strict fixed literal regex contains only normal characters without any regex special characters.
58+
static const RE2 FIXED_LITERAL_RE(R"(^[^\.\^\{\[\(\|\)\]\}\+\*\?\$\\]+$)", re2::RE2::Quiet);
5759

5860
#define THROW_RUNTIME_ERROR_IF_EXCEED_LIMIT(col, func_name) \
5961
if (UNLIKELY(col->capacity_limit_reached())) { \
@@ -3114,8 +3116,10 @@ Status StringFunctions::regexp_replace_prepare(FunctionContext* context, Functio
31143116
state->pattern = pattern_str;
31153117

31163118
std::string search_string;
3119+
31173120
if (pattern_str.size() && RE2::FullMatch(pattern_str, SUBSTRING_RE, &search_string)) {
31183121
state->use_hyperscan = true;
3122+
state->use_hyperscan_vec = RE2::FullMatch(pattern_str, FIXED_LITERAL_RE, &search_string);
31193123
state->size_of_pattern = pattern.size;
31203124
std::string re_pattern(pattern.data, pattern.size);
31213125
RETURN_IF_ERROR(hs_compile_and_alloc_scratch(re_pattern, state, context, pattern));
@@ -3586,35 +3590,36 @@ static StatusOr<ColumnPtr> hyperscan_vec_evaluate(const BinaryColumn* src, Strin
35863590
raw::make_room(&dst_offsets, num_rows + 1);
35873591
dst_bytes.reserve(data_count());
35883592

3589-
// copy data
3593+
// copy data row by row with replacements applied
35903594
char* cursor = reinterpret_cast<char*>(dst_bytes.data());
3591-
size_t last_to = 0;
3592-
for (const auto& info : match_info_chain_in_one_row.info_chain) {
3593-
strings::memcpy_inlined(cursor, data + last_to, info.from - last_to);
3594-
cursor += info.from - last_to;
3595-
strings::memcpy_inlined(cursor, rpl_value.data(), rpl_value.size());
3596-
cursor += rpl_value.size();
3597-
last_to = info.to;
3598-
}
3599-
strings::memcpy_inlined(cursor, data + last_to, src_value_size - last_to);
3600-
3601-
// split offset
36023595
size_t match_index = 0;
3603-
dst_offsets[0] = 0;
36043596
size_t match_size = match_info_chain_in_one_row.info_chain.size();
3597+
dst_offsets[0] = 0;
3598+
36053599
for (size_t i = 0; i < num_rows; i++) {
3606-
size_t from = src_offsets[i];
3607-
size_t to = src_offsets[i + 1];
3608-
size_t dis = to - from;
3609-
DCHECK(match_index == match_size || match_info_chain_in_one_row.info_chain[match_index].to > from);
3610-
while (match_index < match_size && match_info_chain_in_one_row.info_chain[match_index].from >= from &&
3611-
match_info_chain_in_one_row.info_chain[match_index].to <= to) {
3612-
dis -= match_info_chain_in_one_row.info_chain[match_index].to -
3613-
match_info_chain_in_one_row.info_chain[match_index].from;
3614-
dis += rpl_value.size();
3600+
size_t row_start = src_offsets[i];
3601+
size_t row_end = src_offsets[i + 1];
3602+
size_t last_to = row_start;
3603+
3604+
// Process all matches in this row
3605+
while (match_index < match_size && match_info_chain_in_one_row.info_chain[match_index].from >= row_start &&
3606+
match_info_chain_in_one_row.info_chain[match_index].to <= row_end) {
3607+
const auto& info = match_info_chain_in_one_row.info_chain[match_index];
3608+
// Copy data before the match
3609+
strings::memcpy_inlined(cursor, data + last_to, info.from - last_to);
3610+
cursor += info.from - last_to;
3611+
// Copy replacement
3612+
strings::memcpy_inlined(cursor, rpl_value.data(), rpl_value.size());
3613+
cursor += rpl_value.size();
3614+
last_to = info.to;
36153615
match_index++;
36163616
}
3617-
dst_offsets[i + 1] = dst_offsets[i] + dis;
3617+
// Copy remaining data in this row
3618+
strings::memcpy_inlined(cursor, data + last_to, row_end - last_to);
3619+
cursor += row_end - last_to;
3620+
3621+
// Calculate offset for this row
3622+
dst_offsets[i + 1] = cursor - reinterpret_cast<char*>(dst_bytes.data());
36183623
}
36193624
DCHECK(match_index == match_size);
36203625
DCHECK(dst_offsets.back() == data_count());
@@ -3723,7 +3728,7 @@ StatusOr<ColumnPtr> StringFunctions::regexp_replace(FunctionContext* context, co
37233728

37243729
if (state->const_pattern) {
37253730
if (state->use_hyperscan) {
3726-
if (columns[2]->is_constant() && context->state()->enable_hyperscan_vec()) {
3731+
if (columns[2]->is_constant() && context->state()->enable_hyperscan_vec() && state->use_hyperscan_vec) {
37273732
return regexp_replace_use_hyperscan_vec(state, columns);
37283733
} else {
37293734
return regexp_replace_use_hyperscan(state, columns);

be/src/exprs/string_functions.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ struct StringFunctionsState {
6262
DriverMap driver_regex_map; // regex for each pipeline_driver, to make it driver-local
6363

6464
bool use_hyperscan = false;
65+
bool use_hyperscan_vec = false;
66+
6567
int size_of_pattern = -1;
6668

6769
// a pointer to the generated database that responsible for parsed expression.

be/test/exprs/string_fn_regexp_replace_test.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include "common/statusor.h"
2626
#include "exprs/exprs_test_helper.h"
2727
#include "exprs/string_functions.h"
28+
#include "util/defer_op.h"
2829

2930
namespace starrocks {
3031

@@ -63,6 +64,7 @@ class StringFunctionRegexpReplaceTest : public ::testing::Test {
6364
std::string pattern = "-";
6465
_state->pattern = pattern;
6566
_state->use_hyperscan = true;
67+
_state->use_hyperscan_vec = true;
6668
_state->size_of_pattern = int(pattern.size());
6769
if (hs_compile(pattern.c_str(), HS_FLAG_ALLOWEMPTY | HS_FLAG_DOTALL | HS_FLAG_UTF8 | HS_FLAG_SOM_LEFTMOST,
6870
HS_MODE_BLOCK, nullptr, &_state->database, &_state->compile_err) != HS_SUCCESS) {
@@ -109,4 +111,4 @@ TEST_F(StringFunctionRegexpReplaceTest, testHyperscanVec) {
109111
}
110112
}
111113

112-
} // namespace starrocks
114+
} // namespace starrocks

test/sql/test_function/R/test_regex

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,63 @@ SELECT regexp_extract_all(str, regex, 3) from tsr;
170170
-- result:
171171
[]
172172
[]
173-
-- !result
173+
-- !result
174+
-- name: test_regexp_replace_hyperscan_vec_multiple_rows
175+
CREATE TABLE `test_regexp_replace_multirow` (
176+
`column_value` varchar(100) NULL COMMENT ""
177+
) ENGINE=OLAP
178+
DUPLICATE KEY(`column_value`)
179+
COMMENT "OLAP"
180+
DISTRIBUTED BY HASH(`column_value`) BUCKETS 1 PROPERTIES ("replication_num" = "1");
181+
-- result:
182+
-- !result
183+
insert into test_regexp_replace_multirow values
184+
('activity_60_package_2'),
185+
('activity_50_package_7'),
186+
('activity_70_package_1'),
187+
('nomatch_value'),
188+
('test_package_suffix');
189+
-- result:
190+
-- !result
191+
SELECT regexp_replace(column_value, '_package_.*', '')
192+
FROM (VALUES
193+
('activity_60_package_2'),
194+
('activity_50_package_7')
195+
) AS t(column_value);
196+
-- result:
197+
activity_60
198+
activity_50
199+
-- !result
200+
SELECT regexp_replace(column_value, '_package_.*', '')
201+
FROM test_regexp_replace_multirow
202+
ORDER BY column_value;
203+
-- result:
204+
activity_50
205+
activity_60
206+
activity_70
207+
nomatch_value
208+
test
209+
-- !result
210+
SELECT regexp_replace(column_value, '_[0-9]+', '_X')
211+
FROM test_regexp_replace_multirow
212+
ORDER BY column_value;
213+
-- result:
214+
activity_X_package_X
215+
activity_X_package_X
216+
activity_X_package_X
217+
nomatch_value
218+
test_package_suffix
219+
-- !result
220+
SELECT regexp_replace(column_value, '_package_', '_pkg_')
221+
FROM test_regexp_replace_multirow
222+
ORDER BY column_value;
223+
-- result:
224+
activity_50_pkg_7
225+
activity_60_pkg_2
226+
activity_70_pkg_1
227+
nomatch_value
228+
test_pkg_suffix
229+
-- !result
230+
DROP TABLE test_regexp_replace_multirow;
231+
-- result:
232+
-- !result

test/sql/test_function/T/test_regex

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,48 @@ SELECT regexp_extract_all("AbCdExCeF", "([[:lower:]]+)C([[:lower:]]+)", 3);
6262
SELECT regexp_extract_all(str, "([[:lower:]]+)C([[:lower:]]+)", 3) from tsr;
6363
SELECT regexp_extract_all(str, regex, 3) from tsr;
6464

65+
-- name: test_regexp_replace_hyperscan_vec_multiple_rows
66+
67+
-- Test for issue #65936: regexp_replace with enable_hyperscan_vec=true
68+
-- produces incorrect output when processing multiple rows
69+
-- This test ensures row boundaries are respected during data reconstruction
70+
71+
CREATE TABLE `test_regexp_replace_multirow` (
72+
`column_value` varchar(100) NULL COMMENT ""
73+
) ENGINE=OLAP
74+
DUPLICATE KEY(`column_value`)
75+
COMMENT "OLAP"
76+
DISTRIBUTED BY HASH(`column_value`) BUCKETS 1 PROPERTIES ("replication_num" = "1");
77+
78+
-- Insert test data with pattern that should be replaced
79+
insert into test_regexp_replace_multirow values
80+
('activity_60_package_2'),
81+
('activity_50_package_7'),
82+
('activity_70_package_1'),
83+
('nomatch_value'),
84+
('test_package_suffix');
85+
86+
-- Test with VALUES clause (original bug report scenario)
87+
SELECT regexp_replace(column_value, '_package_.*', '')
88+
FROM (VALUES
89+
('activity_60_package_2'),
90+
('activity_50_package_7')
91+
) AS t(column_value);
92+
93+
-- Test with table data
94+
SELECT regexp_replace(column_value, '_package_.*', '')
95+
FROM test_regexp_replace_multirow
96+
ORDER BY column_value;
97+
98+
-- Test with different patterns to ensure general correctness
99+
SELECT regexp_replace(column_value, '_[0-9]+', '_X')
100+
FROM test_regexp_replace_multirow
101+
ORDER BY column_value;
102+
103+
-- Test with non-empty replacement
104+
SELECT regexp_replace(column_value, '_package_', '_pkg_')
105+
FROM test_regexp_replace_multirow
106+
ORDER BY column_value;
107+
108+
-- Clean up
109+
DROP TABLE test_regexp_replace_multirow;

0 commit comments

Comments
 (0)