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