1mod kernel;
5mod pattern;
6
7use std::borrow::Cow;
8use std::fmt::Display;
9use std::fmt::Formatter;
10
11pub use kernel::*;
12use pattern::LikePattern;
13use prost::Message;
14use vortex_buffer::BitBuffer;
15use vortex_buffer::BitBufferMut;
16use vortex_buffer::BufferAllocatorRef;
17use vortex_error::VortexResult;
18use vortex_error::vortex_bail;
19use vortex_error::vortex_err;
20use vortex_session::VortexSession;
21use vortex_session::registry::CachedId;
22
23use crate::ArrayRef;
24use crate::Canonical;
25use crate::ExecutionCtx;
26use crate::IntoArray;
27use crate::arrays::BoolArray;
28use crate::arrays::ConstantArray;
29use crate::arrays::ScalarFnArray;
30use crate::arrays::VarBinViewArray;
31use crate::arrays::varbinview::BinaryView;
32use crate::arrays::varbinview::ResolvedViews;
33use crate::dtype::DType;
34use crate::dtype::Nullability;
35use crate::expr::display::ExprDisplay;
36use crate::proto::expr as pb;
37use crate::scalar::Scalar;
38use crate::scalar_fn::Arity;
39use crate::scalar_fn::ChildName;
40use crate::scalar_fn::ExecutionArgs;
41use crate::scalar_fn::ScalarFnId;
42use crate::scalar_fn::ScalarFnVTable;
43use crate::scalar_fn::ScalarFnVTableExt;
44
45#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub struct LikeOptions {
48 pub negated: bool,
49 pub case_insensitive: bool,
50}
51
52impl Display for LikeOptions {
53 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
54 if self.negated {
55 write!(f, "NOT ")?;
56 }
57 if self.case_insensitive {
58 write!(f, "ILIKE")
59 } else {
60 write!(f, "LIKE")
61 }
62 }
63}
64
65#[derive(Clone)]
67pub struct Like;
68
69impl Like {
70 pub fn try_new(
76 input: ArrayRef,
77 pattern: ArrayRef,
78 options: LikeOptions,
79 ) -> VortexResult<ScalarFnArray> {
80 ScalarFnArray::try_new(Like.bind(options), [input, pattern])
81 }
82}
83
84impl ScalarFnVTable for Like {
85 type Options = LikeOptions;
86
87 fn id(&self) -> ScalarFnId {
88 static ID: CachedId = CachedId::new("vortex.like");
89 *ID
90 }
91
92 fn serialize(&self, instance: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
93 Ok(Some(
94 pb::LikeOpts {
95 negated: instance.negated,
96 case_insensitive: instance.case_insensitive,
97 }
98 .encode_to_vec(),
99 ))
100 }
101
102 fn deserialize(
103 &self,
104 _metadata: &[u8],
105 _session: &VortexSession,
106 ) -> VortexResult<Self::Options> {
107 let opts = pb::LikeOpts::decode(_metadata)?;
108 Ok(LikeOptions {
109 negated: opts.negated,
110 case_insensitive: opts.case_insensitive,
111 })
112 }
113
114 fn arity(&self, _options: &Self::Options) -> Arity {
115 Arity::Exact(2)
116 }
117
118 fn child_name(&self, _instance: &Self::Options, child_idx: usize) -> ChildName {
119 match child_idx {
120 0 => ChildName::from("child"),
121 1 => ChildName::from("pattern"),
122 _ => unreachable!("Invalid child index {} for Like expression", child_idx),
123 }
124 }
125
126 fn fmt_sql(
127 &self,
128 options: &Self::Options,
129 expr: &dyn ExprDisplay,
130 f: &mut Formatter<'_>,
131 ) -> std::fmt::Result {
132 Display::fmt(expr.display_child(0), f)?;
133 if options.negated {
134 write!(f, " not")?;
135 }
136 if options.case_insensitive {
137 write!(f, " ilike ")?;
138 } else {
139 write!(f, " like ")?;
140 }
141 Display::fmt(expr.display_child(1), f)
142 }
143
144 fn return_dtype(&self, _options: &Self::Options, arg_dtypes: &[DType]) -> VortexResult<DType> {
145 let input = &arg_dtypes[0];
146 let pattern = &arg_dtypes[1];
147
148 if !input.is_utf8() {
149 vortex_bail!("LIKE expression requires UTF8 input dtype, got {}", input);
150 }
151 if !pattern.is_utf8() {
152 vortex_bail!(
153 "LIKE expression requires UTF8 pattern dtype, got {}",
154 pattern
155 );
156 }
157
158 Ok(DType::Bool(
159 (input.is_nullable() || pattern.is_nullable()).into(),
160 ))
161 }
162
163 fn execute(
164 &self,
165 options: &Self::Options,
166 args: &dyn ExecutionArgs,
167 ctx: &mut ExecutionCtx,
168 ) -> VortexResult<ArrayRef> {
169 let child = args.get(0)?;
170 let pattern = args.get(1)?;
171
172 execute_like(&child, &pattern, *options, ctx)
173 }
174
175 fn is_strict(&self, _instance: &Self::Options) -> bool {
176 true
177 }
178
179 fn is_infallible(&self, _options: &Self::Options) -> bool {
180 true
181 }
182}
183
184pub(crate) fn execute_like(
189 array: &ArrayRef,
190 pattern: &ArrayRef,
191 options: LikeOptions,
192 ctx: &mut ExecutionCtx,
193) -> VortexResult<ArrayRef> {
194 assert_eq!(
195 array.len(),
196 pattern.len(),
197 "LIKE: length mismatch for {}",
198 array.encoding_id()
199 );
200 let len = array.len();
201 let nullability =
202 Nullability::from(array.dtype().is_nullable() || pattern.dtype().is_nullable());
203
204 if len == 0 {
205 return Ok(Canonical::empty(&DType::Bool(nullability)).into_array());
206 }
207
208 if let Some(pattern_const) = pattern.as_constant() {
209 let Some(pattern_str) = pattern_const.as_utf8().value() else {
210 return Ok(
212 ConstantArray::new(Scalar::null(DType::Bool(Nullability::Nullable)), len)
213 .into_array(),
214 );
215 };
216 let values = array.clone().execute::<VarBinViewArray>(ctx)?;
217 let haystack = values.resolved_views();
218 let ascii_haystack =
221 options.case_insensitive && pattern_str.as_str().is_ascii() && is_ascii(&haystack);
222 let compiled = LikePattern::compile(
223 pattern_str.as_str(),
224 options.case_insensitive,
225 ascii_haystack,
226 )?;
227 let bits = eval_pattern(&haystack, &compiled, options.negated, ctx.allocator());
228 let validity = values.validity()?.union_nullability(nullability);
229 return Ok(BoolArray::new(bits, validity).into_array());
230 }
231
232 let values = array.clone().execute::<VarBinViewArray>(ctx)?;
234 let patterns = pattern.clone().execute::<VarBinViewArray>(ctx)?;
235 let haystack = values.resolved_views();
236 let pattern_views = patterns.resolved_views();
237 let ascii_haystack = options.case_insensitive && is_ascii(&haystack);
238
239 let mut cached: Option<(&[u8], LikePattern)> = None;
242 let mut bits = BitBufferMut::with_capacity_in(len, ctx.allocator().clone());
243 for i in 0..len {
244 let pattern_bytes = pattern_views.bytes(i);
245 let compiled = match &cached {
246 Some((bytes, compiled)) if *bytes == pattern_bytes => compiled,
247 _ => {
248 let pattern_str = std::str::from_utf8(pattern_bytes)
249 .map_err(|e| vortex_err!("LIKE pattern is not valid UTF-8: {e}"))?;
250 let compiled = LikePattern::compile(
251 pattern_str,
252 options.case_insensitive,
253 ascii_haystack && pattern_str.is_ascii(),
254 )?;
255 &cached.insert((pattern_bytes, compiled)).1
256 }
257 };
258 bits.append(compiled.matches(haystack.bytes(i)) != options.negated);
259 }
260 let validity = values
261 .validity()?
262 .and(patterns.validity()?)?
263 .union_nullability(nullability);
264 Ok(BoolArray::new(bits.freeze(), validity).into_array())
265}
266
267fn eval_pattern(
273 haystack: &ResolvedViews<'_>,
274 pattern: &LikePattern,
275 negated: bool,
276 allocator: &BufferAllocatorRef,
277) -> BitBuffer {
278 let len = haystack.len();
279 match pattern {
280 LikePattern::Eq(needle) if needle.len() <= BinaryView::MAX_INLINED_SIZE => {
281 let needle_view = BinaryView::new_inlined(needle).as_u128();
284 BitBuffer::collect_bool_in(
285 len,
286 |i| (haystack.views()[i].as_u128() == needle_view) != negated,
287 allocator.clone(),
288 )
289 }
290 LikePattern::Eq(needle) => {
291 let needle_head = BinaryView::head_of(needle);
294 BitBuffer::collect_bool_in(
295 len,
296 |i| {
297 let view = &haystack.views()[i];
298 let matched =
299 view.head() == needle_head && haystack.bytes(i)[4..] == needle[4..];
300 matched != negated
301 },
302 allocator.clone(),
303 )
304 }
305 LikePattern::StartsWith(needle) => {
306 let needle_len = needle.len();
310 let prefix_len = needle_len.min(4);
311 let needle_prefix = u32::from_le_bytes(BinaryView::prefix_of(needle));
313 let prefix_mask = if prefix_len == 4 {
314 u32::MAX
315 } else {
316 (1u32 << (8 * prefix_len)) - 1
317 };
318 BitBuffer::collect_bool_in(
319 len,
320 |i| {
321 let view = &haystack.views()[i];
322 let matched = view.len() as usize >= needle_len
323 && (u32::from_le_bytes(view.prefix()) & prefix_mask) == needle_prefix
324 && (needle_len <= 4 || haystack.bytes(i)[4..needle_len] == needle[4..]);
325 matched != negated
326 },
327 allocator.clone(),
328 )
329 }
330 LikePattern::EndsWith(needle) => {
331 let needle_len = needle.len();
334 BitBuffer::collect_bool_in(
335 len,
336 |i| {
337 let matched = unsafe {
340 let view = haystack.views().get_unchecked(i);
341 view.len() as usize >= needle_len
342 && bytes_eq(suffix_bytes_unchecked(haystack, view, needle_len), needle)
343 };
344 matched != negated
345 },
346 allocator.clone(),
347 )
348 }
349 LikePattern::IEqAscii(needle) => BitBuffer::collect_bool_in(
350 len,
351 |i| {
352 let view = &haystack.views()[i];
353 let matched = view.len() as usize == needle.len()
354 && haystack.bytes(i).eq_ignore_ascii_case(needle);
355 matched != negated
356 },
357 allocator.clone(),
358 ),
359 LikePattern::Contains(finder, needle_len) => BitBuffer::collect_bool_in(
360 len,
361 |i| {
362 let view = &haystack.views()[i];
363 let matched =
364 view.len() as usize >= *needle_len && finder.find(haystack.bytes(i)).is_some();
365 matched != negated
366 },
367 allocator.clone(),
368 ),
369 _ => BitBuffer::collect_bool_in(
370 len,
371 |i| pattern.matches(haystack.bytes(i)) != negated,
372 allocator.clone(),
373 ),
374 }
375}
376
377fn is_ascii(haystack: &ResolvedViews<'_>) -> bool {
379 haystack
380 .views()
381 .iter()
382 .all(|view| haystack.view_bytes(view).is_ascii())
383}
384
385#[inline]
391unsafe fn suffix_bytes_unchecked<'a>(
392 haystack: &ResolvedViews<'a>,
393 view: &'a BinaryView,
394 suffix_len: usize,
395) -> &'a [u8] {
396 let len = view.len() as usize;
397 if view.is_inlined() {
398 unsafe { view.as_inlined().value().get_unchecked(len - suffix_len..) }
400 } else {
401 let view = view.as_view();
402 let end = view.offset as usize + len;
403 unsafe {
405 haystack
406 .buffers()
407 .get_unchecked(view.buffer_index as usize)
408 .get_unchecked(end - suffix_len..end)
409 }
410 }
411}
412
413#[inline]
418fn bytes_eq(lhs: &[u8], rhs: &[u8]) -> bool {
419 lhs.len() == rhs.len() && std::iter::zip(lhs, rhs).all(|(l, r)| l == r)
420}
421
422#[derive(Debug, PartialEq)]
424pub(crate) enum LikeVariant<'a> {
425 Exact(Cow<'a, str>),
426 Prefix(Cow<'a, str>),
427}
428
429impl<'a> LikeVariant<'a> {
430 pub(crate) fn from_str(string: &'a str) -> Option<LikeVariant<'a>> {
432 let mut literal = None;
433 let mut chars = string.char_indices();
434
435 while let Some((idx, c)) = chars.next() {
436 match c {
437 '\\' => {
438 let literal = literal.get_or_insert_with(|| string[..idx].to_string());
439 match chars.next() {
440 Some((_, escaped)) => literal.push(escaped),
441 None => literal.push('\\'),
442 }
443 }
444 '%' | '_' => {
445 return match literal {
446 Some(literal) => (!literal.is_empty())
447 .then_some(LikeVariant::Prefix(Cow::Owned(literal))),
448 None => {
449 (idx != 0).then_some(LikeVariant::Prefix(Cow::Borrowed(&string[..idx])))
450 }
451 };
452 }
453 c => {
454 if let Some(literal) = &mut literal {
455 literal.push(c);
456 }
457 }
458 }
459 }
460
461 Some(match literal {
462 Some(literal) => LikeVariant::Exact(Cow::Owned(literal)),
463 None => LikeVariant::Exact(Cow::Borrowed(string)),
464 })
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use std::borrow::Cow;
471
472 use rstest::rstest;
473
474 use crate::IntoArray;
475 use crate::VortexSessionExecute;
476 use crate::array_session;
477 use crate::arrays::BoolArray;
478 use crate::arrays::ConstantArray;
479 use crate::arrays::VarBinArray;
480 use crate::arrays::VarBinViewArray;
481 use crate::assert_arrays_eq;
482 use crate::dtype::DType;
483 use crate::dtype::Nullability;
484 use crate::expr::get_item;
485 use crate::expr::like;
486 use crate::expr::lit;
487 use crate::expr::not;
488 use crate::expr::not_ilike;
489 use crate::expr::root;
490 use crate::scalar::Scalar;
491 use crate::scalar_fn::fns::like::Like;
492 use crate::scalar_fn::fns::like::LikeOptions;
493 use crate::scalar_fn::fns::like::LikeVariant;
494 use crate::scalar_fn::fns::like::is_ascii;
495 use crate::scalar_fn::fns::like::suffix_bytes_unchecked;
496
497 const LONG: &str = "a value far too long to live inside its own view";
498
499 fn run_like(
500 array: crate::ArrayRef,
501 pattern: crate::ArrayRef,
502 options: LikeOptions,
503 ) -> crate::ArrayRef {
504 Like::try_new(array, pattern, options).unwrap().into_array()
505 }
506
507 #[rstest]
508 #[case("hello", [true, false, false, false])]
510 #[case("he%", [true, false, true, false])]
511 #[case("%llo", [true, false, false, true])]
512 #[case("%ell%", [true, false, false, true])]
513 #[case("h_llo", [true, false, false, false])]
515 #[case("h%o", [true, false, false, false])]
516 #[case("%", [true, true, true, true])]
517 #[case("_____", [true, true, false, true])]
518 fn test_like_patterns(#[case] pattern: &str, #[case] expected: [bool; 4]) {
519 let mut ctx = array_session().create_execution_ctx();
520 let array =
521 VarBinViewArray::from_iter_str(["hello", "world", "help", "jello"]).into_array();
522 let result = run_like(
523 array,
524 ConstantArray::new(pattern, 4).into_array(),
525 LikeOptions::default(),
526 );
527 assert_arrays_eq!(result, BoolArray::from_iter(expected), &mut ctx);
528 }
529
530 #[test]
531 fn test_like_escapes() {
532 let mut ctx = array_session().create_execution_ctx();
533 let array = VarBinViewArray::from_iter_str(["100%", "100x", "a_b", "axb"]).into_array();
534
535 let result = run_like(
536 array.clone(),
537 ConstantArray::new(r"100\%", 4).into_array(),
538 LikeOptions::default(),
539 );
540 assert_arrays_eq!(
541 result,
542 BoolArray::from_iter([true, false, false, false]),
543 &mut ctx
544 );
545
546 let result = run_like(
547 array,
548 ConstantArray::new(r"a\_b", 4).into_array(),
549 LikeOptions::default(),
550 );
551 assert_arrays_eq!(
552 result,
553 BoolArray::from_iter([false, false, true, false]),
554 &mut ctx
555 );
556 }
557
558 #[test]
559 fn test_like_regex_meta_characters_are_literal() {
560 let mut ctx = array_session().create_execution_ctx();
561 let array = VarBinViewArray::from_iter_str(["a.c", "abc", "a$c"]).into_array();
562 let result = run_like(
563 array,
564 ConstantArray::new("a.%", 3).into_array(),
565 LikeOptions::default(),
566 );
567 assert_arrays_eq!(result, BoolArray::from_iter([true, false, false]), &mut ctx);
568 }
569
570 #[test]
571 fn test_like_unicode() {
572 let mut ctx = array_session().create_execution_ctx();
573 let array =
574 VarBinViewArray::from_iter_str(["h\u{00a3}llo", "hxllo", "h\u{00a3}xllo"]).into_array();
575 let result = run_like(
577 array,
578 ConstantArray::new("h_llo", 3).into_array(),
579 LikeOptions::default(),
580 );
581 assert_arrays_eq!(result, BoolArray::from_iter([true, true, false]), &mut ctx);
582 }
583
584 #[test]
585 fn test_nlike() {
586 let mut ctx = array_session().create_execution_ctx();
587 let array = VarBinViewArray::from_iter_str(["hello", "world"]).into_array();
588 let result = run_like(
589 array,
590 ConstantArray::new("he%", 2).into_array(),
591 LikeOptions {
592 negated: true,
593 case_insensitive: false,
594 },
595 );
596 assert_arrays_eq!(result, BoolArray::from_iter([false, true]), &mut ctx);
597 }
598
599 #[test]
600 fn test_ilike() {
601 let mut ctx = array_session().create_execution_ctx();
602 let ilike = LikeOptions {
603 negated: false,
604 case_insensitive: true,
605 };
606
607 let array = VarBinViewArray::from_iter_str(["HELLO", "world", "Help"]).into_array();
609 let result = run_like(array, ConstantArray::new("he%", 3).into_array(), ilike);
610 assert_arrays_eq!(result, BoolArray::from_iter([true, false, true]), &mut ctx);
611
612 let array = VarBinViewArray::from_iter_str(["\u{212a}", "k", "x"]).into_array();
614 let result = run_like(array, ConstantArray::new("k", 3).into_array(), ilike);
615 assert_arrays_eq!(result, BoolArray::from_iter([true, true, false]), &mut ctx);
616 }
617
618 #[test]
619 fn test_nilike() {
620 let mut ctx = array_session().create_execution_ctx();
621 let array = VarBinViewArray::from_iter_str(["HELLO", "world"]).into_array();
622 let result = run_like(
623 array,
624 ConstantArray::new("he%", 2).into_array(),
625 LikeOptions {
626 negated: true,
627 case_insensitive: true,
628 },
629 );
630 assert_arrays_eq!(result, BoolArray::from_iter([false, true]), &mut ctx);
631 }
632
633 #[test]
634 fn test_like_nullable_input() {
635 let mut ctx = array_session().create_execution_ctx();
636 let array = VarBinViewArray::from_iter_nullable_str([Some("hello"), None, Some("help")])
637 .into_array();
638 let result = run_like(
639 array,
640 ConstantArray::new("he%", 3).into_array(),
641 LikeOptions::default(),
642 );
643 assert_arrays_eq!(
644 result,
645 BoolArray::from_iter([Some(true), None, Some(true)]),
646 &mut ctx
647 );
648 }
649
650 #[test]
651 fn test_like_null_pattern() {
652 let mut ctx = array_session().create_execution_ctx();
653 let array = VarBinViewArray::from_iter_str(["hello", "world"]).into_array();
654 let result = run_like(
655 array,
656 ConstantArray::new(Scalar::null(DType::Utf8(Nullability::Nullable)), 2).into_array(),
657 LikeOptions::default(),
658 );
659 assert_arrays_eq!(result, BoolArray::from_iter([None, None]), &mut ctx);
660 }
661
662 #[test]
663 fn test_like_per_row_patterns() {
664 let mut ctx = array_session().create_execution_ctx();
665 let array = VarBinViewArray::from_iter_str(["hello", "hello", "hello"]).into_array();
666 let patterns = VarBinViewArray::from_iter_str(["he%", "%world", "h_llo"]).into_array();
667 let result = run_like(array, patterns, LikeOptions::default());
668 assert_arrays_eq!(result, BoolArray::from_iter([true, false, true]), &mut ctx);
669 }
670
671 #[test]
672 fn test_like_non_canonical_input() {
673 let mut ctx = array_session().create_execution_ctx();
674 let array = VarBinArray::from_iter(
676 [Some("hello"), Some("world")],
677 DType::Utf8(Nullability::Nullable),
678 )
679 .into_array();
680 let result = run_like(
681 array,
682 ConstantArray::new("he%", 2).into_array(),
683 LikeOptions::default(),
684 );
685 assert_arrays_eq!(
686 result,
687 BoolArray::from_iter([Some(true), Some(false)]),
688 &mut ctx
689 );
690 }
691
692 #[test]
693 fn invert_booleans() {
694 let not_expr = not(root());
695 let bools = BoolArray::from_iter([false, true, false, false, true, true]);
696 let mut ctx = array_session().create_execution_ctx();
697 assert_arrays_eq!(
698 bools.into_array().apply(¬_expr).unwrap(),
699 BoolArray::from_iter([true, false, true, true, false, false]),
700 &mut ctx
701 );
702 }
703
704 #[test]
705 fn dtype() {
706 let dtype = DType::Utf8(Nullability::NonNullable);
707 let like_expr = like(root(), lit("%test%"));
708 assert_eq!(
709 like_expr.return_dtype(&dtype).unwrap(),
710 DType::Bool(Nullability::NonNullable)
711 );
712 }
713
714 #[test]
715 fn signature() {
716 let like_expr = like(root(), lit("%test%"));
717 assert!(
718 like_expr
719 .as_scalar()
720 .is_some_and(|f| f.signature().is_strict())
721 );
722 assert!(
723 like_expr
724 .as_scalar()
725 .is_some_and(|f| f.signature().is_infallible())
726 );
727 }
728
729 #[test]
730 fn test_display() {
731 let expr = like(get_item("name", root()), lit("%john%"));
732 assert_eq!(expr.to_string(), "$.name like \"%john%\"");
733
734 let expr2 = not_ilike(root(), lit("test*"));
735 assert_eq!(expr2.to_string(), "$ not ilike \"test*\"");
736 }
737
738 fn assert_borrowed_exact(pattern: &str, expected: &str) {
739 let Some(LikeVariant::Exact(actual)) = LikeVariant::from_str(pattern) else {
740 panic!("expected borrowed exact pattern");
741 };
742 assert!(matches!(actual, Cow::Borrowed(_)));
743 assert_eq!(actual.as_ref(), expected);
744 }
745
746 fn assert_owned_exact(pattern: &str, expected: &str) {
747 let Some(LikeVariant::Exact(actual)) = LikeVariant::from_str(pattern) else {
748 panic!("expected owned exact pattern");
749 };
750 assert!(matches!(actual, Cow::Owned(_)));
751 assert_eq!(actual.as_ref(), expected);
752 }
753
754 fn assert_borrowed_prefix(pattern: &str, expected: &str) {
755 let Some(LikeVariant::Prefix(actual)) = LikeVariant::from_str(pattern) else {
756 panic!("expected borrowed prefix pattern");
757 };
758 assert!(matches!(actual, Cow::Borrowed(_)));
759 assert_eq!(actual.as_ref(), expected);
760 }
761
762 fn assert_owned_prefix(pattern: &str, expected: &str) {
763 let Some(LikeVariant::Prefix(actual)) = LikeVariant::from_str(pattern) else {
764 panic!("expected owned prefix pattern");
765 };
766 assert!(matches!(actual, Cow::Owned(_)));
767 assert_eq!(actual.as_ref(), expected);
768 }
769
770 #[test]
771 fn test_like_variant_borrowed_patterns() {
772 assert_borrowed_exact("simple", "simple");
773 assert_borrowed_prefix("prefix%", "prefix");
774 assert_borrowed_prefix("first%rest_stuff", "first");
775 }
776
777 #[test]
778 fn test_like_variant_escaped_patterns() {
779 assert_owned_prefix(r"\%%", "%");
780 assert_owned_prefix(r"\_%", "_");
781 assert_owned_prefix(r"\\%", "\\");
782 assert_owned_exact(r"\%", "%");
783 assert_owned_exact("trailing\\", "trailing\\");
784 }
785
786 #[test]
787 fn test_like_variant_unsupported_patterns() {
788 assert_eq!(LikeVariant::from_str("%suffix"), None);
789 assert_eq!(LikeVariant::from_str(r"%\%%"), None);
790 assert_eq!(LikeVariant::from_str("_pattern"), None);
791 }
792
793 #[test]
794 fn suffix_bytes_are_value_suffixes() {
795 let array = VarBinViewArray::from_iter_str(["short", LONG, "another long value here"]);
796 let haystack = array.resolved_views();
797 for (index, view) in haystack.views().iter().enumerate() {
798 let value = haystack.bytes(index);
799 for suffix_len in 0..=value.len() {
800 let suffix = unsafe { suffix_bytes_unchecked(&haystack, view, suffix_len) };
802 assert_eq!(suffix, &value[value.len() - suffix_len..]);
803 }
804 }
805 }
806
807 #[rstest]
808 #[case::inlined(["short", "tiny"], true)]
809 #[case::referenced([LONG, "another long value here"], true)]
810 #[case::inlined_non_ascii(["short", "é"], false)]
811 #[case::referenced_non_ascii([LONG, "a long value ending in é"], false)]
812 fn detects_ascii(#[case] values: [&str; 2], #[case] expected: bool) {
813 let array = VarBinViewArray::from_iter_str(values);
814 assert_eq!(is_ascii(&array.resolved_views()), expected);
815 }
816}