Repository navigation
Open
Labels
Description
What happens?
When using regexp_extract, with the m option as a final argument, the resulting query does not apply the correct, documented behavior.
SELECT regexp_extract(
'Line 1
Some text
Line 2
More text',
'^Line.*$',
0,
'm');Will yield an empty string, ''
The DuckDB documentation states that:
'm', 'n', 'p' | Newline sensitive matching
If I provide (?m) as a prefix, then the newline sensitive matching works correctly...
SELECT regexp_extract(
'Line 1
Some text
Line 2
More text',
'(?m)^Line.*$');== 'Line 1'
From what I've gathered, it might be found here:
duckdb/src/function/scalar/string/regexp.cpp
Lines 224 to 321 in e1dcf13
| static void RegexExtractFunction(DataChunk &args, ExpressionState &state, Vector &result) { | |
| auto &func_expr = state.expr.Cast<BoundFunctionExpression>(); | |
| const auto &info = func_expr.bind_info->Cast<RegexpExtractBindData>(); | |
| auto &strings = args.data[0]; | |
| auto &patterns = args.data[1]; | |
| if (info.constant_pattern) { | |
| auto &lstate = ExecuteFunctionState::GetFunctionState(state)->Cast<RegexLocalState>(); | |
| UnaryExecutor::Execute<string_t, string_t>(strings, result, args.size(), [&](string_t input) { | |
| return Extract(input, result, lstate.constant_pattern, info.rewrite); | |
| }); | |
| } else { | |
| BinaryExecutor::Execute<string_t, string_t, string_t>(strings, patterns, result, args.size(), | |
| [&](string_t input, string_t pattern) { | |
| RE2 re(CreateStringPiece(pattern), info.options); | |
| return Extract(input, result, re, info.rewrite); | |
| }); | |
| } | |
| } | |
| //===--------------------------------------------------------------------===// | |
| // Regexp Extract Struct | |
| //===--------------------------------------------------------------------===// | |
| static void RegexExtractStructFunction(DataChunk &args, ExpressionState &state, Vector &result) { | |
| auto &lstate = ExecuteFunctionState::GetFunctionState(state)->Cast<RegexLocalState>(); | |
| const auto count = args.size(); | |
| auto &input = args.data[0]; | |
| auto &child_entries = StructVector::GetEntries(result); | |
| const auto groupSize = child_entries.size(); | |
| // Reference the 'input' StringBuffer, because we won't need to allocate new data | |
| // for the result, all returned strings are substrings of the originals | |
| for (auto &child_entry : child_entries) { | |
| child_entry->SetAuxiliary(input.GetAuxiliary()); | |
| } | |
| vector<RE2::Arg> argv(groupSize); | |
| vector<RE2::Arg *> groups(groupSize); | |
| vector<duckdb_re2::StringPiece> ws(groupSize); | |
| for (size_t i = 0; i < groupSize; ++i) { | |
| groups[i] = &argv[i]; | |
| argv[i] = &ws[i]; | |
| } | |
| if (input.GetVectorType() == VectorType::CONSTANT_VECTOR) { | |
| result.SetVectorType(VectorType::CONSTANT_VECTOR); | |
| if (ConstantVector::IsNull(input)) { | |
| ConstantVector::SetNull(result, true); | |
| } else { | |
| ConstantVector::SetNull(result, false); | |
| auto idata = ConstantVector::GetData<string_t>(input); | |
| auto str = CreateStringPiece(idata[0]); | |
| auto match = duckdb_re2::RE2::PartialMatchN(str, lstate.constant_pattern, groups.data(), | |
| UnsafeNumericCast<int>(groups.size())); | |
| for (size_t col = 0; col < child_entries.size(); ++col) { | |
| auto &child_entry = child_entries[col]; | |
| ConstantVector::SetNull(*child_entry, false); | |
| auto &extracted = ws[col]; | |
| auto cdata = ConstantVector::GetData<string_t>(*child_entry); | |
| cdata[0] = string_t(extracted.data(), UnsafeNumericCast<uint32_t>(match ? extracted.size() : 0)); | |
| } | |
| } | |
| } else { | |
| UnifiedVectorFormat iunified; | |
| input.ToUnifiedFormat(count, iunified); | |
| const auto &ivalidity = iunified.validity; | |
| auto idata = UnifiedVectorFormat::GetData<string_t>(iunified); | |
| // Start with a valid flat vector | |
| result.SetVectorType(VectorType::FLAT_VECTOR); | |
| // Start with valid children | |
| for (size_t col = 0; col < child_entries.size(); ++col) { | |
| auto &child_entry = child_entries[col]; | |
| child_entry->SetVectorType(VectorType::FLAT_VECTOR); | |
| } | |
| for (idx_t i = 0; i < count; ++i) { | |
| const auto idx = iunified.sel->get_index(i); | |
| if (ivalidity.RowIsValid(idx)) { | |
| auto str = CreateStringPiece(idata[idx]); | |
| auto match = duckdb_re2::RE2::PartialMatchN(str, lstate.constant_pattern, groups.data(), | |
| UnsafeNumericCast<int>(groups.size())); | |
| for (size_t col = 0; col < child_entries.size(); ++col) { | |
| auto &child_entry = child_entries[col]; | |
| auto cdata = FlatVector::GetData<string_t>(*child_entry); | |
| auto &extracted = ws[col]; | |
| cdata[i] = string_t(extracted.data(), UnsafeNumericCast<uint32_t>(match ? extracted.size() : 0)); | |
| } | |
| } else { | |
| FlatVector::SetNull(result, i, true); | |
| } | |
| } | |
| } | |
| } |
To Reproduce
Provide this query:
SELECT regexp_extract(
'Line 1
Some text
Line 2
More text',
'^Line.*$',
0,
'm');
You will get an empty string ''
OS:
macOS (arm64)
DuckDB Version:
1.2.0
DuckDB Client:
CLI / C++
Hardware:
No response
Full Name:
Ryan Melehan
Affiliation:
Coco Alemana
What is the latest build you tested with? If possible, we recommend testing with the latest nightly build.
I have tested with a stable release
Did you include all relevant data sets for reproducing the issue?
Not applicable - the reproduction does not require a data set
Did you include all code required to reproduce the issue?
- Yes, I have
Did you include all relevant configuration (e.g., CPU architecture, Python version, Linux distribution) to reproduce the issue?
- Yes, I have
Activity
github-actions commentedon Apr 11, 2026
This issue is stale because it has been open 365 days with no activity. Remove stale label or comment or this will be closed in 30 days.
IfkumRfnl commentedon Apr 12, 2026
I dug into a fix for this issue, seems like the root cause is mapping
'm'only toset_dot_nl(false)hereduckdb/src/function/scalar/string/regexp/regexp_util.cpp
Lines 22 to 64 in fc55355
That setting controls whether
.matches newlines. It does not enable multiline anchor behavior for^and$. InRE2’s default Perl-style mode, multiline anchor semantics are not driven byRE2::Options; they are expressed inline via(?m).So one solution is to add
(?m)to the pattern when the option'm'is set. But there is one more issue on the optimizer side.duckdb/src/optimizer/rule/regex_optimizations.cpp
Lines 150 to 221 in fc55355
Here it replaces some
regexp_matchescalls intocontains(...) / LIKEcalls (without checking anything special for the'm'option). There is also a test for this.duckdb/test/optimizer/regex_optimizer.test
Lines 232 to 248 in fc55355
If I make the optimizer analyze the normalized
(?m)pattern directly, it becomes too conservative and stops optimizing safe cases likeregexp_matches(s, 'hello', 'm')and if I keep the old optimizer logic unchanged, then anchored patterns likeregexp_matches(s, '^Some', 'm')can still be optimized incorrectly.I see two reasonable options:
^/$outside char classes make multiline mode semantically relevant.@Mytherin what do you think about this? Which direction would be preferable here?
Mytherin commentedon Apr 14, 2026
I think this is just a documentation issue, all the
'm'option controls is whether or not the dot matches a newline, i.e.:It does not enable multi-line mode, which is enabled through
(?m). Multi-line mode affects anchors, i.e. it controls whether or not^/$match at line boundaries vs matching the start/end of the entire string. Multi-line mode does not affect whether or not dot matches newlines. For example, you can see that'm'and's'have different behavior also in multi-line matching mode:I don't see any reason to change any of this functionality or move it around (in backwards incompatible ways). The functionality is available and works as expected - we just need to improve documentation of these features.
Mytherin commentedon Apr 14, 2026
I think the confusion here stems from the fact that
mconflicts with RE2's own flags, which uses(?m)to signal multi-line anchor mode and(?s)to signal "dot matches nl". I suppose since'm'is the default it also does not offer much utility as a flag. We could consider deprecating it, but I would not silently change the behavior of this existing option at this point.IfkumRfnl commentedon Apr 14, 2026
Got it, thanks for clarification. I agree that the behavior shouldn't be changed.
Fix regex 'm'/'n'/'p' option not enabling newline-sensitive matching
Fix regex 'm'/'n'/'p' option not enabling newline-sensitive matching
clang-format: apply canonical formatting to fork fix commits (duckdb#…
Fix regex 'm'/'n'/'p' option not enabling newline-sensitive matching
Fix regex 'm'/'n'/'p' option not enabling newline-sensitive matching (d…
Fix regex 'm'/'n'/'p' option not enabling newline-sensitive matching (d…
Fix regex 'm'/'n'/'p' option not enabling newline-sensitive matching (d…
Fix regex 'm'/'n'/'p' option not enabling newline-sensitive matching (d…
Fix regex 'm'/'n'/'p' option not enabling newline-sensitive matching (d…
Fix regex 'm'/'n'/'p' option not enabling newline-sensitive matching (d…
Fix regex 'm'/'n'/'p' option not enabling newline-sensitive matching (d…
Fix regex 'm'/'n'/'p' option not enabling newline-sensitive matching (d…
Fix regex 'm'/'n'/'p' option not enabling newline-sensitive matching (d…
Fix regex 'm'/'n'/'p' option not enabling newline-sensitive matching (d…
Fix regex 'm'/'n'/'p' option not enabling newline-sensitive matching (d…
Fix regex 'm'/'n'/'p' option not enabling newline-sensitive matching (d…
Fix regex 'm'/'n'/'p' option not enabling newline-sensitive matching (d…
Fix regex 'm'/'n'/'p' option not enabling newline-sensitive matching (d…