From 483a79111f648c1868221ee82b52ce789618d78a Mon Sep 17 00:00:00 2001 From: tamnd Date: Sat, 10 Oct 2026 02:05:16 +0700 Subject: [PATCH] Add the regular expression functions that find each match --- crates/rupg-func/src/regex/mod.rs | 311 +++++++++++++++++- crates/rupg-func/src/srf.rs | 6 +- crates/rupg-session/src/connection.rs | 6 + .../src/connection/regex_matches.test | 178 ++++++++++ 4 files changed, 494 insertions(+), 7 deletions(-) create mode 100644 crates/rupg-session/src/connection/regex_matches.test diff --git a/crates/rupg-func/src/regex/mod.rs b/crates/rupg-func/src/regex/mod.rs index d5162b4..b31f745 100644 --- a/crates/rupg-func/src/regex/mod.rs +++ b/crates/rupg-func/src/regex/mod.rs @@ -2,7 +2,7 @@ //! //! The lexer and the parser are ports of `regc_lex.c` and `regcomp.c`, so that a pattern gives the same error as in PostgreSQL. The matcher is not a port: it is a backtracking machine that tells if a match is in the string and where the leftmost match starts. The positions of the match and of its groups follow the rules of preference of Spencer: the tree of subexpressions of `regcomp.c` and the dissection of `regexec.c` find them. //! -//! The functions here are the operators `~`, `~*`, `!~` and `!~*`, `substring` with a pattern, `similar_to_escape`, `regexp_match` and `regexp_like`. +//! The functions here are the operators `~`, `~*`, `!~` and `!~*`, `substring` with a pattern, `similar_to_escape`, `regexp_replace`, `regexp_match`, `regexp_matches`, `regexp_like`, `regexp_count`, `regexp_instr`, `regexp_substr` and the functions that split a string at the matches. mod exec; mod lex; @@ -113,12 +113,21 @@ impl Regex { /// /// `2201B` for a pattern that is too complex. pub(crate) fn find(&self, s: &str) -> Result> { + let chars: Vec = s.chars().map(u32::from).collect(); + self.find_chars(&chars, 0) + } + + /// [`Regex::find`] in the characters `chars` from the character `from`. The constraints see all the characters, also the characters before `from`. + /// + /// # Errors + /// + /// `2201B` for a pattern that is too complex. + pub(crate) fn find_chars(&self, chars: &[u32], from: usize) -> Result> { let tree = self.tree.get_or_init(|| tree::Tree::new(&self.parsed)); let tree = tree.as_ref().map_err(|code| code.error())?; - let chars: Vec = s.chars().map(u32::from).collect(); - let mut from = 0; - while let Some(begin) = self.prog.first(&chars, from) { - if let Some(groups) = tree.find(&chars, begin) { + let mut from = from; + while let Some(begin) = self.prog.first(chars, from) { + if let Some(groups) = tree.find(chars, begin) { return Ok(Some(groups)); } from = begin + 1; @@ -369,9 +378,14 @@ fn re_flags(options: Option<&str>) -> Result<(u32, bool)> { Ok((cflags, glob)) } +/// The options of a function as its argument at `at`, when it has one. +fn flags_at(args: &[Value], at: usize) -> Result<(u32, bool)> { + re_flags(if args.len() > at { Some(arg(args, at)?) } else { None }) +} + /// The options of a function as its third argument, when it has one. fn options(args: &[Value]) -> Result<(u32, bool)> { - re_flags(if args.len() > 2 { Some(arg(args, 2)?) } else { None }) + flags_at(args, 2) } /// The error for the option `g` of a function that finds one match. @@ -407,6 +421,273 @@ fn regexp_like(_: &Call<'_>, args: &[Value]) -> Result { Ok(Value::Bool(compile(arg(args, 1)?, cflags)?.is_match(arg(args, 0)?))) } +/// The text of the characters `chars` from `so` up to `eo`. +fn text(chars: &[u32], (so, eo): (usize, usize)) -> String { + chars[so..eo].iter().filter_map(|&c| char::from_u32(c)).collect() +} + +/// The position after a match that starts at `so` and ends at `eo`, where the next search starts. The search after an empty match starts one character later, or it finds the same match again. +fn next_search((so, eo): (usize, usize)) -> usize { + if so == eo { eo + 1 } else { eo } +} + +/// The span of the whole match. +fn whole(groups: &tree::Groups) -> Result<(usize, usize)> { + groups.first().copied().flatten().ok_or_else(|| Error::internal("a match without a span")) +} + +/// `setup_regexp_matches`: the spans of each match from the character `start`, or of the first match only when `glob` is false. With `subpatterns`, a match has the span of each group when the pattern has groups. Else it has the span of the whole match. With `ignore_degenerate`, an empty match at the end of the string or just after a match does not count. +fn setup_matches( + chars: &[u32], + re: &Regex, + start: usize, + glob: bool, + subpatterns: bool, + ignore_degenerate: bool, +) -> Result> { + let subpatterns = subpatterns && re.groups() > 0; + let mut out = Vec::new(); + let mut search = start; + let mut prev_end = 0; + while search <= chars.len() { + let Some(groups) = re.find_chars(chars, search)? else { break }; + let (so, eo) = whole(&groups)?; + if !ignore_degenerate || (so < chars.len() && eo > prev_end) { + out.push(if subpatterns { groups[1..].to_vec() } else { groups[..1].to_vec() }); + } + prev_end = eo; + if !glob { + break; + } + search = next_search((so, eo)); + } + Ok(out) +} + +/// The error for a bad value of an integer parameter. +fn bad_parameter(name: &str, value: i64) -> Error { + Error::new( + SqlState::INVALID_PARAMETER_VALUE, + format!("invalid value for parameter \"{name}\": {value}"), + ) +} + +/// The integer parameter `name` at `at`, or `default` when the call does not have it. `valid` tells the values that the parameter takes. +fn param( + args: &[Value], + (at, name): (usize, &str), + default: i64, + valid: impl Fn(i64) -> bool, +) -> Result { + if args.len() <= at { + return Ok(default); + } + let value = args.get(at).and_then(Value::as_i64).ok_or_else(bad_value)?; + if valid(value) { Ok(value) } else { Err(bad_parameter(name, value)) } +} + +/// The character where the search starts, from the parameter `start` that counts from 1. +fn start_at(start: i64) -> usize { + usize::try_from(start - 1).unwrap_or(0) +} + +/// `appendStringInfoRegexpSubstr`: the replacement with `\1` to `\9` as the text of each group, `\&` as the text of the match and `\\` as one backslash. A backslash before another character stays. +fn substitute(out: &mut String, replacement: &str, groups: &tree::Groups, chars: &[u32]) { + let mut it = replacement.chars().peekable(); + while let Some(c) = it.next() { + if c != '\\' { + out.push(c); + continue; + } + let span = match it.peek() { + Some(&d @ '1'..='9') => groups.get(d as usize - '0' as usize).copied().flatten(), + Some('&') => groups.first().copied().flatten(), + Some('\\') => { + it.next(); + out.push('\\'); + continue; + } + _ => { + out.push('\\'); + continue; + } + }; + it.next(); + if let Some(span) = span { + out.push_str(&text(chars, span)); + } + } +} + +/// `replace_text_regexp`: `s` with the match number `n` from the character `start` replaced, or each match when `n` is 0. +fn replace(s: &str, re: &Regex, replacement: &str, start: usize, n: i64) -> Result { + let chars: Vec = s.chars().map(u32::from).collect(); + let mut out = String::new(); + let mut done = 0; + let mut search = start; + let mut count = 0; + while search <= chars.len() { + let Some(groups) = re.find_chars(&chars, search)? else { break }; + let span = whole(&groups)?; + count += 1; + if n > 0 && count != n { + search = next_search(span); + continue; + } + out.push_str(&text(&chars, (done, span.0))); + substitute(&mut out, replacement, &groups, &chars); + done = span.1; + if n > 0 { + break; + } + search = next_search(span); + } + out.push_str(&text(&chars, (done, chars.len()))); + Ok(Value::Text(out)) +} + +/// `textregexreplace_noopt`: `regexp_replace(string, pattern, replacement)`, which replaces the first match. +fn textregexreplace_noopt(_: &Call<'_>, args: &[Value]) -> Result { + let re = compile(arg(args, 1)?, flags::ADVANCED)?; + replace(arg(args, 0)?, &re, arg(args, 2)?, 0, 1) +} + +/// `textregexreplace`: `regexp_replace(string, pattern, replacement, flags)`. Options that start with a digit give an error with a hint, as the call may need the form with a start. +fn textregexreplace(_: &Call<'_>, args: &[Value]) -> Result { + let options = arg(args, 3)?; + if let Some(first) = options.chars().next().filter(char::is_ascii_digit) { + return Err(Error::new( + SqlState::INVALID_PARAMETER_VALUE, + format!("invalid regular expression option: \"{first}\""), + ) + .with_hint("If you meant to use regexp_replace() with a start parameter, cast the fourth argument to integer explicitly.")); + } + let (cflags, glob) = re_flags(Some(options))?; + let re = compile(arg(args, 1)?, cflags)?; + replace(arg(args, 0)?, &re, arg(args, 2)?, 0, i64::from(!glob)) +} + +/// `textregexreplace_extended`: `regexp_replace(string, pattern, replacement, start [, n [, flags]])`. Without `n`, the option `g` tells if each match is replaced. +fn textregexreplace_extended(_: &Call<'_>, args: &[Value]) -> Result { + let start = param(args, (3, "start"), 1, |v| v > 0)?; + let n = param(args, (4, "n"), 1, |v| v >= 0)?; + let (cflags, glob) = flags_at(args, 5)?; + let n = if args.len() <= 4 { i64::from(!glob) } else { n }; + let re = compile(arg(args, 1)?, cflags)?; + replace(arg(args, 0)?, &re, arg(args, 2)?, start_at(start), n) +} + +/// The options at `at` of a function that finds each match, which do not take `g`. +fn all_flags(args: &[Value], at: usize, function: &str) -> Result { + let (cflags, glob) = flags_at(args, at)?; + if glob { + return Err(no_global(function)); + } + Ok(cflags) +} + +/// `regexp_count(string, pattern [, start [, flags]])`: the number of the matches. +fn regexp_count(_: &Call<'_>, args: &[Value]) -> Result { + let start = param(args, (2, "start"), 1, |v| v > 0)?; + let cflags = all_flags(args, 3, "regexp_count()")?; + let re = compile(arg(args, 1)?, cflags)?; + let chars: Vec = arg(args, 0)?.chars().map(u32::from).collect(); + let found = setup_matches(&chars, &re, start_at(start), true, false, false)?; + Ok(Value::Int4(i32::try_from(found.len()).unwrap_or(i32::MAX))) +} + +/// The span of the group `subexpr` of the match number `n` from the character `start` for `regexp_instr` and `regexp_substr`, or of the whole match when `subexpr` is 0. `None` when the string has fewer matches or the pattern fewer groups. +fn nth_span( + chars: &[u32], + re: &Regex, + start: i64, + n: i64, + subexpr: i64, +) -> Result> { + let found = setup_matches(chars, re, start_at(start), true, subexpr > 0, false)?; + let npatterns = if subexpr > 0 && re.groups() > 0 { re.groups() } else { 1 }; + let (Ok(n), Ok(subexpr)) = (usize::try_from(n), usize::try_from(subexpr)) else { + return Ok(None); + }; + if n > found.len() || subexpr > npatterns { + return Ok(None); + } + Ok(found[n - 1][subexpr.saturating_sub(1)]) +} + +/// `regexp_instr(string, pattern [, start [, n [, endoption [, flags [, subexpr]]]]])`: the position from 1 of the start of the match, or of the character after it when `endoption` is 1, or 0. +fn regexp_instr(_: &Call<'_>, args: &[Value]) -> Result { + let start = param(args, (2, "start"), 1, |v| v > 0)?; + let n = param(args, (3, "n"), 1, |v| v > 0)?; + let endoption = param(args, (4, "endoption"), 0, |v| v == 0 || v == 1)?; + let subexpr = param(args, (6, "subexpr"), 0, |v| v >= 0)?; + let cflags = all_flags(args, 5, "regexp_instr()")?; + let re = compile(arg(args, 1)?, cflags)?; + let chars: Vec = arg(args, 0)?.chars().map(u32::from).collect(); + let pos = nth_span(&chars, &re, start, n, subexpr)? + .map_or(0, |(so, eo)| if endoption == 1 { eo + 1 } else { so + 1 }); + Ok(Value::Int4(i32::try_from(pos).unwrap_or(i32::MAX))) +} + +/// `regexp_substr(string, pattern [, start [, n [, flags [, subexpr]]]])`: the text of the match, or null. +fn regexp_substr(_: &Call<'_>, args: &[Value]) -> Result { + let start = param(args, (2, "start"), 1, |v| v > 0)?; + let n = param(args, (3, "n"), 1, |v| v > 0)?; + let subexpr = param(args, (5, "subexpr"), 0, |v| v >= 0)?; + let cflags = all_flags(args, 4, "regexp_substr()")?; + let re = compile(arg(args, 1)?, cflags)?; + let chars: Vec = arg(args, 0)?.chars().map(u32::from).collect(); + Ok(nth_span(&chars, &re, start, n, subexpr)? + .map_or(Value::Null, |span| Value::Text(text(&chars, span)))) +} + +/// `regexp_matches(string, pattern [, flags])`: one row for each match, or for the first match without the option `g`. Each row is an array with the text of each group, or of the match when the pattern has no group. +pub(crate) fn regexp_matches(_: &Call<'_>, args: &[Value]) -> Result>> { + let (cflags, glob) = options(args)?; + let re = compile(arg(args, 1)?, cflags)?; + let chars: Vec = arg(args, 0)?.chars().map(u32::from).collect(); + let found = setup_matches(&chars, &re, 0, glob, true, false)?; + Ok(found + .iter() + .map(|spans| { + let values = spans.iter().map(|span| span.map(|span| Value::Text(text(&chars, span)))); + vec![Value::Array(Box::new(Array::one(values.collect())))] + }) + .collect()) +} + +/// The parts of the string between the matches, for `regexp_split_to_table` and `regexp_split_to_array`. An empty match at the end of the string or just after a match does not split. +fn split(args: &[Value], function: &str) -> Result> { + let cflags = all_flags(args, 2, function)?; + let re = compile(arg(args, 1)?, cflags)?; + let chars: Vec = arg(args, 0)?.chars().map(u32::from).collect(); + let found = setup_matches(&chars, &re, 0, true, false, true)?; + let mut parts = Vec::with_capacity(found.len() + 1); + let mut from = 0; + for groups in &found { + let (so, eo) = whole(groups)?; + parts.push(text(&chars, (from, so))); + from = eo; + } + parts.push(text(&chars, (from, chars.len()))); + Ok(parts) +} + +/// `regexp_split_to_table(string, pattern [, flags])`: one row for each part. +pub(crate) fn regexp_split_to_table(_: &Call<'_>, args: &[Value]) -> Result>> { + Ok(split(args, "regexp_split_to_table()")? + .into_iter() + .map(|part| vec![Value::Text(part)]) + .collect()) +} + +/// `regexp_split_to_array(string, pattern [, flags])`: the parts as an array. +fn regexp_split_to_array(_: &Call<'_>, args: &[Value]) -> Result { + let parts = split(args, "regexp_split_to_array()")?; + let values = parts.into_iter().map(|part| Some(Value::Text(part))).collect(); + Ok(Value::Array(Box::new(Array::one(values)))) +} + /// The kernels of the regular expression functions by `prosrc`. pub(crate) fn by_src(src: &str) -> Option { Some(match src { @@ -420,6 +701,24 @@ pub(crate) fn by_src(src: &str) -> Option { "similar_escape" => similar_escape, "regexp_match" | "regexp_match_no_flags" => regexp_match, "regexp_like" | "regexp_like_no_flags" => regexp_like, + "textregexreplace_noopt" => textregexreplace_noopt, + "textregexreplace" => textregexreplace, + "textregexreplace_extended" + | "textregexreplace_extended_no_n" + | "textregexreplace_extended_no_flags" => textregexreplace_extended, + "regexp_count" | "regexp_count_no_start" | "regexp_count_no_flags" => regexp_count, + "regexp_instr" + | "regexp_instr_no_start" + | "regexp_instr_no_n" + | "regexp_instr_no_endoption" + | "regexp_instr_no_flags" + | "regexp_instr_no_subexpr" => regexp_instr, + "regexp_substr" + | "regexp_substr_no_start" + | "regexp_substr_no_n" + | "regexp_substr_no_flags" + | "regexp_substr_no_subexpr" => regexp_substr, + "regexp_split_to_array" | "regexp_split_to_array_no_flags" => regexp_split_to_array, _ => return None, }) } diff --git a/crates/rupg-func/src/srf.rs b/crates/rupg-func/src/srf.rs index f0316fd..71a8945 100644 --- a/crates/rupg-func/src/srf.rs +++ b/crates/rupg-func/src/srf.rs @@ -1,4 +1,4 @@ -//! The set-returning functions that a query can call in `FROM`: `generate_series` of the integer types, `unnest`, `generate_subscripts`, `string_to_table`, `pg_options_to_table`, `aclexplode`, `pg_get_keywords`, `pg_show_all_settings`, `pg_stat_get_activity`, `pg_stat_get_backend_idset`, `pg_prepared_statement`, the functions of the views of replication, of progress and of the prepared transactions, the functions of the statistics of the whole server that give rows, the functions of the views of asynchronous I/O, shared memory, replication origins, recovery, publications and cursors, and the functions of the views of the time zones. +//! The set-returning functions that a query can call in `FROM`: `generate_series` of the integer types, `unnest`, `generate_subscripts`, `string_to_table`, `regexp_matches`, `regexp_split_to_table`, `pg_options_to_table`, `aclexplode`, `pg_get_keywords`, `pg_show_all_settings`, `pg_stat_get_activity`, `pg_stat_get_backend_idset`, `pg_prepared_statement`, the functions of the views of replication, of progress and of the prepared transactions, the functions of the statistics of the whole server that give rows, the functions of the views of asynchronous I/O, shared memory, replication origins, recovery, publications and cursors, and the functions of the views of the time zones. //! //! A set kernel gives all the rows of the call at once, as a function of PostgreSQL in the materialize mode gives a tuplestore. Each row has one value for each column of the result: one value for a function of a scalar type, or one value for each `OUT` parameter. @@ -24,6 +24,10 @@ pub fn set_kernel(func: u32) -> Option { "array_unnest" => unnest, "generate_subscripts" | "generate_subscripts_nodir" => subscripts, "text_to_table" | "text_to_table_null" => text_to_table, + "regexp_matches" | "regexp_matches_no_flags" => crate::regex::regexp_matches, + "regexp_split_to_table" | "regexp_split_to_table_no_flags" => { + crate::regex::regexp_split_to_table + } "pg_options_to_table" => options_to_table, "aclexplode" => crate::acl::aclexplode, "pg_get_keywords" => keywords, diff --git a/crates/rupg-session/src/connection.rs b/crates/rupg-session/src/connection.rs index 735b49d..77cb255 100644 --- a/crates/rupg-session/src/connection.rs +++ b/crates/rupg-session/src/connection.rs @@ -1895,6 +1895,12 @@ mod tests { big_stack(|| script(include_str!("connection/regex_functions.test"))); } + /// The functions of regular expressions that find each match: `regexp_replace` in each form with the replacement escapes, `regexp_count`, `regexp_instr`, `regexp_substr`, `regexp_matches` and `regexp_split_to_table` in `FROM`, and `regexp_split_to_array`. The cases include empty matches, a start after the first character, the characters of more than one byte, each error and a view. `connection/regex_matches.test` is the output of PostgreSQL 19 for the same script in a new database. + #[test] + fn regex_matches() { + big_stack(|| script(include_str!("connection/regex_matches.test"))); + } + /// `format_type` of `interval` with each range and precision that a typmod can have, the error for a bad range, `_pg_interval_type` of `information_schema`, and the interval types of a view in `pg_get_viewdef` and `pg_attribute`. `connection/interval_typmods.test` is the output of PostgreSQL 19 for the same script in a new database. #[test] fn interval_typmods() { diff --git a/crates/rupg-session/src/connection/regex_matches.test b/crates/rupg-session/src/connection/regex_matches.test new file mode 100644 index 0000000..74c94bf --- /dev/null +++ b/crates/rupg-session/src/connection/regex_matches.test @@ -0,0 +1,178 @@ +> SELECT regexp_replace('foobarbaz', 'b..', 'X'), regexp_replace('foobarbaz', 'b..', 'X', 'g'), regexp_replace('foobarbaz', 'b(..)', 'X\1Y', 'g'), regexp_replace('fooBarBaz', 'b(..)', 'X\1Y', 'gi'), regexp_replace('abc', '(b)', '[\&|\1|\2|\\|\x|\]'), regexp_replace('abc', 'b', '\'), regexp_replace('abc', '', '-', 'g'), regexp_replace('abc', 'x*', '-', 'g'), regexp_replace('', 'x*', '-', 'g') +fields regexp_replace:25:-1:-1,regexp_replace:25:-1:-1,regexp_replace:25:-1:-1,regexp_replace:25:-1:-1,regexp_replace:25:-1:-1,regexp_replace:25:-1:-1,regexp_replace:25:-1:-1,regexp_replace:25:-1:-1,regexp_replace:25:-1:-1 +row fooXbaz,fooXX,fooXarYXazY,fooXarYXazY,a[b|b||\|\x|\]c,a\c,-a-b-c-,-a-b-c-,- +SELECT 1 +> SELECT regexp_replace('A PostgreSQL function', 'a|e|i|o|u', 'X', 1, 0, 'i'), regexp_replace('A PostgreSQL function', 'a|e|i|o|u', 'X', 1, 3, 'i'), regexp_replace('A PostgreSQL function', 'a|e|i|o|u', 'X', 9), regexp_replace('A PostgreSQL function', 'a|e|i|o|u', 'X', 9, 2), regexp_replace('abc', 'b', 'X', 10), regexp_replace('aaa', 'a*?', '-', 1, 0), regexp_replace('aaa', 'a', '-', 2, 0), regexp_replace('héllo wörld', '[éö]', '_', 'g') +fields regexp_replace:25:-1:-1,regexp_replace:25:-1:-1,regexp_replace:25:-1:-1,regexp_replace:25:-1:-1,regexp_replace:25:-1:-1,regexp_replace:25:-1:-1,regexp_replace:25:-1:-1,regexp_replace:25:-1:-1 +row X PXstgrXSQL fXnctXXn,A PostgrXSQL function,A PostgrXSQL function,A PostgreSQL fXnction,abc,-a-a-a-,a--,h_llo w_rld +SELECT 1 +> SELECT regexp_replace('abc', 'b', 'X', '1') +ERROR 22023 invalid regular expression option: "1" +> SELECT regexp_replace('abc', 'b', 'X', 0) +ERROR 22023 invalid value for parameter "start": 0 +> SELECT regexp_replace('abc', 'b', 'X', 1, -1) +ERROR 22023 invalid value for parameter "n": -1 +> SELECT regexp_replace('abc', 'b', 'X', 1, 1, 'z') +ERROR 22023 invalid regular expression option: "z" +> SELECT regexp_count('ABCABCAXYaxy', 'A.'), regexp_count('ABCABCAXYaxy', 'A.', 1, 'i'), regexp_count('ABCABCAXYaxy', 'A.', 4), regexp_count('abc', 'x*'), regexp_count('abc', 'x*', 3), regexp_count('abc', 'x*', 4), regexp_count('abc', 'x*', 5), regexp_count('aaa', 'a*?'), regexp_count('aaa', '(a)\1'), regexp_count('', '') +fields regexp_count:23:4:-1,regexp_count:23:4:-1,regexp_count:23:4:-1,regexp_count:23:4:-1,regexp_count:23:4:-1,regexp_count:23:4:-1,regexp_count:23:4:-1,regexp_count:23:4:-1,regexp_count:23:4:-1,regexp_count:23:4:-1 +row 3,4,2,4,2,1,0,4,1,1 +SELECT 1 +> SELECT regexp_count('abc', 'b', 0) +ERROR 22023 invalid value for parameter "start": 0 +> SELECT regexp_count('abc', 'b', 1, 'g') +ERROR 22023 regexp_count() does not support the "global" option +> SELECT regexp_instr('number of your street, town zip, FR', '[^,]+', 1, 2), regexp_instr('ABCDEFGHI', '(c..)(...)', 1, 1, 0, 'i', 2), regexp_instr('ABCDEFGHI', '(c..)(...)', 1, 1, 1, 'i', 2), regexp_instr('abcabc', 'b', 3), regexp_instr('abcabc', 'b', 1, 3), regexp_instr('abc', 'b', 1, 1, 0, '', 1), regexp_instr('abc', 'b', 1, 1, 0, '', 2), regexp_instr('abc', '(x)?b', 1, 1, 0, '', 1), regexp_instr('abc', 'b', 1, 1, 1), regexp_instr('abc', 'x'), regexp_instr('aaa', 'a*?', 1, 2), regexp_instr('aaa', '(a*?)(a*)', 1, 1, 1, '', 2) +fields regexp_instr:23:4:-1,regexp_instr:23:4:-1,regexp_instr:23:4:-1,regexp_instr:23:4:-1,regexp_instr:23:4:-1,regexp_instr:23:4:-1,regexp_instr:23:4:-1,regexp_instr:23:4:-1,regexp_instr:23:4:-1,regexp_instr:23:4:-1,regexp_instr:23:4:-1,regexp_instr:23:4:-1 +row 23,6,9,5,0,2,0,0,3,0,2,1 +SELECT 1 +> SELECT regexp_instr('abc', 'b', 1, 0) +ERROR 22023 invalid value for parameter "n": 0 +> SELECT regexp_instr('abc', 'b', 1, 1, 2) +ERROR 22023 invalid value for parameter "endoption": 2 +> SELECT regexp_instr('abc', 'b', 1, 1, 0, '', -1) +ERROR 22023 invalid value for parameter "subexpr": -1 +> SELECT regexp_instr('abc', 'b', 1, 1, 0, 'g') +ERROR 22023 regexp_instr() does not support the "global" option +> SELECT regexp_substr('number of your street, town zip, FR', '[^,]+', 1, 2), regexp_substr('ABCDEFGHI', '(c..)(...)', 1, 1, 'i', 2), regexp_substr('abcabc', 'b.', 3), regexp_substr('abc', 'x'), regexp_substr('abc', '(x)?b', 1, 1, '', 1), regexp_substr('abc', 'b', 1, 1, '', 1), regexp_substr('abc', 'b', 1, 1, '', 2), regexp_substr('aaa', 'a*?', 1, 3), regexp_substr('aaa', '(a+?)(a*)', 1, 1, '', 2) +fields regexp_substr:25:-1:-1,regexp_substr:25:-1:-1,regexp_substr:25:-1:-1,regexp_substr:25:-1:-1,regexp_substr:25:-1:-1,regexp_substr:25:-1:-1,regexp_substr:25:-1:-1,regexp_substr:25:-1:-1,regexp_substr:25:-1:-1 +row town zip,FGH,bc,NULL,NULL,b,NULL,, +SELECT 1 +> SELECT regexp_substr('abc', 'b', 1, 1, 'g') +ERROR 22023 regexp_substr() does not support the "global" option +> SELECT regexp_substr('abc', 'b', -1) +ERROR 22023 invalid value for parameter "start": -1 +> SELECT * FROM regexp_matches('foobarbequebaz', '(bar)(beque)') +fields regexp_matches:1009:-1:-1 +row {bar,beque} +SELECT 1 +> SELECT * FROM regexp_matches('foobarbequebazilbarfbonk', '(b[^b]+)(b[^b]+)', 'g') +fields regexp_matches:1009:-1:-1 +row {bar,beque} +row {bazil,barf} +SELECT 2 +> SELECT * FROM regexp_matches('abc', 'x') +fields regexp_matches:1009:-1:-1 +SELECT 0 +> SELECT * FROM regexp_matches('abc', '', 'g') +fields regexp_matches:1009:-1:-1 +row {""} +row {""} +row {""} +row {""} +SELECT 4 +> SELECT * FROM regexp_matches('abc', 'b|(x)', 'g') +fields regexp_matches:1009:-1:-1 +row {NULL} +SELECT 1 +> SELECT * FROM regexp_matches('aaa', 'a*?', 'g') +fields regexp_matches:1009:-1:-1 +row {""} +row {""} +row {""} +row {""} +SELECT 4 +> SELECT m, n FROM regexp_matches('a1b2c3', '[a-z](\d)', 'g') WITH ORDINALITY AS t(m, n) +fields m:1009:-1:-1,n:20:8:-1 +row {1},1 +row {2},2 +row {3},3 +SELECT 3 +> SELECT * FROM regexp_matches('abc', 'b', 'z') +fields regexp_matches:1009:-1:-1 +ERROR 22023 invalid regular expression option: "z" +> SELECT * FROM regexp_split_to_table('the quick brown fox', '\s+') +fields regexp_split_to_table:25:-1:-1 +row the +row quick +row brown +row fox +SELECT 4 +> SELECT * FROM regexp_split_to_table('the quick brown fox', '\s*') +fields regexp_split_to_table:25:-1:-1 +row t +row h +row e +row q +row u +row i +row c +row k +row b +row r +row o +row w +row n +row f +row o +row x +SELECT 16 +> SELECT * FROM regexp_split_to_table('abc', '') +fields regexp_split_to_table:25:-1:-1 +row a +row b +row c +SELECT 3 +> SELECT * FROM regexp_split_to_table('', 'x') +fields regexp_split_to_table:25:-1:-1 +row +SELECT 1 +> SELECT * FROM regexp_split_to_table('xaxbx', 'x') +fields regexp_split_to_table:25:-1:-1 +row +row a +row b +row +SELECT 4 +> SELECT * FROM regexp_split_to_table('aXbxc', 'x', 'i') +fields regexp_split_to_table:25:-1:-1 +row a +row b +row c +SELECT 3 +> SELECT * FROM regexp_split_to_table('abc', 'b', 'g') +fields regexp_split_to_table:25:-1:-1 +ERROR 22023 regexp_split_to_table() does not support the "global" option +> SELECT regexp_split_to_array('the quick brown fox', '\s+'), regexp_split_to_array('abc', ''), regexp_split_to_array('', 'x'), regexp_split_to_array('a,b,,c', ','), regexp_split_to_array('abc', '(b)'), regexp_split_to_array('ab', 'x*') +fields regexp_split_to_array:1009:-1:-1,regexp_split_to_array:1009:-1:-1,regexp_split_to_array:1009:-1:-1,regexp_split_to_array:1009:-1:-1,regexp_split_to_array:1009:-1:-1,regexp_split_to_array:1009:-1:-1 +row {the,quick,brown,fox},{a,b,c},{""},{a,b,"",c},{a,c},{a,b} +SELECT 1 +> SELECT regexp_split_to_array('abc', 'b', 'g') +ERROR 22023 regexp_split_to_array() does not support the "global" option +> SELECT regexp_count('abab', '(?<=a)b', 2), regexp_instr('xab', '^a', 2), regexp_instr('xab', 'a', 2), regexp_replace('abcdefghijk', '(a)(b)(c)(d)(e)(f)(g)(h)(i)(j)', '\10'), regexp_instr('héllo wörld', 'ö'), regexp_substr('héllo wörld', 'w.r'), regexp_count('aAaA', 'a', 1, 'i'), regexp_replace('a.b.c', '.', '-', 'gq'), regexp_count('abcabc', '\yabc', 2), regexp_count(repeat('ab', 50), 'b') +fields regexp_count:23:4:-1,regexp_instr:23:4:-1,regexp_instr:23:4:-1,regexp_replace:25:-1:-1,regexp_instr:23:4:-1,regexp_substr:25:-1:-1,regexp_count:23:4:-1,regexp_replace:25:-1:-1,regexp_count:23:4:-1,regexp_count:23:4:-1 +row 2,0,2,a0k,8,wör,4,a-b-c,0,50 +SELECT 1 +> SELECT * FROM regexp_matches('xaxbx', '(?:^|x)(.)', 'g') +fields regexp_matches:1009:-1:-1 +row {a} +row {b} +SELECT 2 +> SELECT * FROM regexp_split_to_table('héllo wörld', 'ö|\s') +fields regexp_split_to_table:25:-1:-1 +row héllo +row w +row rld +SELECT 3 +> CREATE VIEW rxw AS SELECT regexp_replace(s, 'b', 'X', 'g') AS a, regexp_count(s, 'b') AS b, regexp_instr(s, 'c', 1, 1, 1) AS c, regexp_substr(s, '.c') AS d, regexp_split_to_array(s, '') AS e, regexp_replace(s, 'b', 'Y', 1, 0) AS f FROM (VALUES ('abcbc')) AS t(s) +CREATE VIEW +> SELECT pg_get_viewdef('rxw'::regclass, true) +fields pg_get_viewdef:25:-1:-1 +row SELECT regexp_replace(s, 'b'::text, 'X'::text, 'g'::text) AS a, + regexp_count(s, 'b'::text) AS b, + regexp_instr(s, 'c'::text, 1, 1, 1) AS c, + regexp_substr(s, '.c'::text) AS d, + regexp_split_to_array(s, ''::text) AS e, + regexp_replace(s, 'b'::text, 'Y'::text, 1, 0) AS f + FROM ( VALUES ('abcbc'::text)) t(s); +SELECT 1 +> SELECT * FROM rxw +fields a@rxw.1:25:-1:-1,b@rxw.2:23:4:-1,c@rxw.3:23:4:-1,d@rxw.4:25:-1:-1,e@rxw.5:1009:-1:-1,f@rxw.6:25:-1:-1 +row aXcXc,2,4,bc,{a,b,c,b,c},aYcYc +SELECT 1 +> SELECT r.n, m FROM (VALUES (1, 'a1b22'), (2, 'c333')) AS r(n, s), LATERAL regexp_matches(r.s, '\d+', 'g') AS m ORDER BY 1, 2 +fields n:23:4:-1,m:1009:-1:-1 +row 1,{1} +row 1,{22} +row 2,{333} +SELECT 3