Skip to content

Commit f8a7eac

Browse files
committed
REGEXP_SUBSTR computed: raise for a pattern matching empty text
A start at or past the end leaves empty text, where DuckDB finds an empty match ('' for x*) and Snowflake's result is unrecorded: the receipts cover p = length + 1 and beyond only with a pattern that cannot match empty text. Such a pattern now raises regexp_pattern_syntax on this path, as REGEXP_COUNT and REGEXP_INSTR already do. The 15 recorded cases still match; the other 12 REGEXP_SUBSTR case files are unchanged (178 cases, 0 failed).
1 parent 5840b2d commit f8a7eac

2 files changed

Lines changed: 4 additions & 2 deletions

File tree

‎crates/sqlquack-rules/src/catalogue/constants.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1062,7 +1062,7 @@ pub const RULES: &[RuleSpec] = &[
10621062
"fn:REGEXP_SUBSTR",
10631063
"node:regexp_like",
10641064
],
1065-
conditions: "A literal pattern without lookarounds, group modifiers, lazy quantifiers (except +?, *? and ?? in REGEXP_SUBSTR, recorded reading as optional greedy repetitions, so as *, * and ?), POSIX classes or escapes other than \\d \\s \\w (ASCII, as recorded), their negations, escaped punctuation and, in REGEXP_SUBSTR and REGEXP_SUBSTR_ALL, \\n, \\t, \\r and, outside a bracket expression, the ASCII word boundary \\b, as recorded beside digits, underscores, punctuation and non-ASCII letters. REGEXP_LIKE matches the whole text; REGEXP_REPLACE replaces every match, or only the first, from position 1, case-sensitively or with 'i', with group references only; REGEXP_SUBSTR from position 1, any occurrence, with parameters of c, i, m, s and e and a group, which implies e (1 with e and without one, 0 without either), NULL when there is no match or the group is past the pattern's last; a position, occurrence or group from a NUMBER column is read as recorded: NULL gives NULL, a position or occurrence below 1 or a negative group raises 100050, a start position past the end gives NULL and ^ anchors at the start position (a pattern with \\b and such a position is not converted); REGEXP_SUBSTR_ALL the same, every match as an ARRAY of strings, empty without one. A ':' opening a bracket expression is literal, and ']' first in one is literal. REGEXP_LIKE also takes a VARCHAR pattern from a column, with or without literal parameters, relying on regexp_pattern_syntax: it runs only where the pattern is literal characters, '.', groups, alternation, plain bracket expressions, single '*', '+' or '?' and anchors at its ends, and raises otherwise.",
1065+
conditions: "A literal pattern without lookarounds, group modifiers, lazy quantifiers (except +?, *? and ?? in REGEXP_SUBSTR, recorded reading as optional greedy repetitions, so as *, * and ?), POSIX classes or escapes other than \\d \\s \\w (ASCII, as recorded), their negations, escaped punctuation and, in REGEXP_SUBSTR and REGEXP_SUBSTR_ALL, \\n, \\t, \\r and, outside a bracket expression, the ASCII word boundary \\b, as recorded beside digits, underscores, punctuation and non-ASCII letters. REGEXP_LIKE matches the whole text; REGEXP_REPLACE replaces every match, or only the first, from position 1, case-sensitively or with 'i', with group references only; REGEXP_SUBSTR from position 1, any occurrence, with parameters of c, i, m, s and e and a group, which implies e (1 with e and without one, 0 without either), NULL when there is no match or the group is past the pattern's last; a position, occurrence or group from a NUMBER column is read as recorded: NULL gives NULL, a position or occurrence below 1 or a negative group raises 100050, a start position past the end gives NULL and ^ anchors at the start position (a pattern with \\b and such a position is not converted, and a pattern matching empty text raises regexp_pattern_syntax, its matches unrecorded); REGEXP_SUBSTR_ALL the same, every match as an ARRAY of strings, empty without one. A ':' opening a bracket expression is literal, and ']' first in one is literal. REGEXP_LIKE also takes a VARCHAR pattern from a column, with or without literal parameters, relying on regexp_pattern_syntax: it runs only where the pattern is literal characters, '.', groups, alternation, plain bracket expressions, single '*', '+' or '?' and anchors at its ends, and raises otherwise.",
10661066
failures: REGEXP_PARAMETER_FAILURES,
10671067
},
10681068
RuleSpec {

‎crates/sqlquack-rules/src/conversion/_helpers/expressions/text_functions.rs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -672,10 +672,12 @@ fn regexp_substr_computed(
672672
&format!("{INVALID_PARAMETER_MESSAGE} {reason}"),
673673
))
674674
};
675+
let empty_match = empty_match_guard("REGEXP_SUBSTR", &pattern, (construct, site))?;
675676
Ok(Emitted::new(
676677
format!(
677678
"list_transform([struct_pack(t := {text}, p := {position}, o := {occurrence}, \
678-
g := {group})], lambda r: CASE WHEN r['t'] IS NULL OR r['p'] IS NULL \
679+
g := {group})], lambda r: CASE WHEN {empty_match} THEN CAST(NULL AS VARCHAR) \
680+
WHEN r['t'] IS NULL OR r['p'] IS NULL \
679681
OR r['o'] IS NULL OR r['g'] IS NULL THEN NULL \
680682
WHEN r['p'] < 1 THEN CAST({} AS VARCHAR) WHEN r['o'] < 1 THEN CAST({} AS VARCHAR) \
681683
WHEN r['g'] < 0 THEN CAST({} AS VARCHAR) \

0 commit comments

Comments
 (0)