use std::cell::RefCell;
use rudb_common::{Error, LogicalType, Result, Value};
use rudb_regex::{Options, Regex};
pub(crate) fn before_nulls(name: &str, args: &[Value]) -> Option<Result<Value>> {
let regex = match name {
"string_split" | "str_split" | "string_to_array" | "split" => false,
"string_split_regex" | "str_split_regex" | "regexp_split_to_array" => true,
_ => return None,
};
Some(answer(regex, args))
}
fn answer(regex: bool, args: &[Value]) -> Result<Value> {
let (text, separator, spelling) = match args {
[text, separator] => (text, separator, ""),
[_, _, Value::Null] => {
return Err(Error::invalid_input("Regex options field must not be NULL"));
}
[text, separator, Value::Varchar(spelling)] => (text, separator, spelling.as_str()),
_ => return Err(Error::internal(format!("a split of {args:?}"))),
};
let Value::Varchar(text) = text else {
return Ok(Value::Null);
};
let pieces = match separator {
Value::Varchar(pattern) if regex => {
let options = Options::parse(spelling)?;
if options.global {
return Err(Error::invalid_input(
"Option 'g' (global replace) is only valid for regexp_replace",
));
}
with_compiled(pattern, options, |compiled| {
cut(text, |rest| compiled.find_at(rest, 0).map(|at| (at.start(), at.end())))
})?
}
Value::Varchar(separator) if separator.is_empty() => cut(text, |_| Some((0, 0))),
Value::Varchar(separator) => {
cut(text, |rest| rest.find(separator.as_str()).map(|at| (at, at + separator.len())))
}
_ => vec![text.as_str()],
};
let values = pieces.into_iter().map(|piece| Value::Varchar(piece.to_string())).collect();
Ok(Value::List { element: LogicalType::Varchar, values })
}
fn cut(text: &str, mut find: impl FnMut(&str) -> Option<(usize, usize)>) -> Vec<&str> {
let mut pieces = Vec::new();
let mut rest = text;
while !rest.is_empty() {
let Some((mut start, end)) = find(rest) else {
break;
};
let mut after = end;
if start == 0 && end == 0 {
start = rest.chars().next().map_or(0, char::len_utf8);
if start == rest.len() {
break;
}
after = start;
}
pieces.push(&rest[..start]);
rest = &rest[after..];
}
pieces.push(rest);
pieces
}
thread_local! {
static LAST: RefCell<Option<(String, Options, Regex)>> = const { RefCell::new(None) };
}
fn with_compiled<T>(pattern: &str, options: Options, body: impl FnOnce(&Regex) -> T) -> Result<T> {
LAST.with(|last| {
let mut last = last.borrow_mut();
let fresh = !matches!(&*last, Some((held, set, _)) if held == pattern && *set == options);
if fresh {
*last = Some((pattern.to_string(), options, Regex::with_options(pattern, options)?));
}
let Some((_, _, compiled)) = last.as_ref() else {
return Err(Error::internal("a split pattern that was just compiled"));
};
Ok(body(compiled))
})
}
#[cfg(test)]
mod tests {
use super::*;
fn split(name: &str, args: &[Value]) -> Result<Vec<String>> {
match before_nulls(name, args).expect("a split")? {
Value::List { values, .. } => Ok(values.iter().map(ToString::to_string).collect()),
other => panic!("{other:?}"),
}
}
fn text(value: &str) -> Value {
Value::Varchar(value.to_string())
}
#[test]
fn a_plain_split_keeps_the_empty_pieces_and_an_empty_separator_splits_characters() {
let plain = |a: &str, b: &str| split("string_split", &[text(a), text(b)]).unwrap();
assert_eq!(plain("a,b,,c", ","), ["a", "b", "", "c"]);
assert_eq!(plain("a,", ","), ["a", ""]);
assert_eq!(plain(",", ","), ["", ""]);
assert_eq!(plain("", ","), [""]);
assert_eq!(plain("", ""), [""]);
assert_eq!(plain("héllo", ""), ["h", "é", "l", "l", "o"]);
assert_eq!(plain("aXXbXc", "XX"), ["a", "bXc"]);
assert_eq!(split("split", &[text("a,b"), Value::Null]).unwrap(), ["a,b"]);
assert_eq!(
before_nulls("str_split", &[Value::Null, text(",")]).unwrap().unwrap(),
Value::Null
);
}
#[test]
fn a_regex_split_looks_again_in_what_is_left() {
let regex = |a: &str, b: &str| split("string_split_regex", &[text(a), text(b)]).unwrap();
assert_eq!(regex("a1b22c", "[0-9]+"), ["a", "b", "c"]);
assert_eq!(regex("a1b2", "^[a-z]"), ["", "1b2"]);
assert_eq!(regex("aaa", "a"), ["", "", "", ""]);
assert_eq!(regex("abc", "x*"), ["a", "b", "c"]);
assert_eq!(regex("abc", "$"), ["abc", ""]);
assert_eq!(regex("ab", "\\b"), ["a", "b"]);
let with = |options: Value| {
split("regexp_split_to_array", &[text("aXbxc"), text("x"), options])
.map_err(|error| error.to_string())
};
assert_eq!(with(text("i")).unwrap(), ["a", "b", "c"]);
assert!(with(text("g")).unwrap_err().contains("only valid for regexp_replace"));
assert!(with(text("z")).unwrap_err().contains("Unrecognized Regex option z"));
assert!(with(Value::Null).unwrap_err().contains("must not be NULL"));
}
}