use memchr::memchr3;
use memchr::memmem::Finder;
use regex::bytes::Regex;
use regex::bytes::RegexBuilder;
use vortex_error::VortexResult;
use vortex_error::vortex_err;
pub(crate) enum LikePattern {
Eq(Vec<u8>),
StartsWith(Vec<u8>),
EndsWith(Vec<u8>),
Contains(Box<Finder<'static>>, usize),
IEqAscii(Vec<u8>),
IStartsWithAscii(Vec<u8>),
IEndsWithAscii(Vec<u8>),
Regex(Regex),
}
impl LikePattern {
pub(crate) fn compile(
pattern: &str,
case_insensitive: bool,
ascii_haystack: bool,
) -> VortexResult<Self> {
if case_insensitive {
if ascii_haystack && pattern.is_ascii() {
if !contains_like_pattern(pattern) {
return Ok(Self::IEqAscii(pattern.as_bytes().to_vec()));
} else if pattern.ends_with('%')
&& !contains_like_pattern(&pattern[..pattern.len() - 1])
{
return Ok(Self::IStartsWithAscii(
pattern.as_bytes()[..pattern.len() - 1].to_vec(),
));
} else if pattern.starts_with('%') && !contains_like_pattern(&pattern[1..]) {
return Ok(Self::IEndsWithAscii(pattern.as_bytes()[1..].to_vec()));
}
}
return Ok(Self::Regex(regex_like(pattern, true)?));
}
Ok(if !contains_like_pattern(pattern) {
Self::Eq(pattern.as_bytes().to_vec())
} else if pattern.ends_with('%') && !contains_like_pattern(&pattern[..pattern.len() - 1]) {
Self::StartsWith(pattern.as_bytes()[..pattern.len() - 1].to_vec())
} else if pattern.starts_with('%') && !contains_like_pattern(&pattern[1..]) {
Self::EndsWith(pattern.as_bytes()[1..].to_vec())
} else if pattern.starts_with('%')
&& pattern.ends_with('%')
&& !contains_like_pattern(&pattern[1..pattern.len() - 1])
{
Self::Contains(
Box::new(Finder::new(&pattern.as_bytes()[1..pattern.len() - 1]).into_owned()),
pattern.len() - 2,
)
} else {
Self::Regex(regex_like(pattern, false)?)
})
}
#[inline]
pub(crate) fn matches(&self, haystack: &[u8]) -> bool {
match self {
Self::Eq(needle) => needle == haystack,
Self::StartsWith(needle) => {
needle.len() <= haystack.len() && &haystack[..needle.len()] == needle.as_slice()
}
Self::EndsWith(needle) => {
needle.len() <= haystack.len()
&& &haystack[haystack.len() - needle.len()..] == needle.as_slice()
}
Self::Contains(finder, needle_len) => {
haystack.len() >= *needle_len && finder.find(haystack).is_some()
}
Self::IEqAscii(needle) => haystack.eq_ignore_ascii_case(needle),
Self::IStartsWithAscii(needle) => {
needle.len() <= haystack.len()
&& haystack[..needle.len()].eq_ignore_ascii_case(needle)
}
Self::IEndsWithAscii(needle) => {
needle.len() <= haystack.len()
&& haystack[haystack.len() - needle.len()..].eq_ignore_ascii_case(needle)
}
Self::Regex(regex) => regex.is_match(haystack),
}
}
}
fn contains_like_pattern(pattern: &str) -> bool {
memchr3(b'%', b'_', b'\\', pattern.as_bytes()).is_some()
}
fn regex_like(pattern: &str, case_insensitive: bool) -> VortexResult<Regex> {
let mut result = String::with_capacity(pattern.len() * 2);
let mut chars_iter = pattern.chars().peekable();
match chars_iter.peek() {
Some('%') => {
chars_iter.next();
}
_ => result.push('^'),
};
while let Some(c) = chars_iter.next() {
match c {
'\\' => {
match chars_iter.peek() {
Some(&next) => {
if regex_syntax::is_meta_character(next) {
result.push('\\');
}
result.push(next);
chars_iter.next();
}
None => {
result.push('\\');
result.push('\\');
}
}
}
'%' => result.push_str(".*"),
'_' => result.push('.'),
c => {
if regex_syntax::is_meta_character(c) {
result.push('\\');
}
result.push(c);
}
}
}
if result.ends_with(".*") {
result.pop();
result.pop();
} else {
result.push('$');
}
RegexBuilder::new(&result)
.case_insensitive(case_insensitive)
.dot_matches_new_line(true)
.build()
.map_err(|e| vortex_err!("Unable to build regex from LIKE pattern: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
fn like(pattern: &str) -> LikePattern {
LikePattern::compile(pattern, false, false).unwrap()
}
#[test]
fn compile_fast_paths() {
assert!(matches!(like("foo"), LikePattern::Eq(_)));
assert!(matches!(like("foo%"), LikePattern::StartsWith(_)));
assert!(matches!(like("%foo"), LikePattern::EndsWith(_)));
assert!(matches!(like("%foo%"), LikePattern::Contains(..)));
assert!(matches!(like("%"), LikePattern::StartsWith(_)));
assert!(matches!(like("%%"), LikePattern::Contains(..)));
assert!(matches!(like("f%o"), LikePattern::Regex(_)));
assert!(matches!(like("f_o"), LikePattern::Regex(_)));
assert!(matches!(like(r"foo\%"), LikePattern::Regex(_)));
}
#[test]
fn regex_translation() {
let cases = [
(r"%foobar%", r"foobar"),
(r"foo%bar", r"^foo.*bar$"),
(r"foo_bar", r"^foo.bar$"),
(r"\%\_", r"^%_$"),
(r"\a", r"^a$"),
(r"\\%", r"^\\"),
(r"\\a", r"^\\a$"),
(r".", r"^\.$"),
(r"$", r"^\$$"),
(r"\\", r"^\\$"),
];
for (pattern, expected) in cases {
assert_eq!(regex_like(pattern, false).unwrap().to_string(), expected);
}
}
#[test]
fn matches_wildcards() {
assert!(like("h_llo w%d").matches(b"hello world"));
assert!(like("h_llo w%d").matches("h\u{00a3}llo wd".as_bytes()));
assert!(!like("h_llo w%d").matches(b"hxxllo world"));
assert!(like("hello%world").matches(b"hello\nworld"));
assert!(like("_").matches("\u{00a3}".as_bytes()));
assert!(!like("_").matches("\u{00a3}x".as_bytes()));
}
#[test]
fn matches_escapes() {
assert!(like(r"100\%").matches(b"100%"));
assert!(!like(r"100\%").matches(b"100x"));
assert!(like(r"a\_b").matches(b"a_b"));
assert!(!like(r"a\_b").matches(b"axb"));
assert!(like("trailing\\").matches(b"trailing\\"));
}
#[test]
fn matches_case_insensitive() {
let ilike = |pattern: &str| LikePattern::compile(pattern, true, false).unwrap();
assert!(ilike("hello%").matches(b"HELLO WORLD"));
assert!(ilike("%WORLD").matches(b"hello world"));
assert!(ilike("k").matches("\u{212a}".as_bytes()));
assert!(ilike("\u{03c3}").matches("\u{03a3}".as_bytes()));
assert!(ilike("\u{03c3}").matches("\u{03c2}".as_bytes()));
let ascii = |pattern: &str| LikePattern::compile(pattern, true, true).unwrap();
assert!(matches!(ascii("abc"), LikePattern::IEqAscii(_)));
assert!(matches!(ascii("abc%"), LikePattern::IStartsWithAscii(_)));
assert!(matches!(ascii("%abc"), LikePattern::IEndsWithAscii(_)));
assert!(ascii("abc").matches(b"AbC"));
assert!(ascii("abc%").matches(b"ABCDEF"));
assert!(ascii("%def").matches(b"ABCDEF"));
assert!(!ascii("abc").matches(b"AbCd"));
}
}