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::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/// Options for SQL LIKE function
43#[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/// Expression that performs SQL LIKE pattern matching.
63#[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
177/// Native implementation of LIKE over canonical [`VarBinViewArray`]s.
178///
179/// Patterns are compiled once per distinct pattern (see [`LikePattern`]) and evaluated per
180/// value directly on the view bytes.
181pub(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            // A null pattern makes every row null.
204            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        // The ASCII case-insensitive fast paths are only sound when the haystack is pure
212        // ASCII; see `LikePattern::compile`.
213        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    // Per-row patterns: compile each pattern individually.
226    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    // Reuse the previous row's compiled pattern while the pattern bytes repeat, so runs of
234    // identical patterns (the common case for non-constant pattern children) compile once.
235    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
260/// Resolved views over a canonical [`VarBinViewArray`]: the view structs plus borrowed slices
261/// of every data buffer, supporting cheap per-element byte access.
262struct 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    /// Whether every value (including values under a null) is pure ASCII.
289    fn is_ascii(&self) -> bool {
290        (0..self.views.len()).all(|i| self.bytes(i).is_ascii())
291    }
292
293    /// The last `suffix_len` bytes of `view`, which must belong to this array.
294    ///
295    /// # Safety
296    ///
297    /// `suffix_len` must be at most `view.len()`. Buffer bounds are guaranteed by
298    /// [`VarBinViewArray::validate`], which checks every view against its data buffer at
299    /// construction.
300    #[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            // SAFETY: inlined values hold `len <= 12` value bytes, and the caller
305            // guarantees `suffix_len <= len`.
306            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            // SAFETY: validated views reference `buffer_index < buffers.len()` and bytes
311            // `offset..offset + len` within that buffer.
312            unsafe {
313                self.buffers
314                    .get_unchecked(view.buffer_index as usize)
315                    .get_unchecked(end - suffix_len..end)
316            }
317        }
318    }
319}
320
321/// Evaluate `pattern` against every element of `haystack`.
322///
323/// The equality, prefix, and suffix patterns exploit the view layout: a view stores the value
324/// length and its first four bytes inline, which settles most elements without touching the
325/// data buffers (values of up to 12 bytes are stored entirely inline).
326fn 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            // The needle fits in a view, so equality is a single 16-byte comparison: a view
331            // of a different length or prefix can never share the same bit pattern.
332            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            // Compare the view head (length plus 4-byte prefix) first; only views that agree
339            // on both dereference their data buffer for the remaining bytes.
340            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            // A branch-free masked comparison of the view's inline 4-byte prefix rejects
350            // almost every element without touching the data buffers; only elements whose
351            // prefix matches compare the remaining needle bytes.
352            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            // Inlined values compare their suffix inside the view struct without touching the
374            // data buffers; reference views slice exactly the suffix out of their buffer.
375            let needle_len = needle.len();
376            BitBuffer::collect_bool(len, |i| {
377                // SAFETY: `i` is below the array length, and the suffix length is only read
378                // once the view is known to be at least `needle_len` long.
379                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/// Byte equality as an inlined loop with early exit.
404///
405/// Faster than slice `==` for the short, unpredictable-length comparisons in this module,
406/// which otherwise lower to a `memcmp` libcall per element.
407#[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/// The leading 8 bytes of a view: the `u32` length plus the first 4 bytes of the value
413/// (zero-padded for values shorter than 4 bytes).
414#[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
420/// The view head a needle of more than 4 bytes would have: its length plus first 4 bytes.
421fn 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/// The first 4 value bytes stored inline in any view (zero-padded for values shorter than
427/// 4 bytes), as a raw little-endian `u32` in memory order.
428#[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/// Variants of the LIKE filter that we know how to turn into a stats pruning predicate.
435#[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    /// Parse a LIKE pattern string into its relevant variant
443    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    // Exact, prefix, suffix, contains fast paths.
519    #[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    // Wildcards that require the regex path.
524    #[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        // `_` matches exactly one character of any byte width.
586        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        // Pure-ASCII data takes the ASCII fast paths.
618        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        // Non-ASCII data requires full case folding: `k` matches U+212A KELVIN SIGN.
623        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        // VarBin (not the canonical VarBinView) is canonicalized before evaluation.
685        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(&not_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}