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