Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
311 changes: 305 additions & 6 deletions crates/rupg-func/src/regex/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -113,12 +113,21 @@ impl Regex {
///
/// `2201B` for a pattern that is too complex.
pub(crate) fn find(&self, s: &str) -> Result<Option<tree::Groups>> {
let chars: Vec<u32> = 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<Option<tree::Groups>> {
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<u32> = 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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -407,6 +421,273 @@ fn regexp_like(_: &Call<'_>, args: &[Value]) -> Result<Value> {
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<Vec<tree::Groups>> {
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<i64> {
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<Value> {
let chars: Vec<u32> = 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<Value> {
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<Value> {
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<Value> {
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<u32> {
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<Value> {
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<u32> = 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<Option<(usize, usize)>> {
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<Value> {
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<u32> = 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<Value> {
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<u32> = 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<Vec<Vec<Value>>> {
let (cflags, glob) = options(args)?;
let re = compile(arg(args, 1)?, cflags)?;
let chars: Vec<u32> = 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<Vec<String>> {
let cflags = all_flags(args, 2, function)?;
let re = compile(arg(args, 1)?, cflags)?;
let chars: Vec<u32> = 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<Vec<Vec<Value>>> {
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<Value> {
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<Kernel> {
Some(match src {
Expand All @@ -420,6 +701,24 @@ pub(crate) fn by_src(src: &str) -> Option<Kernel> {
"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,
})
}
Expand Down
6 changes: 5 additions & 1 deletion crates/rupg-func/src/srf.rs
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -24,6 +24,10 @@ pub fn set_kernel(func: u32) -> Option<SetKernel> {
"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,
Expand Down
6 changes: 6 additions & 0 deletions crates/rupg-session/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading
Loading