Skip to main content

vortex_array/scalar_fn/fns/like/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4mod 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/// Options for SQL LIKE function
46#[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/// Expression that performs SQL LIKE pattern matching.
66#[derive(Clone)]
67pub struct Like;
68
69impl Like {
70    /// Creates a lazy SQL `LIKE` operation over `input` and `pattern`.
71    ///
72    /// # Errors
73    ///
74    /// Returns an error if the children have different lengths or are not UTF-8 arrays.
75    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
184/// Native implementation of LIKE over canonical [`VarBinViewArray`]s.
185///
186/// Patterns are compiled once per distinct pattern (see [`LikePattern`]) and evaluated per
187/// value directly on the view bytes.
188pub(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            // A null pattern makes every row null.
211            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        // The ASCII case-insensitive fast paths are only sound when the haystack is pure
219        // ASCII; see `LikePattern::compile`.
220        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    // Per-row patterns: compile each pattern individually.
233    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    // Reuse the previous row's compiled pattern while the pattern bytes repeat, so runs of
240    // identical patterns (the common case for non-constant pattern children) compile once.
241    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
267/// Evaluate `pattern` against every element of `haystack`.
268///
269/// The equality, prefix, and suffix patterns exploit the view layout: a view stores the value
270/// length and its first four bytes inline, which settles most elements without touching the
271/// data buffers (values of up to 12 bytes are stored entirely inline).
272fn 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            // The needle fits in a view, so equality is a single 16-byte comparison: a view
282            // of a different length or prefix can never share the same bit pattern.
283            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            // Compare the view head (length plus 4-byte prefix) first; only views that agree
292            // on both dereference their data buffer for the remaining bytes.
293            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            // A branch-free masked comparison of the view's inline 4-byte prefix rejects
307            // almost every element without touching the data buffers; only elements whose
308            // prefix matches compare the remaining needle bytes.
309            let needle_len = needle.len();
310            let prefix_len = needle_len.min(4);
311            // Raw prefix bytes, unswapped: this is a masked equality, not an ordering.
312            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            // Inlined values compare their suffix inside the view struct without touching the
332            // data buffers; reference views slice exactly the suffix out of their buffer.
333            let needle_len = needle.len();
334            BitBuffer::collect_bool_in(
335                len,
336                |i| {
337                    // SAFETY: `i` is below the array length, and the suffix length is only read
338                    // once the view is known to be at least `needle_len` long.
339                    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
377/// Whether every value in `haystack` is ASCII. Validity is not consulted.
378fn is_ascii(haystack: &ResolvedViews<'_>) -> bool {
379    haystack
380        .views()
381        .iter()
382        .all(|view| haystack.view_bytes(view).is_ascii())
383}
384
385/// The last `suffix_len` bytes of `view`, which must belong to `haystack`.
386///
387/// # Safety
388///
389/// `suffix_len` must be at most `view.len()`.
390#[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        // SAFETY: caller guarantees suffix_len <= len.
399        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        // SAFETY: the array validated this view's buffer index and range on construction.
404        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/// Byte equality as an inlined loop with early exit.
414///
415/// Faster than slice `==` for the short, unpredictable-length comparisons in this module,
416/// which otherwise lower to a `memcmp` libcall per element.
417#[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/// Variants of the LIKE filter that we know how to turn into a stats pruning predicate.
423#[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    /// Parse a LIKE pattern string into its relevant variant
431    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    // Exact, prefix, suffix, contains fast paths.
509    #[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    // Wildcards that require the regex path.
514    #[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        // `_` matches exactly one character of any byte width.
576        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        // Pure-ASCII data takes the ASCII fast paths.
608        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        // Non-ASCII data requires full case folding: `k` matches U+212A KELVIN SIGN.
613        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        // VarBin (not the canonical VarBinView) is canonicalized before evaluation.
675        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(&not_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                // SAFETY: suffix_len <= value.len(), which is view.len().
801                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}