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_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/// Options for SQL LIKE function
45#[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/// Expression that performs SQL LIKE pattern matching.
65#[derive(Clone)]
66pub struct Like;
67
68impl Like {
69    /// Creates a lazy SQL `LIKE` operation over `input` and `pattern`.
70    ///
71    /// # Errors
72    ///
73    /// Returns an error if the children have different lengths or are not UTF-8 arrays.
74    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
194/// Native implementation of LIKE over canonical [`VarBinViewArray`]s.
195///
196/// Patterns are compiled once per distinct pattern (see [`LikePattern`]) and evaluated per
197/// value directly on the view bytes.
198pub(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            // A null pattern makes every row null.
221            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        // The ASCII case-insensitive fast paths are only sound when the haystack is pure
229        // ASCII; see `LikePattern::compile`.
230        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    // Per-row patterns: compile each pattern individually.
243    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    // Reuse the previous row's compiled pattern while the pattern bytes repeat, so runs of
251    // identical patterns (the common case for non-constant pattern children) compile once.
252    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
277/// Resolved views over a canonical [`VarBinViewArray`]: the view structs plus borrowed slices
278/// of every data buffer, supporting cheap per-element byte access.
279struct 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    /// Whether every value (including values under a null) is pure ASCII.
306    fn is_ascii(&self) -> bool {
307        (0..self.views.len()).all(|i| self.bytes(i).is_ascii())
308    }
309
310    /// The last `suffix_len` bytes of `view`, which must belong to this array.
311    ///
312    /// # Safety
313    ///
314    /// `suffix_len` must be at most `view.len()`. Buffer bounds are guaranteed by
315    /// [`VarBinViewArray::validate`], which checks every view against its data buffer at
316    /// construction.
317    #[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            // SAFETY: inlined values hold `len <= 12` value bytes, and the caller
322            // guarantees `suffix_len <= len`.
323            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            // SAFETY: validated views reference `buffer_index < buffers.len()` and bytes
328            // `offset..offset + len` within that buffer.
329            unsafe {
330                self.buffers
331                    .get_unchecked(view.buffer_index as usize)
332                    .get_unchecked(end - suffix_len..end)
333            }
334        }
335    }
336}
337
338/// Evaluate `pattern` against every element of `haystack`.
339///
340/// The equality, prefix, and suffix patterns exploit the view layout: a view stores the value
341/// length and its first four bytes inline, which settles most elements without touching the
342/// data buffers (values of up to 12 bytes are stored entirely inline).
343fn 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            // The needle fits in a view, so equality is a single 16-byte comparison: a view
348            // of a different length or prefix can never share the same bit pattern.
349            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            // Compare the view head (length plus 4-byte prefix) first; only views that agree
356            // on both dereference their data buffer for the remaining bytes.
357            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            // A branch-free masked comparison of the view's inline 4-byte prefix rejects
367            // almost every element without touching the data buffers; only elements whose
368            // prefix matches compare the remaining needle bytes.
369            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            // Inlined values compare their suffix inside the view struct without touching the
391            // data buffers; reference views slice exactly the suffix out of their buffer.
392            let needle_len = needle.len();
393            BitBuffer::collect_bool(len, |i| {
394                // SAFETY: `i` is below the array length, and the suffix length is only read
395                // once the view is known to be at least `needle_len` long.
396                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/// Byte equality as an inlined loop with early exit.
421///
422/// Faster than slice `==` for the short, unpredictable-length comparisons in this module,
423/// which otherwise lower to a `memcmp` libcall per element.
424#[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/// The leading 8 bytes of a view: the `u32` length plus the first 4 bytes of the value
430/// (zero-padded for values shorter than 4 bytes).
431#[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
437/// The view head a needle of more than 4 bytes would have: its length plus first 4 bytes.
438fn 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/// The first 4 value bytes stored inline in any view (zero-padded for values shorter than
444/// 4 bytes), as a raw little-endian `u32` in memory order.
445#[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/// Variants of the LIKE filter that we know how to turn into a stats pruning predicate.
452#[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    /// Parse a LIKE pattern string into its relevant variant
460    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    // Exact, prefix, suffix, contains fast paths.
534    #[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    // Wildcards that require the regex path.
539    #[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        // `_` matches exactly one character of any byte width.
601        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        // Pure-ASCII data takes the ASCII fast paths.
633        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        // Non-ASCII data requires full case folding: `k` matches U+212A KELVIN SIGN.
638        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        // VarBin (not the canonical VarBinView) is canonicalized before evaluation.
700        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(&not_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}