use std::borrow::Cow;
use std::collections::HashMap;
use std::hash::{BuildHasherDefault, Hash, Hasher};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use rudb_common::{Error, LogicalType, Result, Value};
use rudb_regex::{Options, Regex, Rewrite};
use rudb_vector::{Data, StringColumn, TextSource, Validity, 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) => {
if let (Some(call), Some((codes, dictionary))) =
(prepared, text.stable_dictionary_parts())
{
return replace_stable(call, dictionary, codes, base, returns, rows);
}
let mut buffer = String::new();
let mut out = StringColumn::with_capacity(rows);
let validity = over_strings(rows, base, &mut out, |index, out| {
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_strings(rows, base, &mut out, |index, out| {
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,
stable: OnceLock<StableReplace>,
}
impl Call {
fn replaced<'t>(&self, text: &'t [u8], buffer: &'t mut String) -> Result<&'t [u8]> {
replace_one(&self.regex, &self.rewrite, self.global, self.host, text, buffer)
}
}
fn over_strings(
rows: usize,
base: Validity,
out: &mut StringColumn,
mut body: impl FnMut(usize, &mut StringColumn) -> Result<()>,
) -> Result<Validity> {
let validity = over_valid(rows, base, |index| {
while out.len() < index {
out.push("");
}
body(index, out)
})?;
while out.len() < rows {
out.push("");
}
Ok(validity)
}
fn replace_one<'t>(
regex: &Regex,
rewrite: &Rewrite,
global: bool,
host: bool,
text: &'t [u8],
buffer: &'t mut String,
) -> Result<&'t [u8]> {
if host {
return Ok(host_bytes(text));
}
let text = std::str::from_utf8(text)
.map_err(|_| Error::internal("a VARCHAR value that is not UTF-8"))?;
buffer.clear();
regex.replace_into(buffer, text, rewrite, global);
Ok(buffer.as_bytes())
}
const REPLACE_GROUP: usize = 1024;
const REPLACE_SHARDS: usize = 64;
#[derive(Debug)]
struct StableReplace {
memo: Arc<Memo>,
values: Option<Arc<Vector>>,
}
#[derive(Debug)]
struct Memo {
dictionary: Arc<Vector>,
regex: Regex,
rewrite: Rewrite,
global: bool,
host: bool,
groups: Vec<OnceLock<Box<[u32]>>>,
answers: Vec<OnceLock<Replaced>>,
firsts: Vec<Mutex<Seen>>,
kept: AtomicUsize,
}
#[derive(Debug)]
struct Replaced {
owns: Box<[u16]>,
spans: Box<[(u32, u32)]>,
bytes: Box<[u8]>,
}
impl Replaced {
fn get(&self, index: usize) -> &[u8] {
let Ok(index) = u16::try_from(index) else { return &[] };
self.owns.binary_search(&index).map_or(&[], |at| {
let (start, end) = self.spans[at];
&self.bytes[start as usize..end as usize]
})
}
fn footprint(&self) -> usize {
self.owns.len() * (size_of::<u16>() + size_of::<(u32, u32)>()) + self.bytes.len()
}
}
#[derive(Debug)]
struct Pending {
hash: u64,
start: u32,
end: u32,
index: u16,
}
impl Memo {
fn group(&self, code: usize) -> Result<&[u32]> {
let slot = self
.groups
.get(code / REPLACE_GROUP)
.ok_or_else(|| Error::internal("a stable dictionary code is out of range"))?;
if let Some(done) = slot.get() {
return Ok(done);
}
let answers = &self.answers[code / REPLACE_GROUP];
let first = code / REPLACE_GROUP * REPLACE_GROUP;
let last = (first + REPLACE_GROUP).min(self.dictionary.len());
let mut buffer = String::new();
let mut firsts = Vec::with_capacity(last - first);
let mut pending: Vec<Pending> = Vec::new();
let mut mine: Vec<u8> = Vec::new();
let mut previous = Vec::new();
let mut previous_found = None;
let mut local = Local::default();
let mut kept = Vec::new();
let mut at = first;
while at < last {
let stopped = self.dictionary.sweep_text(at, last, &mut |_, text: &[u8]| {
let index = firsts.len();
let own = u32::try_from(first + index)
.ok()
.filter(|&own| own != u32::MAX)
.ok_or_else(|| Error::internal("a dictionary past four billion values"))?;
let answer = replace_one(
&self.regex,
&self.rewrite,
self.global,
self.host,
text,
&mut buffer,
)?;
let found = match previous_found {
Some(found) if previous.as_slice() == answer => found,
_ => {
let hash = hash_of(answer);
let found = match local.get(&hash) {
Some(&(start, end, found))
if kept.get(start as usize..end as usize) == Some(answer) =>
{
found
}
known => {
let found = match self.first_of(hash, answer)? {
Some(found) => found,
None => {
let (Ok(start), Ok(end), Ok(index)) = (
u32::try_from(mine.len()),
u32::try_from(mine.len() + answer.len()),
u16::try_from(index),
) else {
return Err(Error::internal(
"a replaced group too large",
));
};
mine.extend_from_slice(answer);
pending.push(Pending { hash, start, end, index });
own
}
};
if known.is_none() {
let start = kept.len();
kept.extend_from_slice(answer);
if let (Ok(start), Ok(end)) =
(u32::try_from(start), u32::try_from(kept.len()))
{
local.insert(hash, (start, end, found));
}
}
found
}
};
previous.clear();
previous.extend_from_slice(answer);
previous_found = Some(found);
found
}
};
firsts.push(found);
Ok(())
})?;
if stopped <= at {
return Err(Error::internal("a dictionary sweep did not move"));
}
at = stopped;
}
let out = Replaced {
owns: pending.iter().map(|waiting| waiting.index).collect(),
spans: pending.iter().map(|waiting| (waiting.start, waiting.end)).collect(),
bytes: mine.as_slice().into(),
};
let held = out.footprint();
if answers.set(out).is_ok() {
self.kept.fetch_add(held, Ordering::Relaxed);
}
let mut moved: HashMap<u32, u32> = HashMap::new();
let mut added = 0;
for waiting in &pending {
let own = (first + usize::from(waiting.index)) as u32;
let answer = &mine[waiting.start as usize..waiting.end as usize];
let found = self.register(waiting.hash, answer, own, &mut added)?;
if found != own {
moved.insert(own, found);
}
}
if !moved.is_empty() {
for code in &mut firsts {
if let Some(&found) = moved.get(code) {
*code = found;
}
}
}
let firsts = firsts.into_boxed_slice();
let held = firsts.len() * size_of::<u32>();
if slot.set(firsts).is_ok() {
self.kept.fetch_add(held, Ordering::Relaxed);
}
self.kept.fetch_add(added, Ordering::Relaxed);
slot.get()
.map(|done| &**done)
.ok_or_else(|| Error::internal("a replaced group was set and is not there"))
}
fn first_of(&self, hash: u64, answer: &[u8]) -> Result<Option<u32>> {
let seen = self.firsts[shard_of(hash)]
.lock()
.map_err(|_| Error::internal("a replace memo lock is poisoned"))?;
Ok(seen.find(hash, |code| self.owned(code) == Some(answer)))
}
fn register(&self, hash: u64, answer: &[u8], own: u32, added: &mut usize) -> Result<u32> {
let mut seen = self.firsts[shard_of(hash)]
.lock()
.map_err(|_| Error::internal("a replace memo lock is poisoned"))?;
if let Some(found) = seen.find(hash, |code| self.owned(code) == Some(answer)) {
return Ok(found);
}
*added += seen.insert(hash, own);
Ok(own)
}
fn owned(&self, code: u32) -> Option<&[u8]> {
let code = code as usize;
let answers = self.answers.get(code / REPLACE_GROUP)?.get()?;
Some(answers.get(code % REPLACE_GROUP))
}
fn first(&self, code: usize) -> Result<u32> {
Ok(self.group(code)?[code % REPLACE_GROUP])
}
fn answer(&self, code: usize) -> Result<&[u8]> {
let first = self.first(code)?;
self.owned(first).ok_or_else(|| Error::internal("a replaced code has no answer"))
}
}
fn shard_of(hash: u64) -> usize {
(hash >> 32) as usize % REPLACE_SHARDS
}
fn hash_of(answer: &[u8]) -> u64 {
let mut words = Words::default();
answer.hash(&mut words);
words.finish()
}
type Local = HashMap<u64, (u32, u32, u32), BuildHasherDefault<Stored>>;
#[derive(Debug, Default)]
struct Seen {
slots: Vec<u64>,
len: usize,
}
const FREE: u64 = u64::MAX;
impl Seen {
fn find(&self, hash: u64, mut same: impl FnMut(u32) -> bool) -> Option<u32> {
if self.slots.is_empty() {
return None;
}
let mask = self.slots.len() - 1;
let low = hash as u32;
let mut at = low as usize & mask;
loop {
let slot = self.slots[at];
if slot == FREE {
return None;
}
if (slot >> 32) as u32 == low && same(slot as u32) {
return Some(slot as u32);
}
at = (at + 1) & mask;
}
}
fn insert(&mut self, hash: u64, code: u32) -> usize {
let mut grown = 0;
if (self.len + 1) * 4 > self.slots.len() * 3 {
let wanted = (self.slots.len() * 2).max(16);
let old = std::mem::replace(&mut self.slots, vec![FREE; wanted]);
grown = (wanted - old.len()) * size_of::<u64>();
for slot in old {
if slot != FREE {
self.place(slot);
}
}
}
self.place(u64::from(hash as u32) << 32 | u64::from(code));
self.len += 1;
grown
}
fn place(&mut self, slot: u64) {
let mask = self.slots.len() - 1;
let mut at = (slot >> 32) as usize & mask;
while self.slots[at] != FREE {
at = (at + 1) & mask;
}
self.slots[at] = slot;
}
}
#[derive(Debug, Default)]
struct Stored(u64);
impl Hasher for Stored {
fn write(&mut self, bytes: &[u8]) {
for &byte in bytes {
self.0 = (self.0 << 8) | u64::from(byte);
}
}
fn write_u64(&mut self, value: u64) {
self.0 = value;
}
fn finish(&self) -> u64 {
self.0
}
}
#[derive(Debug, Default)]
struct Words(u64);
impl Words {
fn mix(&mut self, word: u64) {
self.0 = (self.0.rotate_left(5) ^ word).wrapping_mul(0x9e37_79b9_7f4a_7c15);
}
}
impl Hasher for Words {
fn write(&mut self, bytes: &[u8]) {
let mut words = bytes.chunks_exact(8);
for word in &mut words {
let mut held = [0; 8];
held.copy_from_slice(word);
self.mix(u64::from_le_bytes(held));
}
let rest = words.remainder();
if !rest.is_empty() {
let mut held = [0; 8];
held[..rest.len()].copy_from_slice(rest);
self.mix(u64::from_le_bytes(held));
}
}
fn write_usize(&mut self, value: usize) {
self.mix(value as u64);
}
fn finish(&self) -> u64 {
let mut spread = self.0;
spread ^= spread >> 32;
spread = spread.wrapping_mul(0x9e37_79b9_7f4a_7c15);
spread ^ (spread >> 29)
}
}
#[derive(Debug)]
struct ReplacedText {
memo: Arc<Memo>,
}
impl TextSource for ReplacedText {
fn len(&self) -> usize {
self.memo.dictionary.len()
}
fn bytes_at(&self, index: usize) -> Result<Option<&[u8]>> {
if index >= self.len() {
return Ok(None);
}
self.memo.answer(index).map(Some)
}
fn footprint(&self) -> usize {
self.memo.kept.load(Ordering::Relaxed)
}
}
fn replace_stable(
call: &Call,
dictionary: &Arc<Vector>,
codes: &[u32],
base: Validity,
returns: &LogicalType,
rows: usize,
) -> Result<Option<Vector>> {
let stable = call.stable.get_or_init(|| {
let memo = Arc::new(Memo {
dictionary: Arc::clone(dictionary),
regex: call.regex.clone(),
rewrite: call.rewrite.clone(),
global: call.global,
host: call.host,
groups: (0..dictionary.len().div_ceil(REPLACE_GROUP))
.map(|_| OnceLock::new())
.collect(),
answers: (0..dictionary.len().div_ceil(REPLACE_GROUP))
.map(|_| OnceLock::new())
.collect(),
firsts: (0..REPLACE_SHARDS).map(|_| Mutex::new(Seen::default())).collect(),
kept: AtomicUsize::new(0),
});
let source = Arc::new(ReplacedText { memo: Arc::clone(&memo) });
let values = (!dictionary.is_empty())
.then(|| Vector::external_text(LogicalType::Varchar, source).ok().map(Arc::new))
.flatten();
StableReplace { memo, values }
});
if let (true, Some(values), LogicalType::Varchar) =
(Arc::ptr_eq(&stable.memo.dictionary, dictionary), &stable.values, returns)
{
let mut out = vec![0u32; rows];
let validity = over_valid(rows, base, |index| {
let code = *codes
.get(index)
.ok_or_else(|| Error::internal("a dictionary vector is shorter than its rows"))?;
out[index] = stable.memo.first(code as usize)?;
Ok(())
})?;
let vector = Vector::stable_dictionary_validated(out, Arc::clone(values), None)?;
return Ok(Some(vector.with_validity(validity)));
}
let mut buffer = String::new();
let mut out = StringColumn::with_capacity(rows);
let validity = over_strings(rows, base, &mut out, |index, out| {
let code = *codes
.get(index)
.ok_or_else(|| Error::internal("a dictionary vector is shorter than its rows"))?
as usize;
let text = dictionary.try_bytes_at(code)?.unwrap_or_default();
out.push_bytes(call.replaced(text, &mut buffer)?);
Ok(())
})?;
finish(returns, Data::Varlen(out), validity)
}
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,
Value::Null => {}
other if name == "regexp_extract" => {
let held = integral(other)
.and_then(|held| usize::try_from(held).ok())
.filter(|&held| held <= 9);
let Some(held) = held else {
return Err(Error::invalid_input("Group index must be between 0 and 9!"));
};
group = 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,
stable: OnceLock::new(),
}))
}
}
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 std::sync::Arc;
use rudb_common::LogicalType;
use rudb_vector::Vector;
use super::{Call, host, value, vectorized};
#[test]
fn a_replace_over_a_stable_dictionary_agrees_with_the_flat_loop() {
let values: Vec<Value> = (0..2_500)
.map(|index| match index % 5 {
0 => Value::Null,
1 => Value::Varchar(format!("http://www.site{index}.ru/page")),
2 => Value::Varchar(format!("https://host{}.com/a/b", index % 17)),
3 => Value::Varchar(String::new()),
_ => Value::Varchar(format!("plain {index} foo")),
})
.collect();
let dictionary =
Arc::new(Vector::from_values(LogicalType::Varchar, &values).expect("builds"));
let other = Arc::new(Vector::from_values(LogicalType::Varchar, &values).expect("builds"));
let patterns = [("^https?://(?:www\\.)?([^/]+)/.*$", "\\1"), ("o+", "0")];
for (pattern, replacement) in patterns {
let constants = [Value::Varchar(pattern.into()), Value::Varchar(replacement.into())];
let call = Call::read("regexp_replace", &constants.iter().collect::<Vec<_>>())
.expect("compiles")
.expect("a shape this file handles");
for (rows, step, held) in
[(2_000_usize, 991, &dictionary), (2_000, 991, &dictionary), (64, 37, &other)]
{
let codes: Vec<u32> =
(0..rows).map(|row| ((row * step) % values.len()) as u32).collect();
let picked: Vec<Value> =
codes.iter().map(|&code| values[code as usize].clone()).collect();
let flat = Vector::from_values(LogicalType::Varchar, &picked).expect("builds");
let column =
Vector::stable_dictionary(codes, Arc::clone(held)).expect("codes are in range");
let answer = |text: &Vector| {
vectorized("regexp_replace", Some(&call), &[text], &LogicalType::Varchar, rows)
.expect("the call is written")
.expect("text in this form has a loop")
};
let (want, got) = (answer(&flat), answer(&column));
for row in 0..rows {
assert_eq!(got.value_at(row), want.value_at(row), "{pattern}, row {row}");
}
if Arc::ptr_eq(held, &dictionary) {
let (codes, _) = got.stable_dictionary_parts().expect("answered as codes");
for one in 0..rows {
for other in (0..rows).step_by(7) {
if got.is_null_at(one) || got.is_null_at(other) {
continue;
}
assert_eq!(
codes[one] == codes[other],
got.value_at(one) == got.value_at(other),
"{pattern}, rows {one} and {other}"
);
}
}
}
}
}
}
#[test]
fn groups_decided_at_once_agree_on_one_code_per_answer() {
let values: Vec<Value> = (0..8_192)
.map(|index| Value::Varchar(format!("http://h{}.ru/{index}", index % 13)))
.collect();
let dictionary =
Arc::new(Vector::from_values(LogicalType::Varchar, &values).expect("builds"));
let constants = [
Value::Varchar("^https?://(?:www\\.)?([^/]+)/.*$".into()),
Value::Varchar("\\1".into()),
];
let call = Call::read("regexp_replace", &constants.iter().collect::<Vec<_>>())
.expect("compiles")
.expect("a shape this file handles");
let answers: Vec<Vector> = std::thread::scope(|scope| {
let threads: Vec<_> = (0..8_u32)
.map(|thread| {
let (call, dictionary) = (&call, &dictionary);
scope.spawn(move || {
let codes: Vec<u32> = (0..1_024).map(|row| thread * 1_024 + row).collect();
let column = Vector::stable_dictionary(codes, Arc::clone(dictionary))
.expect("codes are in range");
vectorized(
"regexp_replace",
Some(call),
&[&column],
&LogicalType::Varchar,
1_024,
)
.expect("the call is written")
.expect("text in this form has a loop")
})
})
.collect();
threads.into_iter().map(|thread| thread.join().expect("no panic")).collect()
});
let mut code_of = std::collections::HashMap::new();
for (thread, got) in answers.iter().enumerate() {
let (codes, _) = got.stable_dictionary_parts().expect("answered as codes");
for (row, &code) in codes.iter().enumerate() {
let index = thread * 1_024 + row;
let want = Value::Varchar(format!("h{}.ru", index % 13));
assert_eq!(got.value_at(row), want, "row {index}");
assert_eq!(code, *code_of.entry(index % 13).or_insert(code), "row {index}");
}
}
}
#[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_outside_zero_to_nine_is_refused() {
let called = |group: Value| {
let args = [Value::Varchar("a".into()), Value::Varchar("a".into()), group];
value("regexp_extract", &args)
};
for outside in [Value::BigInt(-1), Value::BigInt(10), Value::BigInt(i64::MAX)] {
let message = called(outside.clone()).expect_err("outside the range").to_string();
assert!(
message.contains("Group index must be between 0 and 9!"),
"{outside:?} said {message}"
);
}
let empty = Value::Varchar(String::new());
assert_eq!(called(Value::BigInt(7)).expect("inside the range"), empty);
assert_eq!(called(Value::BigInt(9)).expect("inside the range"), empty);
assert_eq!(called(Value::BigInt(0)).expect("inside the range"), Value::Varchar("a".into()));
}
#[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:?}");
}
}
}