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