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::dtype::DType;
33use crate::dtype::Nullability;
34use crate::expr::display::ExprDisplay;
35use crate::proto::expr as pb;
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 is_strict(&self, _instance: &Self::Options) -> bool {
175        true
176    }
177
178    fn is_infallible(&self, _options: &Self::Options) -> bool {
179        true
180    }
181}
182
183/// Native implementation of LIKE over canonical [`VarBinViewArray`]s.
184///
185/// Patterns are compiled once per distinct pattern (see [`LikePattern`]) and evaluated per
186/// value directly on the view bytes.
187pub(crate) fn execute_like(
188    array: &ArrayRef,
189    pattern: &ArrayRef,
190    options: LikeOptions,
191    ctx: &mut ExecutionCtx,
192) -> VortexResult<ArrayRef> {
193    assert_eq!(
194        array.len(),
195        pattern.len(),
196        "LIKE: length mismatch for {}",
197        array.encoding_id()
198    );
199    let len = array.len();
200    let nullability =
201        Nullability::from(array.dtype().is_nullable() || pattern.dtype().is_nullable());
202
203    if len == 0 {
204        return Ok(Canonical::empty(&DType::Bool(nullability)).into_array());
205    }
206
207    if let Some(pattern_const) = pattern.as_constant() {
208        let Some(pattern_str) = pattern_const.as_utf8().value() else {
209            // A null pattern makes every row null.
210            return Ok(
211                ConstantArray::new(Scalar::null(DType::Bool(Nullability::Nullable)), len)
212                    .into_array(),
213            );
214        };
215        let values = array.clone().execute::<VarBinViewArray>(ctx)?;
216        let haystack = ResolvedViews::new(&values);
217        // The ASCII case-insensitive fast paths are only sound when the haystack is pure
218        // ASCII; see `LikePattern::compile`.
219        let ascii_haystack =
220            options.case_insensitive && pattern_str.as_str().is_ascii() && haystack.is_ascii();
221        let compiled = LikePattern::compile(
222            pattern_str.as_str(),
223            options.case_insensitive,
224            ascii_haystack,
225        )?;
226        let bits = eval_pattern(&haystack, &compiled, options.negated, ctx.allocator());
227        let validity = values.validity()?.union_nullability(nullability);
228        return Ok(BoolArray::new(bits, validity).into_array());
229    }
230
231    // Per-row patterns: compile each pattern individually.
232    let values = array.clone().execute::<VarBinViewArray>(ctx)?;
233    let patterns = pattern.clone().execute::<VarBinViewArray>(ctx)?;
234    let haystack = ResolvedViews::new(&values);
235    let pattern_views = ResolvedViews::new(&patterns);
236    let ascii_haystack = options.case_insensitive && haystack.is_ascii();
237
238    // Reuse the previous row's compiled pattern while the pattern bytes repeat, so runs of
239    // identical patterns (the common case for non-constant pattern children) compile once.
240    let mut cached: Option<(&[u8], LikePattern)> = None;
241    let mut bits = BitBufferMut::with_capacity_in(len, ctx.allocator().clone());
242    for i in 0..len {
243        let pattern_bytes = pattern_views.bytes(i);
244        let compiled = match &cached {
245            Some((bytes, compiled)) if *bytes == pattern_bytes => compiled,
246            _ => {
247                let pattern_str = std::str::from_utf8(pattern_bytes)
248                    .map_err(|e| vortex_err!("LIKE pattern is not valid UTF-8: {e}"))?;
249                let compiled = LikePattern::compile(
250                    pattern_str,
251                    options.case_insensitive,
252                    ascii_haystack && pattern_str.is_ascii(),
253                )?;
254                &cached.insert((pattern_bytes, compiled)).1
255            }
256        };
257        bits.append(compiled.matches(haystack.bytes(i)) != options.negated);
258    }
259    let validity = values
260        .validity()?
261        .and(patterns.validity()?)?
262        .union_nullability(nullability);
263    Ok(BoolArray::new(bits.freeze(), validity).into_array())
264}
265
266/// Resolved views over a canonical [`VarBinViewArray`]: the view structs plus borrowed slices
267/// of every data buffer, supporting cheap per-element byte access.
268struct ResolvedViews<'a> {
269    views: &'a [BinaryView],
270    buffers: Vec<&'a [u8]>,
271}
272
273impl<'a> ResolvedViews<'a> {
274    fn new(array: &'a VarBinViewArray) -> Self {
275        Self {
276            views: array.views(),
277            buffers: (0..array.data_buffers().len())
278                .map(|idx| array.buffer(idx).as_slice())
279                .collect(),
280        }
281    }
282
283    #[inline]
284    fn bytes(&self, index: usize) -> &'a [u8] {
285        let view = &self.views[index];
286        if view.is_inlined() {
287            view.as_inlined().value()
288        } else {
289            let view = view.as_view();
290            &self.buffers[view.buffer_index as usize][view.as_range()]
291        }
292    }
293
294    /// Whether every value (including values under a null) is pure ASCII.
295    fn is_ascii(&self) -> bool {
296        (0..self.views.len()).all(|i| self.bytes(i).is_ascii())
297    }
298
299    /// The last `suffix_len` bytes of `view`, which must belong to this array.
300    ///
301    /// # Safety
302    ///
303    /// `suffix_len` must be at most `view.len()`. Buffer bounds are guaranteed by
304    /// [`VarBinViewArray::validate`], which checks every view against its data buffer at
305    /// construction.
306    #[inline]
307    unsafe fn suffix_bytes_unchecked(&self, view: &'a BinaryView, suffix_len: usize) -> &'a [u8] {
308        let len = view.len() as usize;
309        if view.is_inlined() {
310            // SAFETY: inlined values hold `len <= 12` value bytes, and the caller
311            // guarantees `suffix_len <= len`.
312            unsafe { view.as_inlined().value().get_unchecked(len - suffix_len..) }
313        } else {
314            let view = view.as_view();
315            let end = view.offset as usize + len;
316            // SAFETY: validated views reference `buffer_index < buffers.len()` and bytes
317            // `offset..offset + len` within that buffer.
318            unsafe {
319                self.buffers
320                    .get_unchecked(view.buffer_index as usize)
321                    .get_unchecked(end - suffix_len..end)
322            }
323        }
324    }
325}
326
327/// Evaluate `pattern` against every element of `haystack`.
328///
329/// The equality, prefix, and suffix patterns exploit the view layout: a view stores the value
330/// length and its first four bytes inline, which settles most elements without touching the
331/// data buffers (values of up to 12 bytes are stored entirely inline).
332fn eval_pattern(
333    haystack: &ResolvedViews<'_>,
334    pattern: &LikePattern,
335    negated: bool,
336    allocator: &BufferAllocatorRef,
337) -> BitBuffer {
338    let len = haystack.views.len();
339    match pattern {
340        LikePattern::Eq(needle) if needle.len() <= BinaryView::MAX_INLINED_SIZE => {
341            // The needle fits in a view, so equality is a single 16-byte comparison: a view
342            // of a different length or prefix can never share the same bit pattern.
343            let needle_view = BinaryView::new_inlined(needle).as_u128();
344            BitBuffer::collect_bool_in(
345                len,
346                |i| (haystack.views[i].as_u128() == needle_view) != negated,
347                allocator.clone(),
348            )
349        }
350        LikePattern::Eq(needle) => {
351            // Compare the view head (length plus 4-byte prefix) first; only views that agree
352            // on both dereference their data buffer for the remaining bytes.
353            let needle_head = needle_head(needle);
354            BitBuffer::collect_bool_in(
355                len,
356                |i| {
357                    let view = &haystack.views[i];
358                    let matched =
359                        view_head(view) == needle_head && haystack.bytes(i)[4..] == needle[4..];
360                    matched != negated
361                },
362                allocator.clone(),
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_in(
382                len,
383                |i| {
384                    let view = &haystack.views[i];
385                    let matched = view.len() as usize >= needle_len
386                        && (view_prefix(view) & prefix_mask) == needle_prefix
387                        && (needle_len <= 4 || haystack.bytes(i)[4..needle_len] == needle[4..]);
388                    matched != negated
389                },
390                allocator.clone(),
391            )
392        }
393        LikePattern::EndsWith(needle) => {
394            // Inlined values compare their suffix inside the view struct without touching the
395            // data buffers; reference views slice exactly the suffix out of their buffer.
396            let needle_len = needle.len();
397            BitBuffer::collect_bool_in(
398                len,
399                |i| {
400                    // SAFETY: `i` is below the array length, and the suffix length is only read
401                    // once the view is known to be at least `needle_len` long.
402                    let matched = unsafe {
403                        let view = haystack.views.get_unchecked(i);
404                        view.len() as usize >= needle_len
405                            && bytes_eq(haystack.suffix_bytes_unchecked(view, needle_len), needle)
406                    };
407                    matched != negated
408                },
409                allocator.clone(),
410            )
411        }
412        LikePattern::IEqAscii(needle) => BitBuffer::collect_bool_in(
413            len,
414            |i| {
415                let view = &haystack.views[i];
416                let matched = view.len() as usize == needle.len()
417                    && haystack.bytes(i).eq_ignore_ascii_case(needle);
418                matched != negated
419            },
420            allocator.clone(),
421        ),
422        LikePattern::Contains(finder, needle_len) => BitBuffer::collect_bool_in(
423            len,
424            |i| {
425                let view = &haystack.views[i];
426                let matched =
427                    view.len() as usize >= *needle_len && finder.find(haystack.bytes(i)).is_some();
428                matched != negated
429            },
430            allocator.clone(),
431        ),
432        _ => BitBuffer::collect_bool_in(
433            len,
434            |i| pattern.matches(haystack.bytes(i)) != negated,
435            allocator.clone(),
436        ),
437    }
438}
439
440/// Byte equality as an inlined loop with early exit.
441///
442/// Faster than slice `==` for the short, unpredictable-length comparisons in this module,
443/// which otherwise lower to a `memcmp` libcall per element.
444#[inline]
445fn bytes_eq(lhs: &[u8], rhs: &[u8]) -> bool {
446    lhs.len() == rhs.len() && std::iter::zip(lhs, rhs).all(|(l, r)| l == r)
447}
448
449/// The leading 8 bytes of a view: the `u32` length plus the first 4 bytes of the value
450/// (zero-padded for values shorter than 4 bytes).
451#[inline]
452#[expect(clippy::cast_possible_truncation, reason = "intentional bit slicing")]
453fn view_head(view: &BinaryView) -> u64 {
454    view.as_u128() as u64
455}
456
457/// The view head a needle of more than 4 bytes would have: its length plus first 4 bytes.
458fn needle_head(needle: &[u8]) -> u64 {
459    let prefix: [u8; 4] = [needle[0], needle[1], needle[2], needle[3]];
460    (needle.len() as u64) | (u64::from(u32::from_le_bytes(prefix)) << 32)
461}
462
463/// The first 4 value bytes stored inline in any view (zero-padded for values shorter than
464/// 4 bytes), as a raw little-endian `u32` in memory order.
465#[inline]
466#[expect(clippy::cast_possible_truncation, reason = "intentional bit slicing")]
467fn view_prefix(view: &BinaryView) -> u32 {
468    (view.as_u128() >> 32) as u32
469}
470
471/// Variants of the LIKE filter that we know how to turn into a stats pruning predicate.
472#[derive(Debug, PartialEq)]
473pub(crate) enum LikeVariant<'a> {
474    Exact(Cow<'a, str>),
475    Prefix(Cow<'a, str>),
476}
477
478impl<'a> LikeVariant<'a> {
479    /// Parse a LIKE pattern string into its relevant variant
480    pub(crate) fn from_str(string: &'a str) -> Option<LikeVariant<'a>> {
481        let mut literal = None;
482        let mut chars = string.char_indices();
483
484        while let Some((idx, c)) = chars.next() {
485            match c {
486                '\\' => {
487                    let literal = literal.get_or_insert_with(|| string[..idx].to_string());
488                    match chars.next() {
489                        Some((_, escaped)) => literal.push(escaped),
490                        None => literal.push('\\'),
491                    }
492                }
493                '%' | '_' => {
494                    return match literal {
495                        Some(literal) => (!literal.is_empty())
496                            .then_some(LikeVariant::Prefix(Cow::Owned(literal))),
497                        None => {
498                            (idx != 0).then_some(LikeVariant::Prefix(Cow::Borrowed(&string[..idx])))
499                        }
500                    };
501                }
502                c => {
503                    if let Some(literal) = &mut literal {
504                        literal.push(c);
505                    }
506                }
507            }
508        }
509
510        Some(match literal {
511            Some(literal) => LikeVariant::Exact(Cow::Owned(literal)),
512            None => LikeVariant::Exact(Cow::Borrowed(string)),
513        })
514    }
515}
516
517#[cfg(test)]
518mod tests {
519    use std::borrow::Cow;
520
521    use rstest::rstest;
522
523    use crate::IntoArray;
524    use crate::VortexSessionExecute;
525    use crate::array_session;
526    use crate::arrays::BoolArray;
527    use crate::arrays::ConstantArray;
528    use crate::arrays::VarBinArray;
529    use crate::arrays::VarBinViewArray;
530    use crate::assert_arrays_eq;
531    use crate::dtype::DType;
532    use crate::dtype::Nullability;
533    use crate::expr::get_item;
534    use crate::expr::like;
535    use crate::expr::lit;
536    use crate::expr::not;
537    use crate::expr::not_ilike;
538    use crate::expr::root;
539    use crate::scalar::Scalar;
540    use crate::scalar_fn::fns::like::Like;
541    use crate::scalar_fn::fns::like::LikeOptions;
542    use crate::scalar_fn::fns::like::LikeVariant;
543
544    fn run_like(
545        array: crate::ArrayRef,
546        pattern: crate::ArrayRef,
547        options: LikeOptions,
548    ) -> crate::ArrayRef {
549        Like::try_new(array, pattern, options).unwrap().into_array()
550    }
551
552    #[rstest]
553    // Exact, prefix, suffix, contains fast paths.
554    #[case("hello", [true, false, false, false])]
555    #[case("he%", [true, false, true, false])]
556    #[case("%llo", [true, false, false, true])]
557    #[case("%ell%", [true, false, false, true])]
558    // Wildcards that require the regex path.
559    #[case("h_llo", [true, false, false, false])]
560    #[case("h%o", [true, false, false, false])]
561    #[case("%", [true, true, true, true])]
562    #[case("_____", [true, true, false, true])]
563    fn test_like_patterns(#[case] pattern: &str, #[case] expected: [bool; 4]) {
564        let mut ctx = array_session().create_execution_ctx();
565        let array =
566            VarBinViewArray::from_iter_str(["hello", "world", "help", "jello"]).into_array();
567        let result = run_like(
568            array,
569            ConstantArray::new(pattern, 4).into_array(),
570            LikeOptions::default(),
571        );
572        assert_arrays_eq!(result, BoolArray::from_iter(expected), &mut ctx);
573    }
574
575    #[test]
576    fn test_like_escapes() {
577        let mut ctx = array_session().create_execution_ctx();
578        let array = VarBinViewArray::from_iter_str(["100%", "100x", "a_b", "axb"]).into_array();
579
580        let result = run_like(
581            array.clone(),
582            ConstantArray::new(r"100\%", 4).into_array(),
583            LikeOptions::default(),
584        );
585        assert_arrays_eq!(
586            result,
587            BoolArray::from_iter([true, false, false, false]),
588            &mut ctx
589        );
590
591        let result = run_like(
592            array,
593            ConstantArray::new(r"a\_b", 4).into_array(),
594            LikeOptions::default(),
595        );
596        assert_arrays_eq!(
597            result,
598            BoolArray::from_iter([false, false, true, false]),
599            &mut ctx
600        );
601    }
602
603    #[test]
604    fn test_like_regex_meta_characters_are_literal() {
605        let mut ctx = array_session().create_execution_ctx();
606        let array = VarBinViewArray::from_iter_str(["a.c", "abc", "a$c"]).into_array();
607        let result = run_like(
608            array,
609            ConstantArray::new("a.%", 3).into_array(),
610            LikeOptions::default(),
611        );
612        assert_arrays_eq!(result, BoolArray::from_iter([true, false, false]), &mut ctx);
613    }
614
615    #[test]
616    fn test_like_unicode() {
617        let mut ctx = array_session().create_execution_ctx();
618        let array =
619            VarBinViewArray::from_iter_str(["h\u{00a3}llo", "hxllo", "h\u{00a3}xllo"]).into_array();
620        // `_` matches exactly one character of any byte width.
621        let result = run_like(
622            array,
623            ConstantArray::new("h_llo", 3).into_array(),
624            LikeOptions::default(),
625        );
626        assert_arrays_eq!(result, BoolArray::from_iter([true, true, false]), &mut ctx);
627    }
628
629    #[test]
630    fn test_nlike() {
631        let mut ctx = array_session().create_execution_ctx();
632        let array = VarBinViewArray::from_iter_str(["hello", "world"]).into_array();
633        let result = run_like(
634            array,
635            ConstantArray::new("he%", 2).into_array(),
636            LikeOptions {
637                negated: true,
638                case_insensitive: false,
639            },
640        );
641        assert_arrays_eq!(result, BoolArray::from_iter([false, true]), &mut ctx);
642    }
643
644    #[test]
645    fn test_ilike() {
646        let mut ctx = array_session().create_execution_ctx();
647        let ilike = LikeOptions {
648            negated: false,
649            case_insensitive: true,
650        };
651
652        // Pure-ASCII data takes the ASCII fast paths.
653        let array = VarBinViewArray::from_iter_str(["HELLO", "world", "Help"]).into_array();
654        let result = run_like(array, ConstantArray::new("he%", 3).into_array(), ilike);
655        assert_arrays_eq!(result, BoolArray::from_iter([true, false, true]), &mut ctx);
656
657        // Non-ASCII data requires full case folding: `k` matches U+212A KELVIN SIGN.
658        let array = VarBinViewArray::from_iter_str(["\u{212a}", "k", "x"]).into_array();
659        let result = run_like(array, ConstantArray::new("k", 3).into_array(), ilike);
660        assert_arrays_eq!(result, BoolArray::from_iter([true, true, false]), &mut ctx);
661    }
662
663    #[test]
664    fn test_nilike() {
665        let mut ctx = array_session().create_execution_ctx();
666        let array = VarBinViewArray::from_iter_str(["HELLO", "world"]).into_array();
667        let result = run_like(
668            array,
669            ConstantArray::new("he%", 2).into_array(),
670            LikeOptions {
671                negated: true,
672                case_insensitive: true,
673            },
674        );
675        assert_arrays_eq!(result, BoolArray::from_iter([false, true]), &mut ctx);
676    }
677
678    #[test]
679    fn test_like_nullable_input() {
680        let mut ctx = array_session().create_execution_ctx();
681        let array = VarBinViewArray::from_iter_nullable_str([Some("hello"), None, Some("help")])
682            .into_array();
683        let result = run_like(
684            array,
685            ConstantArray::new("he%", 3).into_array(),
686            LikeOptions::default(),
687        );
688        assert_arrays_eq!(
689            result,
690            BoolArray::from_iter([Some(true), None, Some(true)]),
691            &mut ctx
692        );
693    }
694
695    #[test]
696    fn test_like_null_pattern() {
697        let mut ctx = array_session().create_execution_ctx();
698        let array = VarBinViewArray::from_iter_str(["hello", "world"]).into_array();
699        let result = run_like(
700            array,
701            ConstantArray::new(Scalar::null(DType::Utf8(Nullability::Nullable)), 2).into_array(),
702            LikeOptions::default(),
703        );
704        assert_arrays_eq!(result, BoolArray::from_iter([None, None]), &mut ctx);
705    }
706
707    #[test]
708    fn test_like_per_row_patterns() {
709        let mut ctx = array_session().create_execution_ctx();
710        let array = VarBinViewArray::from_iter_str(["hello", "hello", "hello"]).into_array();
711        let patterns = VarBinViewArray::from_iter_str(["he%", "%world", "h_llo"]).into_array();
712        let result = run_like(array, patterns, LikeOptions::default());
713        assert_arrays_eq!(result, BoolArray::from_iter([true, false, true]), &mut ctx);
714    }
715
716    #[test]
717    fn test_like_non_canonical_input() {
718        let mut ctx = array_session().create_execution_ctx();
719        // VarBin (not the canonical VarBinView) is canonicalized before evaluation.
720        let array = VarBinArray::from_iter(
721            [Some("hello"), Some("world")],
722            DType::Utf8(Nullability::Nullable),
723        )
724        .into_array();
725        let result = run_like(
726            array,
727            ConstantArray::new("he%", 2).into_array(),
728            LikeOptions::default(),
729        );
730        assert_arrays_eq!(
731            result,
732            BoolArray::from_iter([Some(true), Some(false)]),
733            &mut ctx
734        );
735    }
736
737    #[test]
738    fn invert_booleans() {
739        let not_expr = not(root());
740        let bools = BoolArray::from_iter([false, true, false, false, true, true]);
741        let mut ctx = array_session().create_execution_ctx();
742        assert_arrays_eq!(
743            bools.into_array().apply(&not_expr).unwrap(),
744            BoolArray::from_iter([true, false, true, true, false, false]),
745            &mut ctx
746        );
747    }
748
749    #[test]
750    fn dtype() {
751        let dtype = DType::Utf8(Nullability::NonNullable);
752        let like_expr = like(root(), lit("%test%"));
753        assert_eq!(
754            like_expr.return_dtype(&dtype).unwrap(),
755            DType::Bool(Nullability::NonNullable)
756        );
757    }
758
759    #[test]
760    fn signature() {
761        let like_expr = like(root(), lit("%test%"));
762        assert!(
763            like_expr
764                .as_scalar()
765                .is_some_and(|f| f.signature().is_strict())
766        );
767        assert!(
768            like_expr
769                .as_scalar()
770                .is_some_and(|f| f.signature().is_infallible())
771        );
772    }
773
774    #[test]
775    fn test_display() {
776        let expr = like(get_item("name", root()), lit("%john%"));
777        assert_eq!(expr.to_string(), "$.name like \"%john%\"");
778
779        let expr2 = not_ilike(root(), lit("test*"));
780        assert_eq!(expr2.to_string(), "$ not ilike \"test*\"");
781    }
782
783    fn assert_borrowed_exact(pattern: &str, expected: &str) {
784        let Some(LikeVariant::Exact(actual)) = LikeVariant::from_str(pattern) else {
785            panic!("expected borrowed exact pattern");
786        };
787        assert!(matches!(actual, Cow::Borrowed(_)));
788        assert_eq!(actual.as_ref(), expected);
789    }
790
791    fn assert_owned_exact(pattern: &str, expected: &str) {
792        let Some(LikeVariant::Exact(actual)) = LikeVariant::from_str(pattern) else {
793            panic!("expected owned exact pattern");
794        };
795        assert!(matches!(actual, Cow::Owned(_)));
796        assert_eq!(actual.as_ref(), expected);
797    }
798
799    fn assert_borrowed_prefix(pattern: &str, expected: &str) {
800        let Some(LikeVariant::Prefix(actual)) = LikeVariant::from_str(pattern) else {
801            panic!("expected borrowed prefix pattern");
802        };
803        assert!(matches!(actual, Cow::Borrowed(_)));
804        assert_eq!(actual.as_ref(), expected);
805    }
806
807    fn assert_owned_prefix(pattern: &str, expected: &str) {
808        let Some(LikeVariant::Prefix(actual)) = LikeVariant::from_str(pattern) else {
809            panic!("expected owned prefix pattern");
810        };
811        assert!(matches!(actual, Cow::Owned(_)));
812        assert_eq!(actual.as_ref(), expected);
813    }
814
815    #[test]
816    fn test_like_variant_borrowed_patterns() {
817        assert_borrowed_exact("simple", "simple");
818        assert_borrowed_prefix("prefix%", "prefix");
819        assert_borrowed_prefix("first%rest_stuff", "first");
820    }
821
822    #[test]
823    fn test_like_variant_escaped_patterns() {
824        assert_owned_prefix(r"\%%", "%");
825        assert_owned_prefix(r"\_%", "_");
826        assert_owned_prefix(r"\\%", "\\");
827        assert_owned_exact(r"\%", "%");
828        assert_owned_exact("trailing\\", "trailing\\");
829    }
830
831    #[test]
832    fn test_like_variant_unsupported_patterns() {
833        assert_eq!(LikeVariant::from_str("%suffix"), None);
834        assert_eq!(LikeVariant::from_str(r"%\%%"), None);
835        assert_eq!(LikeVariant::from_str("_pattern"), None);
836    }
837}