From 4888fcdfc06b7b419924792bba7a708fe96f1128 Mon Sep 17 00:00:00 2001 From: Max Freedom Pollard <272618364+MaxFreedomPollard@users.noreply.github.com> Date: Sun, 6 Sep 2026 13:43:06 -0400 Subject: [PATCH] fix: apply regexp_instr subexpr to the N-th match, not the first regexp_instr(str, regexp, start, N, flags, subexpr) ignored N whenever subexpr was greater than zero, always reporting a position inside the first match. Asking for an occurrence that does not exist returned a position instead of 0. get_index in datafusion/functions/src/regex/regexpinstr.rs selected the match with Regex::captures, which only ever returns the first match, and dropped n on that branch. It now uses Regex::captures_iter(...).nth(n - 1), the counterpart of the find_iter(...).nth(n - 1) the subexpr == 0 branch already used, so both branches count occurrences the same way. Every existing test of subexpr used N = 1, which is how this survived. test_case_sensitive_regexp_instr_scalar_subexp gains N = 2 for each of the three capture groups, N = 3 where no such match exists, and a case combining start with subexpr. regexp/regexp_instr.slt gains the two SQL queries that changed. --- datafusion/functions/src/regex/regexpinstr.rs | 41 ++++++++++++++----- .../test_files/regexp/regexp_instr.slt | 12 ++++++ 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/datafusion/functions/src/regex/regexpinstr.rs b/datafusion/functions/src/regex/regexpinstr.rs index de460c56f63c5..a8816ac7d49fb 100644 --- a/datafusion/functions/src/regex/regexpinstr.rs +++ b/datafusion/functions/src/regex/regexpinstr.rs @@ -390,17 +390,20 @@ fn get_index( }; let search_slice = &value[byte_start_offset..]; - // A subexpression, when requested, takes precedence over the N-th match. + // `n` is 1-based, `nth` is 0-based. + let nth = (n - 1) as usize; + + // A subexpression, when requested, is located within the N-th match. let match_start = if subexpr > 0 { pattern - .captures(search_slice) + .captures_iter(search_slice) + .nth(nth) .and_then(|captures| captures.get(subexpr as usize)) .map(|matched| matched.start()) } else { - // `n` is 1-based, `nth` is 0-based. pattern .find_iter(search_slice) - .nth((n - 1) as usize) + .nth(nth) .map(|matched| matched.start()) }; @@ -685,13 +688,29 @@ mod tests { } fn test_case_sensitive_regexp_instr_scalar_subexp() { - let values = ["12 abc def ghi 34"]; - let regex = ["(abc) (def) (ghi)"]; - let start = [1]; - let nth = [1]; - let flags = ["i"]; - let subexps = [2]; - let expected: Vec = vec![8]; + // The first row locates a subexpression in the only match. The rest + // locate one in the N-th match, including an N that has no match. + let values = [ + "12 abc def ghi 34", + "12 abc def ghi 34 abc def ghi 56", + "12 abc def ghi 34 abc def ghi 56", + "12 abc def ghi 34 abc def ghi 56", + "12 abc def ghi 34 abc def ghi 56", + "12 abc def ghi 34 abc def ghi 56", + ]; + let regex = [ + "(abc) (def) (ghi)", + "(abc) (def) (ghi)", + "(abc) (def) (ghi)", + "(abc) (def) (ghi)", + "(abc) (def) (ghi)", + "(abc) (def) (ghi)", + ]; + let start = [1, 1, 1, 1, 1, 18]; + let nth = [1, 2, 2, 2, 3, 1]; + let flags = ["i", "i", "i", "i", "i", "i"]; + let subexps = [2, 1, 2, 3, 1, 2]; + let expected: Vec = vec![8, 19, 23, 27, 0, 23]; izip!( values.iter(), diff --git a/datafusion/sqllogictest/test_files/regexp/regexp_instr.slt b/datafusion/sqllogictest/test_files/regexp/regexp_instr.slt index bbe9693736442..b9a8062aaa3f3 100644 --- a/datafusion/sqllogictest/test_files/regexp/regexp_instr.slt +++ b/datafusion/sqllogictest/test_files/regexp/regexp_instr.slt @@ -73,6 +73,18 @@ SELECT ---- 11 +# The subexpression is located inside the N-th match, not inside the first one. +query I +SELECT regexp_instr('12 abc def ghi 34 abc def ghi 56', '(abc) (def) (ghi)', 1, 2, 'i', 2); +---- +23 + +# There is no third match, so there is no position to report for any subexpression. +query I +SELECT regexp_instr('12 abc def ghi 34 abc def ghi 56', '(abc) (def) (ghi)', 1, 3, 'i', 1); +---- +0 + statement error DataFusion error: Arrow error: Compute error: regexp_instr\(\) requires start to be 1-based SELECT regexp_instr('123123123123', '123', 0);