Skip to content

[Feature](func) Support the third argument for regexp_extract_all and regexp_extract_all_array - #66050

Open
ccl125 wants to merge 9 commits into
apache:masterfrom
ccl125:feat/regexp-extract-all-group-index
Open

[Feature](func) Support the third argument for regexp_extract_all and regexp_extract_all_array#66050
ccl125 wants to merge 9 commits into
apache:masterfrom
ccl125:feat/regexp-extract-all-group-index

Conversation

@ccl125

@ccl125 ccl125 commented Jul 25, 2026

Copy link
Copy Markdown

What problem does this PR solve?

Issue Number: related #48203 (Spark: regexp_extract_all for the third argument)

Problem Summary:

regexp_extract_all / regexp_extract_all_array only accepted 2 arguments and always extracted the first capturing group. This PR adds the optional third group argument, following Spark semantics:

  • regexp_extract_all(str, pattern) — unchanged, extracts group 1
  • regexp_extract_all(str, pattern, 0) — extracts the whole match (also works for patterns without capturing groups)
  • regexp_extract_all(str, pattern, N) — extracts capturing group N
  • group index outside [0, number_of_capturing_groups] → error (same as Spark); non-participating groups yield empty strings

Implementation:

  • FE: RegexpExtractAll / RegexpExtractAllArray gain 3-arg signatures; the 2-arg form stays a 2-child node (no default padding) so old-FE/new-BE combinations keep working during rolling upgrades
  • BE: both arities are registered as separate variadic forms (mirroring regexp_replace's ThreeParamTypes/FourParamTypes); RegexpExtractEngine::match_all_and_extract takes a group index; illegal indexes (outside [0, number_of_capturing_groups]) raise INVALID_ARGUMENT, and non-participating groups contribute empty strings — both consistent with Spark

Release note

Support the third argument (group index) for regexp_extract_all and regexp_extract_all_array.

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • Manual test (add detailed scripts or steps below)
    • No need to test or manual test. Explain why:

    New BE unit cases in function_like_test.cpp cover group 0 / other groups / out-of-range / negative index for both functions; existing 2-arg cases were ported to the normalized 3-arg form with unchanged expectations. New regression cases added to test_string_function_regexp.

  • Behavior changed:

    • No.
    • Yes.

    2-argument calls keep the exact previous behavior (group 1 extraction, empty result when the pattern has no capturing group).

  • Does this need documentation?

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

… regexp_extract_all_array

Add an optional group-index argument (Spark semantics, default 1) to
regexp_extract_all and regexp_extract_all_array. Index 0 extracts the
whole match, a positive index extracts the corresponding capturing
group, an out-of-range index yields an empty result, and a negative
index yields NULL. Two-argument calls are normalized in the FE by
padding the default index, keeping existing behavior unchanged.
@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@Override
public RegexpExtractAll withChildren(List<Expression> children) {
Preconditions.checkArgument(children.size() == 2);
Preconditions.checkArgument(children.size() == 2 || children.size() == 3);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Preconditions.checkArgument(children.size() == 2 || children.size() == 3);
Preconditions.checkArgument(children.size() == 3);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review! Fixed in aa701aa.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: the design changed based on the rolling-upgrade feedback below — the FE no longer pads a default index, so a node can legitimately have 2 or 3 children and the assertion stays 2 || 3. The BE now accepts both arities.

@Override
public RegexpExtractAllArray withChildren(List<Expression> children) {
Preconditions.checkArgument(children.size() == 2);
Preconditions.checkArgument(children.size() == 2 || children.size() == 3);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Preconditions.checkArgument(children.size() == 2 || children.size() == 3);
Preconditions.checkArgument(children.size() == 3);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review! Fixed in aa701aa.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above — reverted to 2 || 3 since the FE no longer normalizes to 3 arguments.

The 2-arg constructor always pads the default group index, so every
regexp_extract_all / regexp_extract_all_array node physically has 3
children. Tighten the withChildren assertion accordingly.
FunctionSignature.ret(ArrayType.of(StringType.INSTANCE))
.args(StringType.INSTANCE, StringType.INSTANCE)
.args(StringType.INSTANCE, StringType.INSTANCE),
FunctionSignature.ret(ArrayType.of(VarcharType.SYSTEM_DEFAULT))

@linrrzqqq linrrzqqq Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better not to change the parameter limit from two to a hard limit of three. I think support variable parameters is better.

Doris's version upgrades are generally rolling upgrades, first upgrading the backend (be) and then the frontend (fe).An old FE sends two arguments to a new BE, while an old FE pads two-argument SQL calls to three arguments and therefore cannot execute them on an new BE.

Could we keep the original two-argument expression shape and make the new BE accept both 2 and 3 arguments? When only two arguments are provided, the BE can simply use group index 1 as the default. This would preserve existing two-argument queries during a rolling upgrade

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks! Reworked in d387667: the FE no longer pads, and the BE now registers both a 2-arg and a 3-arg variadic form (mirroring regexp_replace's ThreeParamTypes/FourParamTypes), so an old FE keeps working against a new BE during rolling upgrades.

int max_matches = 1 + re2_regex->NumberOfCapturingGroups();
if (max_matches < 2) {
return; // No capturing groups
if (index >= max_matches) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behavior for idx >= max_matches || index < 0 better refer to Spark throwing an error.

spark-sql (default)> SELECT regexp_extract_all('100-200, 300-400', '(\\d+)-(\\d+)', -1); 
[INVALID_PARAMETER_VALUE.REGEX_GROUP_INDEX] The value of parameter(s) `idx` in `regexp_extract_all` is invalid: Expects group index between 0 and 2, but got -1. SQLSTATE: 22023

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — the index contract now matches Spark: an index outside [0, number_of_capturing_groups] fails with InvalidArgument, with the valid range in the message.

if (matches.size() > 1 && !matches[1].empty()) {
results.emplace_back(matches[1].data(), matches[1].size());
// Extract the capturing group with the given index
if (static_cast<size_t>(index) < matches.size() && !matches[index].empty()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should not judge matches[idx].empty() anymore:

spark-sql (default)> select regexp_extract_all('a b', '(a)|(b)', 2);
["","b"]

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — non-participating groups now contribute empty strings instead of being skipped (your (a)|(b) group-2 example is covered in the unit tests).

while (boost::regex_search(search_start, search_end, matches, *boost_regex)) {
if (matches.size() > 1 && matches[1].matched) {
results.emplace_back(matches[1].str());
if (static_cast<size_t>(index) < matches.size() && matches[index].matched) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, both the re2 and the boost path keep empty groups now.

qt_sql_regexp_extract_all_group_negative "select regexp_extract_all('abc', '(b)', -1);"
qt_sql_regexp_extract_all_array_group0 "select regexp_extract_all_array('x=a3&x=18abc&x=2&y=3&x=4&x=17bcd', 'x=([0-9]+)([a-z]+)', 0);"
qt_sql_regexp_extract_all_array_group2 "select regexp_extract_all_array('x=a3&x=18abc&x=2&y=3&x=4&x=17bcd', 'x=([0-9]+)([a-z]+)', 2);"
qt_sql_regexp_extract_all_array_group_out_of_range "select regexp_extract_all_array('hitdecisiondlist', '(i)(.*?)(e)', 3);"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add more comprehensive tests, covering more cases, including but not limited to: including column input, including null, illegal index...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, why this case named after _out_of_range?

I think (i)(.*?)(e) has three capturing groups, so index 3 is valid and should extract "e". The first out-of-range index is 4.

seems to be some issue in ur impl:

spark-sql (default)> select regexp_extract_all('hitdecisiondlist', '(i)(.*?)(e)', 3);
["e"]

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanded: column input (three group indexes over the test table), NULL literals for string/pattern/index, illegal indexes via exception blocks, and the non-participating-group case.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right, I miscounted — (i)(.*?)(e) has three capturing groups and index 3 extracts "e". Fixed the unit and regression cases; index 4 is now covered as the out-of-range error.

@linrrzqqq linrrzqqq self-assigned this Jul 30, 2026
@linrrzqqq

Copy link
Copy Markdown
Collaborator

also remember to edit doc: https://github.com/apache/doris-website

dev & 4.x versions of zh & en

…dex semantics

- Support both 2-arg and 3-arg forms at the BE (separate variadic
  registrations) instead of FE-side default padding, so old FE/new BE
  and new FE/old BE combinations keep working during rolling upgrades.
- Align the group index contract with Spark: an index outside
  [0, number_of_capturing_groups] is an error instead of NULL/empty.
- Keep empty strings for non-participating groups instead of skipping
  them, consistent with Spark.
- Fix test group counting ((i)(.*?)(e) has three groups, index 3 is
  valid) and expand regression coverage: column input, NULL literals,
  illegal indexes, empty groups.
@linrrzqqq

Copy link
Copy Markdown
Collaborator

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review result: changes requested on d387667321b1ef9852fc3eefc3a89c04ba7c9778.

I found six blocking comment threads: the RE2 loop drops valid zero-length/terminal group-0 matches and can revisit a contextual match; the Boost loop can repeat zero-width matches and advance through UTF-8 bytes; the new bounds check regresses legacy two-argument/no-capture behavior and mixed nullable rows; the invalid-index BE unit fixture compiles a truncated regex; and the added column regressions query a table after it has been dropped.

Checkpoint conclusions:

  • Goal/correctness: the optional-index goal is not yet met because the matcher and validation paths return incorrect results or errors for the cases above.
  • Scope and interfaces: the implementation is focused. FE 2/3-argument construction, fixed signatures, child preservation, scalar translation, and the distinct BE StringString / StringStringInt64 registrations are structurally coherent.
  • Compatibility/upgrades: the BE-first rolling-upgrade wiring is structurally sound, but the implicit-index zero-capture regression still breaks existing two-argument behavior.
  • Parallel and special paths: both string/array outputs, RE2/Boost engines, constness combinations, nullable wrapping, error cleanup, and test-table lifecycle were traced. The inline comments cover every substantiated issue found.
  • Concurrency/lifecycle/configuration/persistence/data writes/observability: no applicable shared-state, lock, distributed-lifecycle, configuration, persistence, transaction, write-path, or observability issue was introduced.
  • Performance: no distinct performance regression was substantiated beyond the incorrect traversal behavior already called out.
  • Tests: static review only, as required by the review runner; no build or test was run. The added cases cover ordinary groups and literal NULL/error inputs, but do not cover zero-width/contextual traversal or mixed nullable rows, and the BE fixture plus table-lifetime bug prevent two intended test paths from executing.
  • User focus: review_focus.txt contains no additional focus guidance; the full PR was reviewed.
  • Completion: three bounded rounds were completed. All final-round reviewers returned NO_NEW_VALUABLE_FINDINGS against the exact six-comment payload, every candidate was accepted or dismissed with evidence, and the live PR head/base were reverified immediately before submission.

if (matches.size() > 1 && !matches[1].empty()) {
results.emplace_back(matches[1].data(), matches[1].size());
// Extract the capturing group with the given index
if (static_cast<size_t>(index) < matches.size()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the newly supported idx = 0, this drops valid zero-length matches before the selected group is appended, and while (pos < size) also skips a terminal match. For example, regexp_extract_all_array('b', 'a*', 0) returns [] here, while Spark's matcher yields ["", ""] (offsets 0 and 1); empty input/pattern should similarly yield one empty match. Please emit the selected group for successful empty matches and advance safely while allowing one terminal-position search.

}
}
// Move position forward
auto offset = std::string(str_pos, str_size)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Advancing via a fresh textual find can select an earlier identical substring that did not satisfy the regex context. With regexp_extract_all_array('ab b', '\\bb', 0), RE2 matches only the final b, but .find("b") chooses the earlier non-boundary byte, so the loop reaches and emits the final match twice. Please advance from the actual RE2 match pointer (matches[0].data() - str_pos) plus its length.

while (boost::regex_search(search_start, search_end, matches, *boost_regex)) {
if (matches.size() > 1 && matches[1].matched) {
results.emplace_back(matches[1].str());
if (static_cast<size_t>(index) < matches.size()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a zero-width match found after the current origin, search_start += 1 advances from the old origin, not from matches[0].first. Thus regexp_extract_all_array('ab', '(?=b)', 0) (Boost path) emits the same lookahead twice; multibyte input can also move the next search into UTF-8 continuation bytes. Please base progress on the actual match location and use an encoding-safe one-character advance after an empty match.

// Same contract as Spark: the group index must be within
// [0, number_of_capturing_groups], anything else is an error.
const int num_groups = engine->number_of_capturing_groups();
if (index_data < 0 || index_data > num_groups) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new bounds check has two call-path regressions. First, the two-argument overload reaches it with implicit idx = 1, so a zero-capture regex now errors; before this PR it called push_empty, and existing regexp_extract_all('xxfs','f') / array regressions still expect empty results. Second, default null handling executes nested values for mixed nullable blocks before restoring null maps, so (NULL,'(a)',2), ('abc','(a)',1) aborts on the NULL row instead of returning NULL there. Please preserve legacy no-index/no-capture behavior and skip nullable rows before explicit idx validation; NULL-literal tests are FE-folded and do not cover this BE path.

auto col_str = ColumnString::create();
col_str->insert_data("hitdecisiondlist", 16);
auto col_pattern = ColumnString::create();
col_pattern->insert_data("(i)(.*?)(e)", 10);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This literal is 11 bytes, so length 10 stores the malformed pattern "(i)(.*?)(e". Because the pattern is constant, open(THREAD_LOCAL) fails before execute() can test either invalid index. Please use the actual string size (or 11) so these cases validate the intended idx error path.

qt_sql_regexp_extract_all_array_group3 "select regexp_extract_all_array('hitdecisiondlist', '(i)(.*?)(e)', 3);"

// column input
qt_sql_regexp_extract_all_col_group0 "SELECT regexp_extract_all(k, '(ll)(i)', 0) from test_string_function_regexp ORDER BY k;"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These column cases run after test_string_function_regexp is dropped at line 193 and after the suite switches to test_query_db; the table is never recreated. The first query therefore fails with a missing-table error instead of producing the added .out rows or exercising the three-argument path. Please move these cases before the drop (or recreate/populate the table here) and regenerate the expected output with the regression runner.

…advancing

- Only enforce the out-of-range group index error for the explicit
  three-argument form; two-argument calls on patterns without
  capturing groups keep returning an empty result.
- Advance past re2 matches via the match pointer instead of a textual
  find, which could select an earlier identical substring.
- Advance past boost zero-width matches from the match position, not
  from the old search origin.
- Fix a truncated pattern literal in the invalid-index unit test and
  recreate the test table for the new column-input regression cases.
@ccl125

ccl125 commented Jul 30, 2026

Copy link
Copy Markdown
Author

Thanks for the thorough scan! All five points are addressed in 1ca8dcf (pushed):

  1. 2-arg regression — the out-of-range error now only applies to the explicit three-argument form; two-argument calls on patterns without capturing groups keep the legacy empty-result behavior (covered by a ported unit case).
  2. Truncated pattern literal — fixed, the string is 11 bytes.
  3. Dropped test table — the column-input cases now recreate test_string_function_regexp before running.
  4. re2 advancing — the loop now advances via the match pointer offset instead of a textual find (this and the next one were pre-existing issues in match_all_and_extract, fixed here since the function was being reworked anyway; regression case added).
  5. boost zero-width advancing — now advances from matches[0].first + 1 instead of the old origin.

@linrrzqqq

Copy link
Copy Markdown
Collaborator

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review result: changes requested on 1ca8dcfd949917d2f7da8c6004ec188f971b8abc.

I found one new blocking issue: after each non-empty match, both engines search the remaining suffix as a new regex subject, so anchors and left-context assertions change. The inline comment gives concrete RE2 and Boost failures.

Existing-thread status at this head:

  • Fixed: RE2 textual-occurrence advancement, Boost delayed-zero-width origin advancement, legacy two-argument/no-capture behavior, the malformed invalid-index unit fixture, and the dropped-table regression setup.
  • Still blocking and already reported (not duplicated here): RE2 empty/terminal group-0 matching, encoding-safe Boost advancement after empty matches, and mixed nullable-row index validation.

Checkpoint conclusions:

  • Goal/correctness: not met yet; the new group-index path still returns incorrect match sets for contextual patterns, in addition to the unresolved existing-thread cases.
  • Scope/interfaces: the six-file change is focused. FE two-/three-argument signatures, child preservation, nullable return shapes, and the BE's exact typed registrations are structurally coherent.
  • Compatibility/upgrades: two-argument old-FE/new-BE calls and new-FE/fully-upgraded-BE calls resolve correctly under the BE-first rolling order; no separate serialization-compatibility issue was found.
  • Parallel/special paths: both string/array handlers, RE2/Boost engines, constant/nonconstant pattern/index shapes, nullable wrapping, invalid-index status flow, and test-table lifecycle were traced. The new inline plus existing threads cover every substantiated correctness issue.
  • Concurrency/lifecycle/configuration/persistence/data writes/observability: no new shared-state, lock, lifecycle, configuration, persistence, transaction, write-path, or observability issue applies.
  • Performance: no distinct performance regression was substantiated.
  • Tests: static review only, as required by the review runner; no build or test was run. Current-head fixes make the intended unit/regression paths structurally reachable and deterministic, but coverage is still missing for suffix-context matching and the already-reported empty/multibyte/mixed-null paths.
  • Documentation: the user-visible feature has a linked website PR in the PR description.
  • User focus: review_focus.txt contains no additional guidance; the full PR was reviewed.
  • Completion: three bounded rounds were completed. All final-round reviewers returned NO_NEW_VALUABLE_FINDINGS against this exact one-comment payload and body, every candidate was accepted/dismissed/deduplicated with evidence, and the live PR head/base were reverified immediately before submission.

pos += offset + matches[0].size();
// Move position forward. matches[0] points into the searched string,
// so its address gives the exact match offset — no textual find needed.
pos += (matches[0].data() - str_pos) + matches[0].size();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve the original regex subject between matches

This exact-pointer advance fixes the earlier textual-find bug, but the next iteration still passes StringPiece(str_pos, str_size) (and Boost [search_start, search_end)) as a brand-new subject. That changes left context after any non-empty match: regexp_extract_all_array('aa', '^a', 0) treats the second byte as another beginning and returns ["a","a"] instead of Spark's single anchored match; with extended regex, regexp_extract_all_array('ab', 'a|(?<=a)b', 0) loses the a before search_start and misses the valid b. This is distinct from the existing zero-width/progress threads. Please keep the original subject/range and advance a search offset/flags that preserve beginning and previous-character context.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cf47ba1 — the re2 loop now calls Match on the original subject with an advancing start position (so ^ and left context refer to the original string), and the boost loop keeps the original begin reachable with match_prev_avail and suppresses bol at the advanced origin with match_not_bol. Added ('aa', '^a', 0) -> ['a'] as a regression case.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RE2 side and the simple ^ case are fixed, but the Boost loop still restarts buffer-anchor semantics after the first match. The advanced call uses match_prev_avail | match_not_bol; Boost documents that match_prev_avail makes match_not_bol ignored, defines match_not_bob for \A, and uses match_prev_avail | match_not_bob in its repeated-regex_search example.

With enable_extended_regex=true, regexp_extract_all_array('ab', 'a|(?<=a)\\Ab', 0) can therefore treat the suffix at b as a new buffer and return ["a","b"] instead of ["a"]. Please preserve beginning-of-buffer semantics for the advanced search and add a Boost-path regression. This is a follow-up to the same original-subject issue rather than a new thread.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4737a9d — switched the boost flags to the documented repeated-search idiom match_prev_avail | match_not_bob (with match_prev_avail set, match_not_bol is ignored; match_not_bob keeps \A anchored to the start of the original buffer).

re2 now searches the original subject with an advancing start
position instead of a fresh substring, so `^` stays anchored to the
beginning of the original string; the boost path keeps the original
begin reachable via match_prev_avail and suppresses bol at the
advanced origin via match_not_bol.
@linrrzqqq

Copy link
Copy Markdown
Collaborator

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review result: changes requested on cf47ba19e4de62b5f1f78008ad318cf9075c34ed.

No new inline threads were opened because every current-head correctness issue deduplicates to an existing thread. I added one follow-up reply to the attempted Boost subject-preservation fix.

Current-head status:

Checkpoint conclusions:

  • Goal/correctness: not met yet. The optional group-index path still loses valid empty/terminal matches, can enter UTF-8 continuation bytes after a Boost zero-width match, can fail NULL propagation before explicit-index validation, and can change Boost buffer-anchor semantics after a prior match.
  • Scope and interfaces: the six-file change is focused. FE two-/three-argument constructor discovery, signature selection, child preservation, and the distinct BE StringString / StringStringInt64 factory registrations are coherent.
  • Compatibility/upgrades: the BE-first rolling-upgrade wiring is sound for old-FE/new-BE and legacy two-argument new-FE/old-BE calls; explicit three-argument syntax appropriately requires upgraded BEs.
  • Parallel and special paths: both string/array handlers, RE2/Boost engines, constant/nonconstant pattern/index shapes, nullable wrapping, invalid-index status flow, and test-table lifecycle were traced. The existing threads plus the follow-up reply cover every substantiated issue.
  • Concurrency/lifecycle/configuration/persistence/data writes/observability: no new shared-state, lock, lifecycle, configuration, persistence, transaction, write-path, or observability issue applies.
  • Performance: no distinct performance regression was substantiated beyond the incorrect repeated-search behavior already covered by the correctness threads.
  • Tests: static review only, as required by the review runner; no build or test was run. The repaired unit fixture and recreated regression table make their intended cases structurally reachable and deterministic, and the new .out rows match the inserted data. Coverage is still missing for the live empty/terminal, multibyte Boost, mixed-nullable, and Boost buffer-anchor failures.
  • Documentation: the user-visible feature has a linked website PR in the PR description.
  • User focus: review_focus.txt contains no additional guidance; the full PR was reviewed.
  • Completion: three bounded rounds were completed. All final-round reviewers returned NO_NEW_VALUABLE_FINDINGS against this exact reply and review payload, every candidate was accepted, dismissed, or deduplicated with evidence, and the live PR head/base were reverified immediately before submission.

Boost ignores match_not_bol once match_prev_avail is set; the
documented repeated-regex_search idiom is match_prev_avail |
match_not_bob, which keeps `\A` anchored to the start of the
original buffer.
@linrrzqqq

Copy link
Copy Markdown
Collaborator

/review

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Codex automated review failed and did not complete.

Error: All Codex review accounts are usage-limited; earliest retry is 2026-08-08T03:32:00Z.
Workflow run: https://github.com/apache/doris/actions/runs/31062172341

Please trigger /review again after that time.

@ccl125

ccl125 commented Aug 14, 2026

Copy link
Copy Markdown
Author

Hi @linrrzqqq @morrySnow, all review feedback has been addressed (latest: the boost repeated-search flags in 4737a9d). Could someone help trigger run buildall when convenient so we can see the full CI result? Thanks!

@linrrzqqq

Copy link
Copy Markdown
Collaborator

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review result: changes requested on 4737a9dc3e37552c486c92980202530c6fdd2606.

No new inline threads were opened because every substantiated current-head issue deduplicates to an existing review thread.

Current blockers:

Resolved since the prior review: exact RE2 match-offset advancement, legacy two-argument/no-capture handling, the invalid-index unit-test literal, regression-table recreation, original-subject searching, and Boost beginning-of-buffer preservation with match_not_bob.

Checkpoint conclusions:

  • Goal and correctness: not yet met because the three current-head defects above remain blocking.
  • Scope and interfaces: the six-file change is focused; FE constructor discovery, coercion, rebuild, translation, and BE two-/three-argument registrations are coherent.
  • Compatibility: the physical two-argument form remains available for old-FE/new-BE rollout, and the new three-argument form maps to a distinct BE signature. The disputed implicit-group behavior is a same-matcher-line duplicate of an existing thread, not a separate reportable instance.
  • Parallel and special paths: string/array outputs, RE2/Boost engines, constant/nonconstant arguments, nullable execution, and error paths were traced. The surviving gaps are already represented by the linked threads.
  • Concurrency, lifecycle, configuration, persistence, transactions, data writes, observability, and FE-BE session variables: no applicable new mechanism or shared state is introduced; regex state remains thread-local.
  • Performance: no distinct material regression was found beyond the existing correctness blockers.
  • Tests and results: static review only; this runner forbids builds and tests. The changed expected output is deterministic and the table setup is valid. Missing mixed-nullable, terminal/zero-width, and multibyte coverage corresponds to the existing blockers.
  • Documentation and focus: the PR links a website documentation change; no extra review focus was supplied, so the full PR was reviewed.
  • Completion: two independent review rounds were completed. In Round 2, the full BE, full FE/tests, and risk-focused reviewers all returned NO_NEW_VALUABLE_FINDINGS; every candidate was independently verified and adjudicated.

@linrrzqqq

Copy link
Copy Markdown
Collaborator

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 0.00% (0/16) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 17000 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 4737a9dc3e37552c486c92980202530c6fdd2606, data reload: false

------ Round 1 ----------------------------------
orders	Doris	NULL	NULL	0	0	0	NULL	0	NULL	NULL	2023-12-26 18:27:23	2023-12-26 18:42:55	NULL	utf-8	NULL	NULL	
============================================
q1	17585	3112	3075	3075
q2	q3	10895	848	517	517
q4	4682	256	203	203
q5	7671	572	384	384
q6	142	115	92	92
q7	525	476	384	384
q8	9251	937	914	914
q9	3553	2383	2360	2360
q10	6510	868	738	738
q11	456	253	237	237
q12	698	408	329	329
q13	17877	1536	1175	1175
q14	153	148	138	138
q15	q16	479	389	375	375
q17	809	787	759	759
q18	3112	2246	2258	2246
q19	1214	931	676	676
q20	680	526	460	460
q21	5564	1708	1878	1708
q22	336	272	230	230
Total cold run time: 92192 ms
Total hot run time: 17000 ms

----- Round 2, with runtime_filter_mode=off -----
orders	Doris	NULL	NULL	150000000	42	6422171781	NULL	22778155	NULL	NULL	2023-12-26 18:27:23	2023-12-26 18:42:55	NULL	utf-8	NULL	NULL	
============================================
q1	3497	3375	3383	3375
q2	q3	2242	2266	2124	2124
q4	1200	1159	887	887
q5	2200	2126	2092	2092
q6	166	117	91	91
q7	1052	901	871	871
q8	1605	1422	1425	1422
q9	3121	3159	3091	3091
q10	1846	1776	1625	1625
q11	368	273	258	258
q12	451	426	353	353
q13	1498	1545	1164	1164
q14	174	163	169	163
q15	q16	400	385	367	367
q17	1068	1031	1034	1031
q18	4967	4382	4728	4382
q19	866	867	852	852
q20	962	952	824	824
q21	3517	3234	3246	3234
q22	408	341	310	310
Total cold run time: 31608 ms
Total hot run time: 28516 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 80644 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 4737a9dc3e37552c486c92980202530c6fdd2606, data reload: false

query5	4254	425	355	355
query6	452	174	162	162
query7	4824	471	261	261
query8	333	142	117	117
query9	8681	2924	2911	2911
query10	410	258	218	218
query11	5459	1042	937	937
query12	120	75	75	75
query13	1207	442	334	334
query14	6031	2222	2111	2111
query14_1	2012	1984	1973	1973
query15	194	119	115	115
query16	1010	377	362	362
query17	829	451	370	370
query18	2359	335	243	243
query19	195	146	110	110
query20	72	71	69	69
query21	230	115	101	101
query22	5440	5275	5353	5275
query23	6646	6186	6009	6009
query23_1	6241	6039	6070	6039
query24	7350	1109	815	815
query24_1	781	785	778	778
query25	510	307	260	260
query26	1254	276	164	164
query27	2712	440	288	288
query28	4637	1512	1495	1495
query29	1113	448	356	356
query30	284	182	151	151
query31	856	432	356	356
query32	134	52	50	50
query33	512	227	178	178
query34	1138	838	496	496
query35	413	406	346	346
query36	596	566	528	528
query37	141	81	74	74
query38	1104	844	814	814
query39	493	486	480	480
query39_1	446	470	445	445
query40	338	124	116	116
query41	68	69	68	68
query42	84	80	79	79
query43	250	246	211	211
query44	
query45	112	100	103	100
query46	782	821	519	519
query47	775	751	711	711
query48	299	267	243	243
query49	566	230	188	188
query50	858	335	258	258
query51	8129	8077	8052	8052
query52	85	81	69	69
query53	212	230	161	161
query54	282	210	195	195
query55	109	61	56	56
query56	248	237	215	215
query57	704	676	641	641
query58	309	200	196	196
query59	1236	1220	1095	1095
query60	288	220	189	189
query61	108	114	132	114
query62	487	218	174	174
query63	181	158	153	153
query64	2778	684	601	601
query65	
query66	2028	305	245	245
query67	9860	9901	9930	9901
query68	
query69	393	225	198	198
query70	633	624	633	624
query71	313	252	244	244
query72	2443	1419	1561	1419
query73	771	594	358	358
query74	1650	1246	1162	1162
query75	1273	1157	1055	1055
query76	2341	710	551	551
query77	260	261	215	215
query78	3799	3587	3251	3251
query79	3314	784	576	576
query80	1719	420	359	359
query81	682	205	182	182
query82	1205	138	103	103
query83	338	251	232	232
query84	
query85	954	442	385	385
query86	630	170	175	170
query87	1037	986	875	875
query88	4290	2133	2102	2102
query89	374	227	200	200
query90	2274	149	147	147
query91	163	141	121	121
query92	88	43	45	43
query93	3024	1066	821	821
query94	826	266	218	218
query95	644	450	353	353
query96	850	574	283	283
query97	1051	1071	1029	1029
query98	196	131	130	130
query99	521	337	310	310
Total cold run time: 180158 ms
Total hot run time: 80644 ms

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100% (0/0) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 75.71% (34090/45029)
Line Coverage 60.64% (383825/632916)
Region Coverage 56.83% (322278/567116)
Branch Coverage 57.65% (146862/254758)

Cover the two- and three-argument forms, signature list, and
withChildren arity handling of RegexpExtractAll and
RegexpExtractAllArray to feed the FE coverage gate.
@ccl125

ccl125 commented Aug 18, 2026

Copy link
Copy Markdown
Author

Thanks for triggering buildall! Everything passed except the FE coverage gate (our FE change is only declarative signatures, so the increment had no UT coverage). Added a dedicated FE unit test for both function classes in 2817282 covering constructors/signatures/withChildren for the 2-arg and 3-arg forms. Could you help trigger run buildall once more?

@@ -159,29 +165,42 @@ struct RegexpExtractEngine {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the newly supported idx = 0, this drops valid zero-length matches before the selected group is appended, and while (pos < size) also skips a terminal match. For example, regexp_extract_all('b', 'a*', 0) returns [] here, while Spark's matcher yields ["", ""] (offsets 0 and 1); empty input/pattern should similarly yield one empty match. Please emit the selected group for successful empty matches and advance safely while allowing one terminal-position search.

fix this

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1bcc272 — explicit group-index calls now follow Spark exactly: zero-width matches are emitted ('b','a*',0 -> ['',''] and '' -> ['']), one terminal-position search is allowed, and advancement is UTF-8-safe (whole characters, no continuation-byte splits). The legacy two-argument form keeps the old skip behavior, covered by a dedicated case. New unit cases added for all three examples.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also fixed in 1bcc272 — input null maps are now merged before validation, so (NULL,'(a)',99), ('hitdecisiondlist','(i)(.*?)(e)',3) yields NULL for the first row and ['e'] for the second instead of aborting on the garbage index. A dedicated two-row nullable-block test covers it. The UTF-8-unsafe advance in the boost path is fixed the same way (whole-character steps via get_utf8_byte_length).

- Explicit group-index calls now emit successful zero-width matches
  (including one terminal-position match) and advance one full UTF-8
  character, matching Spark ('b','a*',0 -> ["",""]). The legacy
  two-argument form keeps skipping zero-width matches.
- Merge input null maps before validation so a row with a null
  string/pattern/index yields NULL for that row instead of aborting
  the query on an out-of-range garbage index.
- The boost zero-width advance now moves by whole UTF-8 characters
  instead of a raw byte, and both engines allow a terminal search.
@linrrzqqq

Copy link
Copy Markdown
Collaborator

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review result: changes requested on 1bcc27269761970bc7213cfebe14e13194b18811.

I found four new issues: two legacy two-argument behavior regressions, an RE2 later-origin zero-width progress bug, and a mandatory clang-format v16 failure. The mixed-null validation failure remains present on this head, but it is already fully covered by the existing thread, so I did not duplicate it.

Checkpoint conclusions:

  • Goal and proof: the optional index is wired through both FE expressions, both BE result forms, and distinct two-/three-argument factory keys, but the runtime goal is not met. Existing two-argument results change for nonparticipating/empty captures and subject-sensitive repeated matching; explicit-index RE2 searches can duplicate a zero-width match found after the current origin; and the existing mixed-null path still validates a garbage nested index before NULL propagation.
  • Scope and focus: the seven-file change is focused on the shared regexp engine, its two output handlers/registrations, FE binding, and tests. review_focus.txt contains no additional guidance; the full PR was reviewed.
  • Concurrency: no new thread, lock, atomic, or cross-thread mutable state is introduced.
  • Lifecycle: constant patterns retain the established thread-local compiled engine, dynamic patterns use per-row scoped ownership, and no cross-TU static initialization dependency or abnormal release path was added.
  • Configuration: no configuration item is added; the existing enable_extended_regex switch is reused.
  • Compatibility: the separate StringString and StringStringInt64 registrations preserve old-FE/new-BE two-argument routing and new-FE/new-BE three-argument routing, and nullable type-family lookup is coherent. Result compatibility is nevertheless broken by the two legacy-arities issues called out inline. No storage format, serialized symbol, or protocol field changes are introduced.
  • Parallel and special paths: both string/array outputs, RE2/Boost engines, legacy/explicit arities, constant/vector pattern and index shapes, zero-width/terminal matches, capture participation, nullable inputs, error propagation, and output-offset finalization were traced. Boost's current returned-match-relative progress and original-context flags add no separate issue; the remaining mixed-null failure is the existing-thread duplicate noted above.
  • Tests and expected results: the FE tests, BE unit cases, regression labels, expected rows, ordering, and table lifecycle were reviewed. Concrete missing cases correspond to the inline legacy nonparticipating/anchor regressions and later-origin RE2 boundary case. The new mixed-null unit test cannot pass through the default nullable-unwrapping path already described in the existing thread. No build or test suite was run because this review runner forbids builds; the read-only clang-format v16 check was run and fails on both changed C++ files, while their base revisions pass.
  • Observability: no new distributed or operational path needs logs or metrics; existing status/warning propagation is otherwise sufficient.
  • Persistence, transactions, and writes: no EditLog, failover, transaction, storage-visibility, delete-bitmap, or production data-write path is involved.
  • FE-BE variables: the index is an ordinary scalar-function child carried by existing translation; no new session or thrift variable requires propagation.
  • Performance and memory: no distinct hot-path, allocation-accounting, ownership, or memory-safety issue was substantiated beyond the incorrect repeated-search behavior already reported.
  • Completion: three bounded review rounds were completed. All three final-round reviewers returned NO_NEW_VALUABLE_FINDINGS; every candidate was accepted, dismissed with evidence, or deduplicated, so this review is converged rather than capped/incomplete. The live head/base and current review threads were reverified immediately before submission.

bool success = re2_regex->Match(re2::StringPiece(str_pos, str_size), 0, str_size,
// Search within the original subject starting from pos, so `^` stays
// anchored to the beginning of the original string.
bool success = re2_regex->Match(re2::StringPiece(data, size), pos, size,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve suffix anchoring for the legacy arity

Searching the original subject is needed for the new explicit-index semantics, but this shared call also changes existing two-argument results. Before this PR, regexp_extract_all('aa', '^(a)') searched each remaining suffix as a new subject and returned ['a','a']; this now keeps ^ tied to offset 0 and returns only ['a'] (the array form changes too). The existing original-subject thread covered an explicitly supplied index, while the PR promises the no-index form remains unchanged. Please split the cursor semantics by arity, or explicitly declare and test the legacy behavior change.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ec1c238 — the legacy two-argument form now runs the pre-change substring loop verbatim, so ^ anchoring and suffix behavior are byte-identical to before this PR; only the explicit group-index form searches the original subject.

if (matches.size() > 1 && !matches[1].empty()) {
results.emplace_back(matches[1].data(), matches[1].size());
// Extract the capturing group with the given index
if (static_cast<size_t>(index) < matches.size()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve nonparticipating groups for the legacy arity

This unconditional append also runs when emit_empty_matches is false, so it changes existing two-argument queries. For example, regexp_extract_all('a b', '(a)|(b)') previously skipped the unmatched default group 1 on the b match and returned ['a']; it now appends an empty element and returns ['a',''] (the array variant changes likewise). The existing thread about preserving empty groups covered an explicitly supplied index, whereas the PR promises the no-index form remains unchanged. Please retain the former unmatched/empty-group filtering for the two-argument path and add a legacy-arity case for both outputs.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ec1c238 — the legacy loop again appends only the first group when non-empty (the unconditional append now lives only in the Spark loop used by explicit group-index calls).

break;
}
// Advance one full UTF-8 character, never into a continuation byte.
pos += get_utf8_byte_length(static_cast<uint8_t>(data[pos]));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Advance from the returned RE2 empty-match offset

An unanchored RE2 search can return a zero-width match strictly after pos, but this advances from data[pos] instead of from whole.data(). For example, regexp_extract_all_array('ab', '\\b', 0) should emit the two boundaries at offsets 0 and 2. After the first match sets pos to 1, the next search finds offset 2; this line advances only to 2, so the following iteration emits that same boundary again. The earlier progress thread covered the Boost branch, which now correctly uses matches[0].first; please compute the RE2 match offset from whole.data() - data before the UTF-8 step and add this later-than-origin case for both outputs.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ec1c238 — zero-width advancement now starts from the returned match location (whole.data() + one UTF-8 character), not from data[pos].

break;
}
search_start += 1;
search_start =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Apply the mandatory clang-format v16 pass

The required formatter rejects both changed BE files: clang-format-16 --dry-run --Werror be/src/exprs/function/function_regexp.cpp be/test/exprs/function/function_like_test.cpp reports this cursor wrapping plus the new include/null-map code and several changed unit-test lines. The base revisions of both files pass the same check, so these violations are introduced here. Please run build-support/clang-format.sh on the changed C++ files so the repository's formatting gate can pass.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ec1c238 — both BE files now pass clang-format-16 --dry-run --Werror locally.

The shared loop leaked Spark semantics into the legacy two-argument
form: empty/non-participating groups were appended where the old code
skipped them, and the original-subject search changed `^` anchoring.
Restore the pre-change loop verbatim for the legacy form (substring
searches, zero-width skipped, first-group-only non-empty appends) and
keep Spark semantics in a dedicated loop for the explicit group-index
form. Empty-match advancement now starts from the returned match
location. Files formatted with clang-format 16.
@linrrzqqq

Copy link
Copy Markdown
Collaborator

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review result: request changes. The review converged after the third full round with two new inline correctness findings. A third confirmed failure—the mixed-null row whose nested invalid index is validated after nullable wrappers are stripped—remains covered by existing discussion r3681145562, so I did not duplicate it inline.

Critical-checkpoint conclusions:

  • Goal and scope: The patch is focused on adding the optional Spark-style group index to both result representations while retaining the two-argument form. No additional user focus was supplied.
  • Correctness and special conditions: Ordinary group selection, nonparticipating groups, ASCII zero-width/terminal matches, and checked index errors are consistent. The two new inline findings cover lost Boost previous-match state and supplementary-character zero-width iteration; the existing nullable-row failure is still unresolved.
  • Compatibility and parallel paths: Exact FE constructors/signatures and BE family keys resolve both arities without collision. The supported old-FE/new-BE rolling-upgrade direction is preserved, and the string/array outputs share the same implementation.
  • Concurrency and lifecycle: Compiled regex state is function-context thread-local. No new locks, cross-thread state, static-initialization dependency, startup/shutdown lifecycle, transaction/persistence, or data-write path is involved.
  • Configuration and FE/BE propagation: No new variable is introduced. The existing enable_extended_regex path selects Boost and exposes the \G finding; no separate propagation defect was found.
  • Tests and result provenance: The added FE, BE, and regression cases are reachable and their inspected ordinary outputs are deterministic. However, the mixed-null BE test should fail on the normal wrapper path, and there is no explicit-index Boost \G or supplementary-plane zero-width case. Per the review-runner instructions, repository builds/tests were not run.
  • Performance, memory, and observability: No distinct material regression survived review; work and result allocation remain proportional to emitted matches. Existing checked statuses/warnings are sufficient, with no new observability issue.

Please resolve both inline findings and the existing nullable-row thread before approval.

}
// Advance one full UTF-8 character FROM THE MATCH LOCATION, never
// into a continuation byte and never rescanning the same match.
pos = (whole.data() - data) +

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve Spark's supplementary-character empty matches

Spark delegates this loop to Java Matcher.find(), whose progress after an empty match is one UTF-16 code unit. For regexp_extract_all_array('😀a', 'a*', 0), Java emits four entries: empty matches at UTF-16 offsets 0 and 1, a at 2:3, and the terminal empty match. This branch instead advances from byte 0 over the whole four-byte UTF-8 character, so it returns only ["", "a", ""]; the Boost branch makes the same jump. Since the explicit-index path is documented as Spark semantics, please either emulate the UTF-16 iteration positions without passing invalid byte offsets to the engines or narrow that contract, and add supplementary-character cases for both outputs.

// match_not_bob keeps `\A` anchored to the start of the original
// buffer (Boost's documented repeated-regex_search idiom; with
// match_prev_avail set, `^` is decided by the preceding character).
while (search_start <= search_end &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve previous-match state for \G

\G is stateful: Spark reuses one Java Matcher, so on ab the pattern \G matches only at offset 0; after that zero-width match, find() advances while the previous match still ended at 0. This loop instead starts a fresh Boost search at each new search_start, so each suffix origin is treated as a new continuation point and the array form emits empty matches at offsets 0, 1, and 2 (the string form shares the same engine). Please preserve continuation state across searches or reject \G, and add a Boost-path regression for this case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants