Repository navigation
MDEV-39520: REGEXP_INSTR(): add the optional MySQL 8.0 arguments - #5035
MohamedM216 wants to merge 1 commit into
Conversation
d2ac796 to
ef7d499
Compare
gkodinov
left a comment
There was a problem hiding this comment.
Thank you for your contribution! This is a preliminary review.
First of all: this is implementing just one sub-task of what https://jira.mariadb.org/browse/MDEV-39106 calls for. Either do all of it, as scoped in the jira, in a single go. Or create Jira sub-task(s) into the above jira and target these instead.
There's also some suggestions below.
REGEXP_INSTRREGEXP_INSTR
d0e479a to
2634104
Compare
gkodinov
left a comment
There was a problem hiding this comment.
Please fix the failing buildbot tests. Some improvement suggestions below too.
2634104 to
1518554
Compare
|
The current failing tests aren't related to my patch and they pass locally. |
gkodinov
left a comment
There was a problem hiding this comment.
LGTM. Please stand by for the final review.
sanja-byelkin
left a comment
There was a problem hiding this comment.
Found implementation problem should be fixed. Also performance and functionality test should be made after code review done
| { | ||
| char mt_buf[64]; | ||
| String mt_tmp(mt_buf, sizeof(mt_buf), &my_charset_latin1); | ||
| String *match_type_str= args[5]->val_str(&mt_tmp); |
There was a problem hiding this comment.
If It was constant and flags was calculated on fix field what sens to repeat here again and again the same exercise?
1518554 to
96f67b0
Compare
|
Thanks @sanja-byelkin for the detailed feedback! |
|
Hi @sanja-byelkin, just following up on this PR. I'd appreciate your feedback. Thanks! |
There was a problem hiding this comment.
Pull request overview
This PR extends MariaDB’s REGEXP_INSTR() implementation to support MySQL 8.0-style extended syntax (2–6 arguments: pos, occurrence, return_option, match_type) and adds an MTR test to validate compatibility and edge cases.
Changes:
- Switch
REGEXP_INSTRfunction creation to native-function parsing to accept 2–6 arguments. - Implement extended-argument semantics in
Item_func_regexp_instr(positioning, occurrences, return option, match_type flag parsing). - Add mysql-test coverage for the extended syntax (
regexp_instr_mysql8.test/.result).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| sql/item_create.cc | Updates function factory to accept 2–6 args for REGEXP_INSTR. |
| sql/item_cmpfunc.h | Extends regexp processor API and updates Item_func_regexp_instr ctor to take an argument list. |
| sql/item_cmpfunc.cc | Implements MySQL 8.0 extended REGEXP_INSTR behavior, including match_type parsing and occurrence scanning. |
| mysql-test/main/regexp_instr_mysql8.test | Adds a new MTR test covering the extended syntax and edge cases. |
| mysql-test/main/regexp_instr_mysql8.result | Expected results for the new MTR test. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // args[1]: pattern (may be recompiled per row) | ||
| if ((null_value= re.recompile(args[1]))) | ||
| return 0; | ||
|
|
|
I buildbot above tests failing with memory leak around regexp |
|
@sanja-byelkin I believe these failures are unrelated to my PR; the |
96f67b0 to
3152786
Compare
gkodinov
left a comment
There was a problem hiding this comment.
LGTM. Please consider squashing the two commits before the end of the review.
A stylistic change proposed below.
3152786 to
14cbc27
Compare
ff294d6 to
d638ca5
Compare
It's MDEV-39520. I've updated the commit message. Sorry for confusion. |
d638ca5 to
18a2b36
Compare
|
@sanja-byelkin Thanks for the detailed feedback. I've pushed a new patch. Looking forward to your review. Thanks! |
18a2b36 to
f9ecca4
Compare
|
https://buildbot.mariadb.org/#builders/719/builds/18033 has environment-specific encoding issue. It passes locally and on other buidbots. Also the only part that fails is the Multibyte characters. |
|
It looks like the fail connected to the fix https://buildbot.mariadb.org/#/builders/719/builds/18033/steps/9/logs/stdio |
gkodinov
left a comment
There was a problem hiding this comment.
Requesting changes on Sanja's behalf
9bb64dd to
11a699c
Compare
@sanja-byelkin @gkodinov Fixed. Since we can't reliably force the mysqltest client's file-I/O charset from within the .test file via SQL, I've replaced the raw multibyte strings with |
gkodinov
left a comment
There was a problem hiding this comment.
I do not mind a good workaround. Approved. With one suggestion.
|
@gkodinov I've already added the original strings as comments above each query in the |
not really. there's one you've missed though. |
11a699c to
98558d5
Compare
|
@gkodinov Yeah! You mean the last query. Actually, it uses the same string as the query before it. I've added the comment again, BTW. |
gkodinov
left a comment
There was a problem hiding this comment.
LGTM. Please keep working with the final reviewer.
|
Hi @sanja-byelkin, I'm just following up on this PR. Thanks! |
sanja-byelkin
left a comment
There was a problem hiding this comment.
Review of 98558d5. I built both this PR and MySQL 8.4.13 (bundled ICU 77.1) and ran the same queries on both; "MySQL:" values below are from that run, not from the manual (the manual says nothing about errors for bad arguments).
Blocker
- Zero-length match advance is measured at the wrong place (
sql/item_cmpfunc.cc,val_int(), thematch_end == match_startbranch). After an empty match the code adds the length of the character atbyte_offset(where the search started) instead of the character atmatch_end. With mixed character widths the next search skips a character or starts inside one:Measure the character atSELECT REGEXP_INSTR(_utf8mb4 X'CEB26161', '(?=a)', 1, 2); -- 'βaa': this PR 0, MySQL 3 SELECT REGEXP_INSTR(_utf8mb4 X'61CEB2CEB2', _utf8mb4 X'283F3DCEB229', 1, 2); -- 'aββ','(?=β)': this PR 0 + warning 1139 -- "Regex error 'bad offset into UTF string'", MySQL 3
subject_ptr + match_endand add both cases to the test.
Behaviour that differs from MySQL 8.0
- pos past the end. MySQL raises error 3686 "Index out of bounds in regular expression search." already for
pos = CHAR_LENGTH + 1on a non-empty subject (REGEXP_INSTR('abc','c',4)); this PR returns 0, and the test records 0 for exactly that query. For an empty subject MySQL returns 0 for any pos, including 0. - pos below 1 (0, -1, 18446744073709551615): MySQL error 3686, this PR error 1583. With an empty subject MySQL returns 0 for pos 0, this PR raises 1583.
- occurrence below 1: MySQL returns the first match (
REGEXP_INSTR('abcabc','b',1,0)and...,1,-5)give 2); this PR raises error 1583. - return_option not 0/1: MySQL raises 1210 "Incorrect arguments to regexp_instr: return_option must be 1 or 0." (at prepare for a constant); this PR raises 1583, whose text names no argument. The same 1583 for pos/occurrence/return_option does not tell the user which argument is wrong.
- Unknown match_type letter: MySQL 1210 "Incorrect arguments to regexp_instr"; this PR 1411 ER_WRONG_VALUE_FOR_TYPE.
-
'u'is accepted and ignored. MySQL maps it to UREGEX_UNIX_LINES. With PCRE2 configured for LF it happens to change nothing, but the code comment "no direct PCRE2 equivalent" is wrong (PCRE2_NEWLINE_LF /pcre2_set_newline()). Please fix the comment, or implement it. -
@@default_regex_flagsvs match_type (my earlier question): any match_type, including'', clears MULTILINE and DOTALL coming from@@default_regex_flags:In MySQL a missing match_type andSET default_regex_flags='DOTALL'; SELECT REGEXP_INSTR('a\nb','a.b'); -- 1 SELECT REGEXP_INSTR('a\nb','a.b',1,1,0,''); -- 0
''are the same thing (there is no such variable). Please either override only the flags a letter names, or document and test this. The comment "The MySQL 8.0 behavior is that match_type fully overrides the default flags" should state what MariaDB does instead of referring to MySQL. - Empty patterns in the test.
REGEXP_INSTR('hello',''),('','')and('abc','',1,N)all give error 3685 "Illegal argument to a regular expression." in MySQL. MariaDB accepted the 2-argument form before this patch, so this is not a regression, but the new occurrence tests with''record behaviour MySQL does not have; please say so in the test or drop them.
Correctness / robustness
- Constant match_type evaluated at prepare without
!is_expensive()(fix_length_and_dec()).fix_owner()skips expensive constant patterns; the match_type path does not, so a subquery or stored function as match_type runs during prepare, andthd->is_error()is not checked afterval_str(). (A two-row subquery still ends with error 1242 in my run, so I did not see a user-visible failure, but the pattern differs from the sibling.) - match_type is parsed as raw bytes. A ucs2/utf16
'i'is rejected withIncorrect match_type value: '' for function regexp_instr: the message shows the value as empty. MySQL rejects it too (1210), so this is about the message: please convert to ASCII before parsing, or at least format the value withErrConvString. -
const_cast<String*>(match_type_str)->c_ptr_safe()in the error path can write to / reallocate the argument item's own buffer; useErrConvString(match_type_str).ptr(). - Hardcoded
"regexp_instr"inRegexp_processor_pcre::init(match_type, ...). The processor is shared by all REGEXP functions, and the PR says the others follow; pass the owner'sfunc_name(). - Per-row recompilation with a non-constant match_type:
init()emptiesm_prev_pattern, socompile()runs on every row even if neither pattern nor flags changed, and the code bypassesrecompile()by hand. Remember the flags the pattern was compiled with and recompile only when they change. This also bears on the performance test I asked for earlier, which is still needed. - match_type evaluated twice per row (
args[5]->is_null()thenval_str()); dropis_null(), thenull_valuecheck afterval_str()covers it. - pos/occurrence ignore
unsigned_flag: 18446744073709551615 wraps to -1. - Nullability (my earlier comment):
Item_func::fix_fields()already ORs MAYBE_NULL from all arguments, so the new loop afterfix_owner()is a no-op and can go. One case remains: with a non-constant match_type, a failed per-rowcompile()returns NULL even when no argument is nullable. -
m_match_type_is_constis not initialized in the constructor.
Code structure and style
- The fetch / NULL / range-check /
my_errorblock is repeated for args 2, 3, 4;check_arguments()repeats the int check three times (check_argument_types_can_return_int(2, MY_MIN(arg_count, 5))exists); the match_type read (mt_buf[64],val_str,re.init) is duplicated infix_length_and_dec()andval_int(). Please factor. - The
init()overload has a different contract from the existinginit()(can fail, raises an error, drops the pattern cache), so consider a separateset_match_type()called afterinit(), which also avoids recomputing the case rule; and document it (// false = OK, true = erroris all there is now). - Coding style:
a = b/ column-aligned assignments instead ofa= b, trailing whitespace on six new lines, C-style casts(size_t),(longlong),(int). - Comments: the
m_prev_patternreset comment limits it to "non-constant match_type with a constant pattern" but it runs on every call; say whyrecompile()is skipped for a non-constant match_type; say pos counts characters; "1=end pos" should be "position after the end of the match"; drop comments that restate the code (args[5]: match_type string).
Tests
- No error path is tested: pos 0 / negative / past the end, occurrence 0, return_option 2, an unknown letter (constant and from a column), 1 and 7 arguments.
- No zero-length match on a multibyte subject (would have caught the blocker); only utf8mb4 subjects; NULL only as literals.
- "Non-constant match_type with constant pattern" uses rows
'c'and'ic', which both resolve to case-sensitive and both print 0, so the result cannot show a per-row flag change — use'c'and'i'. - The multibyte section still uses plain
#comments that do not reach the result file (my earlier--echocomment). - Header says "Improve REGEXP_INSTR for MySQL 8.0 compatibility" (neither the ticket title nor the commit subject); footer should be
--echo # End of 13.2 tests.
Commit message
- The body says the multibyte section keeps the test file "100% pure ASCII", but the
#comments there contain raw UTF-8. - Please describe the user-visible behaviour: the new errors for bad arguments, the
@@default_regex_flagsinteraction, and that the result can now be NULL from the new arguments. The first paragraph could name the use case (queries ported from MySQL 8.0 failing with error 1582). The PR description ("one of a series") cannot serve as the squashed commit message; use the commit's own message.
|
Just to be clear I do not request one-to-one MySQL compatibilities just all incompatibilities (which we do not can/want fix) should be listed (probably so far commit message is the best place) |
add8548 to
e3b6f45
Compare
|
Hi @sanja-byelkin , thanks for the feedback!
FIXED.
applied and tests are updated. The differences between MariaDB and MySQL are listed in the commit message and in the PR description. Thanks! |
REGEXP_INSTRREGEXP_INSTR(): add the optional MySQL 8.0 arguments
REGEXP_INSTR(subject, pattern[, pos[, occurrence[, return_option[,
match_type]]]])
Implement the optional arguments that MySQL 8.0 added to REGEXP_INSTR():
- pos 1-based character position to start the search from
(default 1),
- occurrence the 1-based number of the match to report (default 1),
- return_option 0 = position of the first character of the match,
1 = position right after the match end (default 0),
- match_type 'c', 'i', 'm', 'n', 'u' -- the same letters as in
REGEXP_LIKE()/REGEXP_SUBSTR()/REGEXP_REPLACE().
All positions are character positions: the byte offsets returned by
PCRE2 (in the library charset, utf8mb4) are converted to 1-based code
point positions with CHARSET_INFO::numchars(), the same way MySQL 8.0
(ICU) counts them.
After a zero-length match the next occurrence cannot be found at the
same position again: the search continues one character past the end
of the match, exactly as MySQL 8.0 (ICU) continues one code point past
an empty match. The character length is read at the end of the match,
not at the position the search started from: a zero-width match like
"(?=x)" is normally found away from the position the search started
at, and restarting the search inside a multibyte character would make
PCRE2 report a UTF-8 error.
A constant match_type is parsed at prepare time (a constant NULL
match_type makes every row return NULL, a constant invalid letter
raises the error at prepare time). A non-constant match_type is
re-parsed and re-applied per row; Regexp_processor_pcre::compile()
caches the compiled pattern keyed on both the pattern and the library
flags, so the regular expression is only recompiled when the effective
flags actually change.
match_type is parsed byte by byte as ASCII. Charsets that are not
ASCII-compatible (ucs2, utf16, utf32) are converted to latin1 first,
so both the parsing and the error message operate on the real value,
not on raw bytes.
Error handling:
- a NULL subject/pattern/pos/occurrence/return_option/match_type
argument makes the result NULL;
- a negative pos/occurrence/return_option, a zero pos or occurrence,
and a return_option outside {0,1} raise
ER_WRONG_PARAMETERS_TO_NATIVE_FCT;
- an invalid match_type letter raises ER_WRONG_VALUE_FOR_TYPE;
- a pos or occurrence above the 32-bit range can never match and makes
the function return 0;
- if the requested occurrence does not exist, the function returns 0.
The new test mysql-test/main/regexp_instr_mysql8.test covers all
2..6-argument forms, multibyte subjects, zero-length matches (including
zero-width lookaheads found away from the search start), NULL and
unsigned argument propagation, non-constant match_type (recompilation
through the (pattern, flags) cache), prepared statements and the error
paths.
Differences from MySQL 8.0
--------------------------
The MariaDB regexp functions run on PCRE2 rather than ICU. All of the
differences below are inherent to the existing MariaDB regexp
implementation (shared with REGEXP_LIKE/REGEXP_SUBSTR/REGEXP_REPLACE),
not newly introduced by this patch:
1. An empty pattern is accepted and matches at every character
position. MySQL 8.0 rejects it with ER_REGEXP_ILLEGAL_ARGUMENT
(3685, "Illegal argument to a regular expression").
2. The error codes differ. Out-of-range pos/occurrence/return_option
raise ER_WRONG_PARAMETERS_TO_NATIVE_FCT (1210) and an invalid
match_type letter raises ER_WRONG_VALUE_FOR_TYPE (1411); MySQL 8.0
reports regexp-specific ICU errors instead.
3. The 'u' match_type letter (Unix-only line endings) is accepted as a
no-op: PCRE2 has no direct equivalent of ICU's Unix-only line end
handling.
4. The MariaDB-only @@default_regex_flags variable remains in effect;
a match_type argument only overrides the case sensitivity,
multiline and dotall parts of the flags.
5. The regexp dialect is PCRE2, so Unicode property classes, case
folding and some pattern constructs can behave differently from ICU
in edge cases.
6. A subject whose bytes are not valid UTF-8 (possible with a binary
literal introduced as _utf8mb4) makes PCRE2 report a UTF-8 error,
which is pushed as a warning and treated as "no match" by
REGEXP_INSTR(). MySQL 8.0 (ICU) may behave differently for such
input. This is deliberately not covered by the test file, because
the warning text is PCRE2-version specific.
e3b6f45 to
80a090e
Compare
Reference Issue
MDEV-39520
Description
This PR implements the MySQL 8.0 extension of
REGEXP_INSTR():pos: the 1-based character position to start the search from (default 1)occurrence: which occurrence of a match to report (default 1)return_option: 0 = position of the match start, 1 = position after the match end (default 0)match_type:'c','i','m','n','u', same letters asREGEXP_LIKE()Examples
Implementation notes
Item_func_regexp_instr::val_int()iterates the matches withRegexp_processor_pcre::exec()and converts the byte offset of the requested occurrence into a 1-based character position withCHARSET_INFO::numchars(), the same way MySQL 8.0 (ICU) counts positions.(?=x)is normally found away from the search start, and restarting the search inside a multibyte character makes PCRE2 report a UTF-8 error.match_typeis parsed at prepare time. A non-constant one is re-applied per row, andRegexp_processor_pcre::compile()caches the compiled pattern keyed on (pattern, library flags), so the regex is only recompiled when the effective flags actually change.match_typeletters are parsed, so both the parsing and the error messages see the real value, not raw bytes.posoroccurrence, areturn_optionoutside{0,1}raiseER_WRONG_PARAMETERS_TO_NATIVE_FCT; an invalidmatch_typeletter raisesER_WRONG_VALUE_FOR_TYPE; an argument above the 32-bit range can never match and yields 0.Differences from MySQL 8.0
As agreed with the @sanja-byelkin , all remaining incompatibilities are documented in
the commit message. In short:
ER_REGEXP_ILLEGAL_ARGUMENT); pre-existing MariaDB behaviour of the 2-argument form.ER_WRONG_PARAMETERS_TO_NATIVE_FCT,ER_WRONG_VALUE_FOR_TYPEinstead of the ICU-specificER_REGEXP_*).'u'match_type letter is accepted as a no-op.@@default_regex_flagsremains in effect; amatch_typeargument only overrides the case/multiline/dotall parts.Test
New test
mysql-test/main/regexp_instr_mysql8.testcovers all 2..6-argument forms, multibyte subjects, zero-length matches and zero-width lookaheads (found away from the search start), NULL and unsigned argument propagation, non-constantmatch_type(recompilation through the (pattern, flags) cache), prepared statements, and the error paths.