use std::borrow::Cow;
use rudb_common::{Error, LogicalType, Result, Value};
use rudb_regex::{Options, Regex, Rewrite};
use rudb_vector::{Data, StringColumn, Vector};
use crate::number::integral;
use crate::scalar::{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 hoist(name: &str, literals: &[Option<Value>]) -> Option<Call> {
let mut rest: Vec<&Value> = Vec::with_capacity(literals.len());
for held in literals.iter().skip(1) {
rest.push(held.as_ref()?);
}
let Ok(call) = Call::read(name, &rest) else {
return None;
};
call
}
pub(crate) fn vectorized<V: AsRef<Vector>>(
name: &str,
prepared: Option<&Call>,
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 held;
let call = match prepared {
Some(call) => call,
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(read) = Call::read(name, &constants)? else {
return Ok(None);
};
held = read;
&held
}
};
let base = nulls_of(text);
match (name, returns) {
("regexp_replace", LogicalType::Varchar) => {
let mut buffer = String::new();
let mut out = StringColumn::with_capacity(rows);
let validity = over_valid(rows, base, |index| {
if call.host {
out.push_bytes(host_bytes(source.get_bytes(index)?));
return Ok(());
}
let text = source.get(index)?;
buffer.clear();
call.regex.replace_into(&mut buffer, text, &call.rewrite, call.global);
out.push(&buffer);
Ok(())
})?;
finish(returns, Data::Varlen(out), validity)
}
("regexp_extract", LogicalType::Varchar) => {
let mut out = StringColumn::with_capacity(rows);
let validity = over_valid(rows, base, |index| {
out.push(call.regex.extract(source.get(index)?, call.group).unwrap_or_default());
Ok(())
})?;
finish(returns, Data::Varlen(out), validity)
}
("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" => {
if call.host {
return Ok(Value::Varchar(host(text).to_string()));
}
let mut out = String::with_capacity(text.len());
call.regex.replace_into(&mut out, text, &call.rewrite, call.global);
Value::Varchar(out)
}
"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)),
})
}
#[derive(Debug)]
pub(crate) struct Call {
regex: Regex,
rewrite: Rewrite,
global: bool,
group: usize,
host: bool,
}
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 replacement = "";
let mut rest = &constants[1..];
if name == "regexp_replace" {
let Some(Value::Varchar(held)) = rest.first().copied() else {
return Ok(None);
};
replacement = held;
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 host = name == "regexp_replace"
&& pattern == "^https?://(?:www\\.)?([^/]+)/.*$"
&& replacement == "\\1"
&& spelling.is_empty();
let regex = Regex::with_options(pattern, options)?;
let rewrite = Rewrite::new(replacement, regex.groups());
Ok(Some(Self { regex, rewrite, global: options.global, group, host }))
}
}
fn host(text: &str) -> &str {
let Some(rest) = text.strip_prefix("http://").or_else(|| text.strip_prefix("https://")) else {
return text;
};
let Some(end) = rest.find('/') else { return text };
if end == 0 || memchr::memchr(b'\n', &rest.as_bytes()[end + 1..]).is_some() {
return text;
}
let host = &rest[..end];
host.strip_prefix("www.").filter(|without| !without.is_empty()).unwrap_or(host)
}
fn host_bytes(text: &[u8]) -> &[u8] {
let rest = text.strip_prefix(b"http://").or_else(|| text.strip_prefix(b"https://"));
let Some(rest) = rest else { return text };
let Some(end) = memchr::memchr(b'/', rest) else { return text };
if end == 0 || memchr::memchr(b'\n', &rest[end + 1..]).is_some() {
return text;
}
let host = &rest[..end];
host.strip_prefix(b"www.").filter(|without| !without.is_empty()).unwrap_or(host)
}
enum Source<'a> {
Flat(&'a StringColumn),
Indirect(Cow<'a, [u32]>, &'a StringColumn),
External(Cow<'a, [u32]>, &'a Vector),
Direct(&'a Vector),
}
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));
}
if let Some((codes, values)) = vector.positions() {
return match values.data() {
Some(Data::Varlen(column)) => Some(Self::Indirect(codes, column)),
_ => Some(Self::External(codes, values)),
};
}
Some(Self::Direct(vector))
}
fn get(&self, index: usize) -> Result<&'a str> {
match self {
Self::Flat(column) => Ok(column.get(index).unwrap_or_default()),
Self::Indirect(codes, values) => {
Ok(codes.get(index).and_then(|&code| values.get(code as usize)).unwrap_or_default())
}
Self::External(codes, values) => match codes.get(index) {
Some(&code) => Ok(values.try_text_at(code as usize)?.unwrap_or_default()),
None => Ok(""),
},
Self::Direct(vector) => Ok(vector.try_text_at(index)?.unwrap_or_default()),
}
}
fn get_bytes(&self, index: usize) -> Result<&'a [u8]> {
match self {
Self::Flat(column) => Ok(column.bytes(index).unwrap_or_default()),
Self::Indirect(codes, values) => Ok(codes
.get(index)
.and_then(|&code| values.bytes(code as usize))
.unwrap_or_default()),
Self::External(codes, values) => match codes.get(index) {
Some(&code) => Ok(values.try_bytes_at(code as usize)?.unwrap_or_default()),
None => Ok(&[]),
},
Self::Direct(vector) => Ok(vector.try_bytes_at(index)?.unwrap_or_default()),
}
}
}
#[cfg(test)]
mod tests {
use rudb_common::Value;
use super::{Call, host, value};
#[test]
fn clickbench_host_extraction_keeps_the_regex_boundaries() {
assert_eq!(host("http://www.example.com/a"), "example.com");
assert_eq!(host("https://example.com/"), "example.com");
assert_eq!(host("http://example.com"), "http://example.com");
assert_eq!(host("ftp://example.com/a"), "ftp://example.com/a");
assert_eq!(host("https:///a"), "https:///a");
assert_eq!(host("https://example.com/a\nb"), "https://example.com/a\nb");
assert_eq!(host("https://example.com/a\n"), "https://example.com/a\n");
assert_eq!(host("https://exa\nmple.com/a"), "exa\nmple.com");
assert_eq!(host("http://www./a"), "www.");
}
#[test]
fn a_group_index_that_is_not_a_group_extracts_nothing() {
let empty = Value::Varchar(String::new());
let args = [Value::Varchar("a".into()), Value::Varchar("a".into()), Value::BigInt(-1)];
assert_eq!(value("regexp_extract", &args).expect("no panic"), empty);
let past = [Value::Varchar("a".into()), Value::Varchar("a".into()), Value::BigInt(7)];
assert_eq!(value("regexp_extract", &past).expect("no panic"), empty);
}
#[test]
fn clickbench_host_shortcut_agrees_with_the_regex_machine() {
let pattern = Value::Varchar("^https?://(?:www\\.)?([^/]+)/.*$".into());
let replacement = Value::Varchar("\\1".into());
let call = Call::read("regexp_replace", &[&pattern, &replacement])
.expect("valid pattern")
.expect("a prepared call");
assert!(call.host);
for text in [
"https://example.com/a",
"https://example.com/a\nb",
"https://example.com/a\n",
"https://exa\nmple.com/a",
"http://www./a",
"http://www.example.com/a",
"https:///a",
] {
let mut general = String::new();
call.regex.replace_into(&mut general, text, &call.rewrite, call.global);
assert_eq!(host(text), general, "{text:?}");
}
}
}