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::ScalarFnArray;
29use crate::arrays::VarBinViewArray;
30use crate::arrays::varbinview::BinaryView;
31use crate::dtype::DType;
32use crate::dtype::Nullability;
33use crate::expr::Expression;
34use crate::expr::and;
35use crate::expr::display::ExprDisplay;
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 validity(
175 &self,
176 _options: &Self::Options,
177 expression: &Expression,
178 ) -> VortexResult<Option<Expression>> {
179 tracing::warn!("Computing validity for LIKE expression");
180 let child_validity = expression.child(0).validity()?;
181 let pattern_validity = expression.child(1).validity()?;
182 Ok(Some(and(child_validity, pattern_validity)))
183 }
184
185 fn is_strict(&self, _instance: &Self::Options) -> bool {
186 true
187 }
188
189 fn is_infallible(&self, _options: &Self::Options) -> bool {
190 true
191 }
192}
193
194pub(crate) fn execute_like(
199 array: &ArrayRef,
200 pattern: &ArrayRef,
201 options: LikeOptions,
202 ctx: &mut ExecutionCtx,
203) -> VortexResult<ArrayRef> {
204 assert_eq!(
205 array.len(),
206 pattern.len(),
207 "LIKE: length mismatch for {}",
208 array.encoding_id()
209 );
210 let len = array.len();
211 let nullability =
212 Nullability::from(array.dtype().is_nullable() || pattern.dtype().is_nullable());
213
214 if len == 0 {
215 return Ok(Canonical::empty(&DType::Bool(nullability)).into_array());
216 }
217
218 if let Some(pattern_const) = pattern.as_constant() {
219 let Some(pattern_str) = pattern_const.as_utf8().value() else {
220 return Ok(
222 ConstantArray::new(Scalar::null(DType::Bool(Nullability::Nullable)), len)
223 .into_array(),
224 );
225 };
226 let values = array.clone().execute::<VarBinViewArray>(ctx)?;
227 let haystack = ResolvedViews::new(&values);
228 let ascii_haystack =
231 options.case_insensitive && pattern_str.as_str().is_ascii() && haystack.is_ascii();
232 let compiled = LikePattern::compile(
233 pattern_str.as_str(),
234 options.case_insensitive,
235 ascii_haystack,
236 )?;
237 let bits = eval_pattern(&haystack, &compiled, options.negated);
238 let validity = values.validity()?.union_nullability(nullability);
239 return Ok(BoolArray::new(bits, validity).into_array());
240 }
241
242 let values = array.clone().execute::<VarBinViewArray>(ctx)?;
244 let patterns = pattern.clone().execute::<VarBinViewArray>(ctx)?;
245 let haystack = ResolvedViews::new(&values);
246 let pattern_views = ResolvedViews::new(&patterns);
247 let ascii_haystack = options.case_insensitive && haystack.is_ascii();
248
249 let mut bits = Vec::with_capacity(len);
250 let mut cached: Option<(&[u8], LikePattern)> = None;
253 for i in 0..len {
254 let pattern_bytes = pattern_views.bytes(i);
255 let compiled = match &cached {
256 Some((bytes, compiled)) if *bytes == pattern_bytes => compiled,
257 _ => {
258 let pattern_str = std::str::from_utf8(pattern_bytes)
259 .map_err(|e| vortex_err!("LIKE pattern is not valid UTF-8: {e}"))?;
260 let compiled = LikePattern::compile(
261 pattern_str,
262 options.case_insensitive,
263 ascii_haystack && pattern_str.is_ascii(),
264 )?;
265 &cached.insert((pattern_bytes, compiled)).1
266 }
267 };
268 bits.push(compiled.matches(haystack.bytes(i)) != options.negated);
269 }
270 let validity = values
271 .validity()?
272 .and(patterns.validity()?)?
273 .union_nullability(nullability);
274 Ok(BoolArray::new(BitBuffer::from_iter(bits), validity).into_array())
275}
276
277struct ResolvedViews<'a> {
280 views: &'a [BinaryView],
281 buffers: Vec<&'a [u8]>,
282}
283
284impl<'a> ResolvedViews<'a> {
285 fn new(array: &'a VarBinViewArray) -> Self {
286 Self {
287 views: array.views(),
288 buffers: (0..array.data_buffers().len())
289 .map(|idx| array.buffer(idx).as_slice())
290 .collect(),
291 }
292 }
293
294 #[inline]
295 fn bytes(&self, index: usize) -> &'a [u8] {
296 let view = &self.views[index];
297 if view.is_inlined() {
298 view.as_inlined().value()
299 } else {
300 let view = view.as_view();
301 &self.buffers[view.buffer_index as usize][view.as_range()]
302 }
303 }
304
305 fn is_ascii(&self) -> bool {
307 (0..self.views.len()).all(|i| self.bytes(i).is_ascii())
308 }
309
310 #[inline]
318 unsafe fn suffix_bytes_unchecked(&self, view: &'a BinaryView, suffix_len: usize) -> &'a [u8] {
319 let len = view.len() as usize;
320 if view.is_inlined() {
321 unsafe { view.as_inlined().value().get_unchecked(len - suffix_len..) }
324 } else {
325 let view = view.as_view();
326 let end = view.offset as usize + len;
327 unsafe {
330 self.buffers
331 .get_unchecked(view.buffer_index as usize)
332 .get_unchecked(end - suffix_len..end)
333 }
334 }
335 }
336}
337
338fn eval_pattern(haystack: &ResolvedViews<'_>, pattern: &LikePattern, negated: bool) -> BitBuffer {
344 let len = haystack.views.len();
345 match pattern {
346 LikePattern::Eq(needle) if needle.len() <= BinaryView::MAX_INLINED_SIZE => {
347 let needle_view = BinaryView::new_inlined(needle).as_u128();
350 BitBuffer::collect_bool(len, |i| {
351 (haystack.views[i].as_u128() == needle_view) != negated
352 })
353 }
354 LikePattern::Eq(needle) => {
355 let needle_head = needle_head(needle);
358 BitBuffer::collect_bool(len, |i| {
359 let view = &haystack.views[i];
360 let matched =
361 view_head(view) == needle_head && haystack.bytes(i)[4..] == needle[4..];
362 matched != negated
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(len, |i| {
382 let view = &haystack.views[i];
383 let matched = view.len() as usize >= needle_len
384 && (view_prefix(view) & prefix_mask) == needle_prefix
385 && (needle_len <= 4 || haystack.bytes(i)[4..needle_len] == needle[4..]);
386 matched != negated
387 })
388 }
389 LikePattern::EndsWith(needle) => {
390 let needle_len = needle.len();
393 BitBuffer::collect_bool(len, |i| {
394 let matched = unsafe {
397 let view = haystack.views.get_unchecked(i);
398 view.len() as usize >= needle_len
399 && bytes_eq(haystack.suffix_bytes_unchecked(view, needle_len), needle)
400 };
401 matched != negated
402 })
403 }
404 LikePattern::IEqAscii(needle) => BitBuffer::collect_bool(len, |i| {
405 let view = &haystack.views[i];
406 let matched = view.len() as usize == needle.len()
407 && haystack.bytes(i).eq_ignore_ascii_case(needle);
408 matched != negated
409 }),
410 LikePattern::Contains(finder, needle_len) => BitBuffer::collect_bool(len, |i| {
411 let view = &haystack.views[i];
412 let matched =
413 view.len() as usize >= *needle_len && finder.find(haystack.bytes(i)).is_some();
414 matched != negated
415 }),
416 _ => BitBuffer::collect_bool(len, |i| pattern.matches(haystack.bytes(i)) != negated),
417 }
418}
419
420#[inline]
425fn bytes_eq(lhs: &[u8], rhs: &[u8]) -> bool {
426 lhs.len() == rhs.len() && std::iter::zip(lhs, rhs).all(|(l, r)| l == r)
427}
428
429#[inline]
432#[expect(clippy::cast_possible_truncation, reason = "intentional bit slicing")]
433fn view_head(view: &BinaryView) -> u64 {
434 view.as_u128() as u64
435}
436
437fn needle_head(needle: &[u8]) -> u64 {
439 let prefix: [u8; 4] = [needle[0], needle[1], needle[2], needle[3]];
440 (needle.len() as u64) | (u64::from(u32::from_le_bytes(prefix)) << 32)
441}
442
443#[inline]
446#[expect(clippy::cast_possible_truncation, reason = "intentional bit slicing")]
447fn view_prefix(view: &BinaryView) -> u32 {
448 (view.as_u128() >> 32) as u32
449}
450
451#[derive(Debug, PartialEq)]
453pub(crate) enum LikeVariant<'a> {
454 Exact(Cow<'a, str>),
455 Prefix(Cow<'a, str>),
456}
457
458impl<'a> LikeVariant<'a> {
459 pub(crate) fn from_str(string: &'a str) -> Option<LikeVariant<'a>> {
461 let mut literal = None;
462 let mut chars = string.char_indices();
463
464 while let Some((idx, c)) = chars.next() {
465 match c {
466 '\\' => {
467 let literal = literal.get_or_insert_with(|| string[..idx].to_string());
468 match chars.next() {
469 Some((_, escaped)) => literal.push(escaped),
470 None => literal.push('\\'),
471 }
472 }
473 '%' | '_' => {
474 return match literal {
475 Some(literal) => (!literal.is_empty())
476 .then_some(LikeVariant::Prefix(Cow::Owned(literal))),
477 None => {
478 (idx != 0).then_some(LikeVariant::Prefix(Cow::Borrowed(&string[..idx])))
479 }
480 };
481 }
482 c => {
483 if let Some(literal) = &mut literal {
484 literal.push(c);
485 }
486 }
487 }
488 }
489
490 Some(match literal {
491 Some(literal) => LikeVariant::Exact(Cow::Owned(literal)),
492 None => LikeVariant::Exact(Cow::Borrowed(string)),
493 })
494 }
495}
496
497#[cfg(test)]
498mod tests {
499 use std::borrow::Cow;
500
501 use rstest::rstest;
502
503 use crate::IntoArray;
504 use crate::VortexSessionExecute;
505 use crate::array_session;
506 use crate::arrays::BoolArray;
507 use crate::arrays::ConstantArray;
508 use crate::arrays::VarBinArray;
509 use crate::arrays::VarBinViewArray;
510 use crate::assert_arrays_eq;
511 use crate::dtype::DType;
512 use crate::dtype::Nullability;
513 use crate::expr::get_item;
514 use crate::expr::like;
515 use crate::expr::lit;
516 use crate::expr::not;
517 use crate::expr::not_ilike;
518 use crate::expr::root;
519 use crate::scalar::Scalar;
520 use crate::scalar_fn::fns::like::Like;
521 use crate::scalar_fn::fns::like::LikeOptions;
522 use crate::scalar_fn::fns::like::LikeVariant;
523
524 fn run_like(
525 array: crate::ArrayRef,
526 pattern: crate::ArrayRef,
527 options: LikeOptions,
528 ) -> crate::ArrayRef {
529 Like::try_new(array, pattern, options).unwrap().into_array()
530 }
531
532 #[rstest]
533 #[case("hello", [true, false, false, false])]
535 #[case("he%", [true, false, true, false])]
536 #[case("%llo", [true, false, false, true])]
537 #[case("%ell%", [true, false, false, true])]
538 #[case("h_llo", [true, false, false, false])]
540 #[case("h%o", [true, false, false, false])]
541 #[case("%", [true, true, true, true])]
542 #[case("_____", [true, true, false, true])]
543 fn test_like_patterns(#[case] pattern: &str, #[case] expected: [bool; 4]) {
544 let mut ctx = array_session().create_execution_ctx();
545 let array =
546 VarBinViewArray::from_iter_str(["hello", "world", "help", "jello"]).into_array();
547 let result = run_like(
548 array,
549 ConstantArray::new(pattern, 4).into_array(),
550 LikeOptions::default(),
551 );
552 assert_arrays_eq!(result, BoolArray::from_iter(expected), &mut ctx);
553 }
554
555 #[test]
556 fn test_like_escapes() {
557 let mut ctx = array_session().create_execution_ctx();
558 let array = VarBinViewArray::from_iter_str(["100%", "100x", "a_b", "axb"]).into_array();
559
560 let result = run_like(
561 array.clone(),
562 ConstantArray::new(r"100\%", 4).into_array(),
563 LikeOptions::default(),
564 );
565 assert_arrays_eq!(
566 result,
567 BoolArray::from_iter([true, false, false, false]),
568 &mut ctx
569 );
570
571 let result = run_like(
572 array,
573 ConstantArray::new(r"a\_b", 4).into_array(),
574 LikeOptions::default(),
575 );
576 assert_arrays_eq!(
577 result,
578 BoolArray::from_iter([false, false, true, false]),
579 &mut ctx
580 );
581 }
582
583 #[test]
584 fn test_like_regex_meta_characters_are_literal() {
585 let mut ctx = array_session().create_execution_ctx();
586 let array = VarBinViewArray::from_iter_str(["a.c", "abc", "a$c"]).into_array();
587 let result = run_like(
588 array,
589 ConstantArray::new("a.%", 3).into_array(),
590 LikeOptions::default(),
591 );
592 assert_arrays_eq!(result, BoolArray::from_iter([true, false, false]), &mut ctx);
593 }
594
595 #[test]
596 fn test_like_unicode() {
597 let mut ctx = array_session().create_execution_ctx();
598 let array =
599 VarBinViewArray::from_iter_str(["h\u{00a3}llo", "hxllo", "h\u{00a3}xllo"]).into_array();
600 let result = run_like(
602 array,
603 ConstantArray::new("h_llo", 3).into_array(),
604 LikeOptions::default(),
605 );
606 assert_arrays_eq!(result, BoolArray::from_iter([true, true, false]), &mut ctx);
607 }
608
609 #[test]
610 fn test_nlike() {
611 let mut ctx = array_session().create_execution_ctx();
612 let array = VarBinViewArray::from_iter_str(["hello", "world"]).into_array();
613 let result = run_like(
614 array,
615 ConstantArray::new("he%", 2).into_array(),
616 LikeOptions {
617 negated: true,
618 case_insensitive: false,
619 },
620 );
621 assert_arrays_eq!(result, BoolArray::from_iter([false, true]), &mut ctx);
622 }
623
624 #[test]
625 fn test_ilike() {
626 let mut ctx = array_session().create_execution_ctx();
627 let ilike = LikeOptions {
628 negated: false,
629 case_insensitive: true,
630 };
631
632 let array = VarBinViewArray::from_iter_str(["HELLO", "world", "Help"]).into_array();
634 let result = run_like(array, ConstantArray::new("he%", 3).into_array(), ilike);
635 assert_arrays_eq!(result, BoolArray::from_iter([true, false, true]), &mut ctx);
636
637 let array = VarBinViewArray::from_iter_str(["\u{212a}", "k", "x"]).into_array();
639 let result = run_like(array, ConstantArray::new("k", 3).into_array(), ilike);
640 assert_arrays_eq!(result, BoolArray::from_iter([true, true, false]), &mut ctx);
641 }
642
643 #[test]
644 fn test_nilike() {
645 let mut ctx = array_session().create_execution_ctx();
646 let array = VarBinViewArray::from_iter_str(["HELLO", "world"]).into_array();
647 let result = run_like(
648 array,
649 ConstantArray::new("he%", 2).into_array(),
650 LikeOptions {
651 negated: true,
652 case_insensitive: true,
653 },
654 );
655 assert_arrays_eq!(result, BoolArray::from_iter([false, true]), &mut ctx);
656 }
657
658 #[test]
659 fn test_like_nullable_input() {
660 let mut ctx = array_session().create_execution_ctx();
661 let array = VarBinViewArray::from_iter_nullable_str([Some("hello"), None, Some("help")])
662 .into_array();
663 let result = run_like(
664 array,
665 ConstantArray::new("he%", 3).into_array(),
666 LikeOptions::default(),
667 );
668 assert_arrays_eq!(
669 result,
670 BoolArray::from_iter([Some(true), None, Some(true)]),
671 &mut ctx
672 );
673 }
674
675 #[test]
676 fn test_like_null_pattern() {
677 let mut ctx = array_session().create_execution_ctx();
678 let array = VarBinViewArray::from_iter_str(["hello", "world"]).into_array();
679 let result = run_like(
680 array,
681 ConstantArray::new(Scalar::null(DType::Utf8(Nullability::Nullable)), 2).into_array(),
682 LikeOptions::default(),
683 );
684 assert_arrays_eq!(result, BoolArray::from_iter([None, None]), &mut ctx);
685 }
686
687 #[test]
688 fn test_like_per_row_patterns() {
689 let mut ctx = array_session().create_execution_ctx();
690 let array = VarBinViewArray::from_iter_str(["hello", "hello", "hello"]).into_array();
691 let patterns = VarBinViewArray::from_iter_str(["he%", "%world", "h_llo"]).into_array();
692 let result = run_like(array, patterns, LikeOptions::default());
693 assert_arrays_eq!(result, BoolArray::from_iter([true, false, true]), &mut ctx);
694 }
695
696 #[test]
697 fn test_like_non_canonical_input() {
698 let mut ctx = array_session().create_execution_ctx();
699 let array = VarBinArray::from_iter(
701 [Some("hello"), Some("world")],
702 DType::Utf8(Nullability::Nullable),
703 )
704 .into_array();
705 let result = run_like(
706 array,
707 ConstantArray::new("he%", 2).into_array(),
708 LikeOptions::default(),
709 );
710 assert_arrays_eq!(
711 result,
712 BoolArray::from_iter([Some(true), Some(false)]),
713 &mut ctx
714 );
715 }
716
717 #[test]
718 fn invert_booleans() {
719 let not_expr = not(root());
720 let bools = BoolArray::from_iter([false, true, false, false, true, true]);
721 let mut ctx = array_session().create_execution_ctx();
722 assert_arrays_eq!(
723 bools.into_array().apply(¬_expr).unwrap(),
724 BoolArray::from_iter([true, false, true, true, false, false]),
725 &mut ctx
726 );
727 }
728
729 #[test]
730 fn dtype() {
731 let dtype = DType::Utf8(Nullability::NonNullable);
732 let like_expr = like(root(), lit("%test%"));
733 assert_eq!(
734 like_expr.return_dtype(&dtype).unwrap(),
735 DType::Bool(Nullability::NonNullable)
736 );
737 }
738
739 #[test]
740 fn signature() {
741 let like_expr = like(root(), lit("%test%"));
742 assert!(
743 like_expr
744 .as_scalar()
745 .is_some_and(|f| f.signature().is_strict())
746 );
747 assert!(
748 like_expr
749 .as_scalar()
750 .is_some_and(|f| f.signature().is_infallible())
751 );
752 }
753
754 #[test]
755 fn test_display() {
756 let expr = like(get_item("name", root()), lit("%john%"));
757 assert_eq!(expr.to_string(), "$.name like \"%john%\"");
758
759 let expr2 = not_ilike(root(), lit("test*"));
760 assert_eq!(expr2.to_string(), "$ not ilike \"test*\"");
761 }
762
763 fn assert_borrowed_exact(pattern: &str, expected: &str) {
764 let Some(LikeVariant::Exact(actual)) = LikeVariant::from_str(pattern) else {
765 panic!("expected borrowed exact pattern");
766 };
767 assert!(matches!(actual, Cow::Borrowed(_)));
768 assert_eq!(actual.as_ref(), expected);
769 }
770
771 fn assert_owned_exact(pattern: &str, expected: &str) {
772 let Some(LikeVariant::Exact(actual)) = LikeVariant::from_str(pattern) else {
773 panic!("expected owned exact pattern");
774 };
775 assert!(matches!(actual, Cow::Owned(_)));
776 assert_eq!(actual.as_ref(), expected);
777 }
778
779 fn assert_borrowed_prefix(pattern: &str, expected: &str) {
780 let Some(LikeVariant::Prefix(actual)) = LikeVariant::from_str(pattern) else {
781 panic!("expected borrowed prefix pattern");
782 };
783 assert!(matches!(actual, Cow::Borrowed(_)));
784 assert_eq!(actual.as_ref(), expected);
785 }
786
787 fn assert_owned_prefix(pattern: &str, expected: &str) {
788 let Some(LikeVariant::Prefix(actual)) = LikeVariant::from_str(pattern) else {
789 panic!("expected owned prefix pattern");
790 };
791 assert!(matches!(actual, Cow::Owned(_)));
792 assert_eq!(actual.as_ref(), expected);
793 }
794
795 #[test]
796 fn test_like_variant_borrowed_patterns() {
797 assert_borrowed_exact("simple", "simple");
798 assert_borrowed_prefix("prefix%", "prefix");
799 assert_borrowed_prefix("first%rest_stuff", "first");
800 }
801
802 #[test]
803 fn test_like_variant_escaped_patterns() {
804 assert_owned_prefix(r"\%%", "%");
805 assert_owned_prefix(r"\_%", "_");
806 assert_owned_prefix(r"\\%", "\\");
807 assert_owned_exact(r"\%", "%");
808 assert_owned_exact("trailing\\", "trailing\\");
809 }
810
811 #[test]
812 fn test_like_variant_unsupported_patterns() {
813 assert_eq!(LikeVariant::from_str("%suffix"), None);
814 assert_eq!(LikeVariant::from_str(r"%\%%"), None);
815 assert_eq!(LikeVariant::from_str("_pattern"), None);
816 }
817}