use rudb_common::{Error, LogicalType, Result, Value};
use rudb_regex::{Options, Regex};
use rudb_vector::{Data, StringColumn, Vector};
use crate::number::integral;
use crate::scalar::{each_string, finish, over_valid};
use crate::shape::nulls_of;
pub(crate) fn is_regexp(name: &str) -> bool {
matches!(name, "regexp_replace" | "regexp_matches" | "regexp_full_match" | "regexp_extract")
}
pub(crate) fn vectorized<V: AsRef<Vector>>(
name: &str,
args: &[V],
returns: &LogicalType,
rows: usize,
) -> Result<Option<Vector>> {
let Some(text) = args.first().map(AsRef::as_ref) else {
return Ok(None);
};
let Some(source) = Source::of(text) else {
return Ok(None);
};
let mut constants: Vec<&Value> = Vec::new();
for arg in args.iter().skip(1) {
let Some(value) = arg.as_ref().constant_value() else {
return Ok(None);
};
constants.push(value);
}
let Some(call) = Call::read(name, &constants)? else {
return Ok(None);
};
let base = nulls_of(text);
match (name, returns) {
("regexp_replace", LogicalType::Varchar) => {
let out = each_string(rows, &base, |index, into| {
into.push(&call.regex.replace(source.get(index), &call.rewrite, call.global));
});
finish(returns, Data::Varlen(out), base.normalize(rows))
}
("regexp_extract", LogicalType::Varchar) => {
let out = each_string(rows, &base, |index, into| {
into.push(call.regex.extract(source.get(index), call.group).unwrap_or_default());
});
finish(returns, Data::Varlen(out), base.normalize(rows))
}
("regexp_matches" | "regexp_full_match", LogicalType::Boolean) => {
let whole = name == "regexp_full_match";
let mut out = vec![false; rows];
let validity = over_valid(rows, base, |index| {
let text = source.get(index);
out[index] =
if whole { call.regex.is_full_match(text) } else { call.regex.is_match(text) };
Ok(())
})?;
finish(returns, Data::Bool(out.into()), validity)
}
_ => Ok(None),
}
}
pub(crate) fn value(name: &str, args: &[Value]) -> Result<Value> {
let Some(Value::Varchar(text)) = args.first() else {
return Err(Error::internal(format!("{name} of something that is not a string")));
};
let constants: Vec<&Value> = args.iter().skip(1).collect();
let Some(call) = Call::read(name, &constants)? else {
return Err(Error::internal(format!("{name} with arguments it does not have")));
};
Ok(match name {
"regexp_replace" => Value::Varchar(call.regex.replace(text, &call.rewrite, call.global)),
"regexp_extract" => {
Value::Varchar(call.regex.extract(text, call.group).unwrap_or_default().to_string())
}
"regexp_full_match" => Value::Boolean(call.regex.is_full_match(text)),
_ => Value::Boolean(call.regex.is_match(text)),
})
}
struct Call {
regex: Regex,
rewrite: String,
global: bool,
group: usize,
}
impl Call {
fn read(name: &str, constants: &[&Value]) -> Result<Option<Self>> {
let Some(Value::Varchar(pattern)) = constants.first().copied() else {
return Ok(None);
};
let mut rewrite = String::new();
let mut rest = &constants[1..];
if name == "regexp_replace" {
let Some(Value::Varchar(held)) = rest.first().copied() else {
return Ok(None);
};
rewrite = held.clone();
rest = &rest[1..];
}
let mut group = 0;
let mut spelling = "";
for value in rest {
match value {
Value::Varchar(held) => spelling = held,
other => {
group = integral(other)
.and_then(|held| usize::try_from(held).ok())
.unwrap_or(usize::MAX);
}
}
}
let options = Options::parse(spelling)?;
let regex = Regex::with_options(pattern, options)?;
Ok(Some(Self { regex, rewrite, global: options.global, group }))
}
}
enum Source<'a> {
Flat(&'a StringColumn),
Dictionary(&'a [u32], &'a StringColumn),
}
impl<'a> Source<'a> {
fn of(vector: &'a Vector) -> Option<Self> {
if *vector.logical_type() != LogicalType::Varchar {
return None;
}
if let Some(Data::Varlen(column)) = vector.data() {
return Some(Self::Flat(column));
}
let (codes, values) = vector.dictionary_parts()?;
match values.data() {
Some(Data::Varlen(column)) => Some(Self::Dictionary(codes, column)),
_ => None,
}
}
fn get(&self, index: usize) -> &'a str {
match self {
Self::Flat(column) => column.get(index).unwrap_or_default(),
Self::Dictionary(codes, values) => {
codes.get(index).and_then(|&code| values.get(code as usize)).unwrap_or_default()
}
}
}
}