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::scalar::Scalar;
35use crate::scalar_fn::Arity;
36use crate::scalar_fn::ChildName;
37use crate::scalar_fn::ExecutionArgs;
38use crate::scalar_fn::ScalarFnId;
39use crate::scalar_fn::ScalarFnVTable;
40
41/// Options for SQL LIKE function
42#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub struct LikeOptions {
44    pub negated: bool,
45    pub case_insensitive: bool,
46}
47
48impl Display for LikeOptions {
49    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
50        if self.negated {
51            write!(f, "NOT ")?;
52        }
53        if self.case_insensitive {
54            write!(f, "ILIKE")
55        } else {
56            write!(f, "LIKE")
57        }
58    }
59}
60
61/// Expression that performs SQL LIKE pattern matching.
62#[derive(Clone)]
63pub struct Like;
64
65impl ScalarFnVTable for Like {
66    type Options = LikeOptions;
67
68    fn id(&self) -> ScalarFnId {
69        static ID: CachedId = CachedId::new("vortex.like");
70        *ID
71    }
72
73    fn serialize(&self, instance: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
74        Ok(Some(
75            pb::LikeOpts {
76                negated: instance.negated,
77                case_insensitive: instance.case_insensitive,
78            }
79            .encode_to_vec(),
80        ))
81    }
82
83    fn deserialize(
84        &self,
85        _metadata: &[u8],
86        _session: &VortexSession,
87    ) -> VortexResult<Self::Options> {
88        let opts = pb::LikeOpts::decode(_metadata)?;
89        Ok(LikeOptions {
90            negated: opts.negated,
91            case_insensitive: opts.case_insensitive,
92        })
93    }
94
95    fn arity(&self, _options: &Self::Options) -> Arity {
96        Arity::Exact(2)
97    }
98
99    fn child_name(&self, _instance: &Self::Options, child_idx: usize) -> ChildName {
100        match child_idx {
101            0 => ChildName::from("child"),
102            1 => ChildName::from("pattern"),
103            _ => unreachable!("Invalid child index {} for Like expression", child_idx),
104        }
105    }
106
107    fn fmt_sql(
108        &self,
109        options: &Self::Options,
110        expr: &Expression,
111        f: &mut Formatter<'_>,
112    ) -> std::fmt::Result {
113        expr.child(0).fmt_sql(f)?;
114        if options.negated {
115            write!(f, " not")?;
116        }
117        if options.case_insensitive {
118            write!(f, " ilike ")?;
119        } else {
120            write!(f, " like ")?;
121        }
122        expr.child(1).fmt_sql(f)
123    }
124
125    fn return_dtype(&self, _options: &Self::Options, arg_dtypes: &[DType]) -> VortexResult<DType> {
126        let input = &arg_dtypes[0];
127        let pattern = &arg_dtypes[1];
128
129        if !input.is_utf8() {
130            vortex_bail!("LIKE expression requires UTF8 input dtype, got {}", input);
131        }
132        if !pattern.is_utf8() {
133            vortex_bail!(
134                "LIKE expression requires UTF8 pattern dtype, got {}",
135                pattern
136            );
137        }
138
139        Ok(DType::Bool(
140            (input.is_nullable() || pattern.is_nullable()).into(),
141        ))
142    }
143
144    fn execute(
145        &self,
146        options: &Self::Options,
147        args: &dyn ExecutionArgs,
148        ctx: &mut ExecutionCtx,
149    ) -> VortexResult<ArrayRef> {
150        let child = args.get(0)?;
151        let pattern = args.get(1)?;
152
153        execute_like(&child, &pattern, *options, ctx)
154    }
155
156    fn validity(
157        &self,
158        _options: &Self::Options,
159        expression: &Expression,
160    ) -> VortexResult<Option<Expression>> {
161        tracing::warn!("Computing validity for LIKE expression");
162        let child_validity = expression.child(0).validity()?;
163        let pattern_validity = expression.child(1).validity()?;
164        Ok(Some(and(child_validity, pattern_validity)))
165    }
166
167    fn is_null_sensitive(&self, _instance: &Self::Options) -> bool {
168        false
169    }
170
171    fn is_fallible(&self, _options: &Self::Options) -> bool {
172        false
173    }
174}
175
176/// Native implementation of LIKE over canonical [`VarBinViewArray`]s.
177///
178/// Patterns are compiled once per distinct pattern (see [`LikePattern`]) and evaluated per
179/// value directly on the view bytes.
180pub(crate) fn execute_like(
181    array: &ArrayRef,
182    pattern: &ArrayRef,
183    options: LikeOptions,
184    ctx: &mut ExecutionCtx,
185) -> VortexResult<ArrayRef> {
186    assert_eq!(
187        array.len(),
188        pattern.len(),
189        "LIKE: length mismatch for {}",
190        array.encoding_id()
191    );
192    let len = array.len();
193    let nullability =
194        Nullability::from(array.dtype().is_nullable() || pattern.dtype().is_nullable());
195
196    if len == 0 {
197        return Ok(Canonical::empty(&DType::Bool(nullability)).into_array());
198    }
199
200    if let Some(pattern_const) = pattern.as_constant() {
201        let Some(pattern_str) = pattern_const.as_utf8().value() else {
202            // A null pattern makes every row null.
203            return Ok(
204                ConstantArray::new(Scalar::null(DType::Bool(Nullability::Nullable)), len)
205                    .into_array(),
206            );
207        };
208        let values = array.clone().execute::<VarBinViewArray>(ctx)?;
209        let haystack = ResolvedViews::new(&values);
210        // The ASCII case-insensitive fast paths are only sound when the haystack is pure
211        // ASCII; see `LikePattern::compile`.
212        let ascii_haystack =
213            options.case_insensitive && pattern_str.as_str().is_ascii() && haystack.is_ascii();
214        let compiled = LikePattern::compile(
215            pattern_str.as_str(),
216            options.case_insensitive,
217            ascii_haystack,
218        )?;
219        let bits = eval_pattern(&haystack, &compiled, options.negated);
220        let validity = values.validity()?.union_nullability(nullability);
221        return Ok(BoolArray::new(bits, validity).into_array());
222    }
223
224    // Per-row patterns: compile each pattern individually.
225    let values = array.clone().execute::<VarBinViewArray>(ctx)?;
226    let patterns = pattern.clone().execute::<VarBinViewArray>(ctx)?;
227    let haystack = ResolvedViews::new(&values);
228    let pattern_views = ResolvedViews::new(&patterns);
229    let ascii_haystack = options.case_insensitive && haystack.is_ascii();
230
231    let mut bits = Vec::with_capacity(len);
232    // Reuse the previous row's compiled pattern while the pattern bytes repeat, so runs of
233    // identical patterns (the common case for non-constant pattern children) compile once.
234    let mut cached: Option<(&[u8], LikePattern)> = None;
235    for i in 0..len {
236        let pattern_bytes = pattern_views.bytes(i);
237        let compiled = match &cached {
238            Some((bytes, compiled)) if *bytes == pattern_bytes => compiled,
239            _ => {
240                let pattern_str = std::str::from_utf8(pattern_bytes)
241                    .map_err(|e| vortex_err!("LIKE pattern is not valid UTF-8: {e}"))?;
242                let compiled = LikePattern::compile(
243                    pattern_str,
244                    options.case_insensitive,
245                    ascii_haystack && pattern_str.is_ascii(),
246                )?;
247                &cached.insert((pattern_bytes, compiled)).1
248            }
249        };
250        bits.push(compiled.matches(haystack.bytes(i)) != options.negated);
251    }
252    let validity = values
253        .validity()?
254        .and(patterns.validity()?)?
255        .union_nullability(nullability);
256    Ok(BoolArray::new(BitBuffer::from_iter(bits), validity).into_array())
257}
258
259/// Resolved views over a canonical [`VarBinViewArray`]: the view structs plus borrowed slices
260/// of every data buffer, supporting cheap per-element byte access.
261struct ResolvedViews<'a> {
262    views: &'a [BinaryView],
263    buffers: Vec<&'a [u8]>,
264}
265
266impl<'a> ResolvedViews<'a> {
267    fn new(array: &'a VarBinViewArray) -> Self {
268        Self {
269            views: array.views(),
270            buffers: (0..array.data_buffers().len())
271                .map(|idx| array.buffer(idx).as_slice())
272                .collect(),
273        }
274    }
275
276    #[inline]
277    fn bytes(&self, index: usize) -> &'a [u8] {
278        let view = &self.views[index];
279        if view.is_inlined() {
280            view.as_inlined().value()
281        } else {
282            let view = view.as_view();
283            &self.buffers[view.buffer_index as usize][view.as_range()]
284        }
285    }
286
287    /// Whether every value (including values under a null) is pure ASCII.
288    fn is_ascii(&self) -> bool {
289        (0..self.views.len()).all(|i| self.bytes(i).is_ascii())
290    }
291
292    /// The last `suffix_len` bytes of `view`, which must belong to this array.
293    ///
294    /// # Safety
295    ///
296    /// `suffix_len` must be at most `view.len()`. Buffer bounds are guaranteed by
297    /// [`VarBinViewArray::validate`], which checks every view against its data buffer at
298    /// construction.
299    #[inline]
300    unsafe fn suffix_bytes_unchecked(&self, view: &'a BinaryView, suffix_len: usize) -> &'a [u8] {
301        let len = view.len() as usize;
302        if view.is_inlined() {
303            // SAFETY: inlined values hold `len <= 12` value bytes, and the caller
304            // guarantees `suffix_len <= len`.
305            unsafe { view.as_inlined().value().get_unchecked(len - suffix_len..) }
306        } else {
307            let view = view.as_view();
308            let end = view.offset as usize + len;
309            // SAFETY: validated views reference `buffer_index < buffers.len()` and bytes
310            // `offset..offset + len` within that buffer.
311            unsafe {
312                self.buffers
313                    .get_unchecked(view.buffer_index as usize)
314                    .get_unchecked(end - suffix_len..end)
315            }
316        }
317    }
318}
319
320/// Evaluate `pattern` against every element of `haystack`.
321///
322/// The equality, prefix, and suffix patterns exploit the view layout: a view stores the value
323/// length and its first four bytes inline, which settles most elements without touching the
324/// data buffers (values of up to 12 bytes are stored entirely inline).
325fn eval_pattern(haystack: &ResolvedViews<'_>, pattern: &LikePattern, negated: bool) -> BitBuffer {
326    let len = haystack.views.len();
327    match pattern {
328        LikePattern::Eq(needle) if needle.len() <= BinaryView::MAX_INLINED_SIZE => {
329            // The needle fits in a view, so equality is a single 16-byte comparison: a view
330            // of a different length or prefix can never share the same bit pattern.
331            let needle_view = BinaryView::new_inlined(needle).as_u128();
332            BitBuffer::collect_bool(len, |i| {
333                (haystack.views[i].as_u128() == needle_view) != negated
334            })
335        }
336        LikePattern::Eq(needle) => {
337            // Compare the view head (length plus 4-byte prefix) first; only views that agree
338            // on both dereference their data buffer for the remaining bytes.
339            let needle_head = needle_head(needle);
340            BitBuffer::collect_bool(len, |i| {
341                let view = &haystack.views[i];
342                let matched =
343                    view_head(view) == needle_head && haystack.bytes(i)[4..] == needle[4..];
344                matched != negated
345            })
346        }
347        LikePattern::StartsWith(needle) => {
348            // A branch-free masked comparison of the view's inline 4-byte prefix rejects
349            // almost every element without touching the data buffers; only elements whose
350            // prefix matches compare the remaining needle bytes.
351            let needle_len = needle.len();
352            let prefix_len = needle_len.min(4);
353            let needle_prefix = u32::from_le_bytes({
354                let mut padded = [0u8; 4];
355                padded[..prefix_len].copy_from_slice(&needle[..prefix_len]);
356                padded
357            });
358            let prefix_mask = if prefix_len == 4 {
359                u32::MAX
360            } else {
361                (1u32 << (8 * prefix_len)) - 1
362            };
363            BitBuffer::collect_bool(len, |i| {
364                let view = &haystack.views[i];
365                let matched = view.len() as usize >= needle_len
366                    && (view_prefix(view) & prefix_mask) == needle_prefix
367                    && (needle_len <= 4 || haystack.bytes(i)[4..needle_len] == needle[4..]);
368                matched != negated
369            })
370        }
371        LikePattern::EndsWith(needle) => {
372            // Inlined values compare their suffix inside the view struct without touching the
373            // data buffers; reference views slice exactly the suffix out of their buffer.
374            let needle_len = needle.len();
375            BitBuffer::collect_bool(len, |i| {
376                // SAFETY: `i` is below the array length, and the suffix length is only read
377                // once the view is known to be at least `needle_len` long.
378                let matched = unsafe {
379                    let view = haystack.views.get_unchecked(i);
380                    view.len() as usize >= needle_len
381                        && bytes_eq(haystack.suffix_bytes_unchecked(view, needle_len), needle)
382                };
383                matched != negated
384            })
385        }
386        LikePattern::IEqAscii(needle) => BitBuffer::collect_bool(len, |i| {
387            let view = &haystack.views[i];
388            let matched = view.len() as usize == needle.len()
389                && haystack.bytes(i).eq_ignore_ascii_case(needle);
390            matched != negated
391        }),
392        LikePattern::Contains(finder, needle_len) => BitBuffer::collect_bool(len, |i| {
393            let view = &haystack.views[i];
394            let matched =
395                view.len() as usize >= *needle_len && finder.find(haystack.bytes(i)).is_some();
396            matched != negated
397        }),
398        _ => BitBuffer::collect_bool(len, |i| pattern.matches(haystack.bytes(i)) != negated),
399    }
400}
401
402/// Byte equality as an inlined loop with early exit.
403///
404/// Faster than slice `==` for the short, unpredictable-length comparisons in this module,
405/// which otherwise lower to a `memcmp` libcall per element.
406#[inline]
407fn bytes_eq(lhs: &[u8], rhs: &[u8]) -> bool {
408    lhs.len() == rhs.len() && std::iter::zip(lhs, rhs).all(|(l, r)| l == r)
409}
410
411/// The leading 8 bytes of a view: the `u32` length plus the first 4 bytes of the value
412/// (zero-padded for values shorter than 4 bytes).
413#[inline]
414#[expect(clippy::cast_possible_truncation, reason = "intentional bit slicing")]
415fn view_head(view: &BinaryView) -> u64 {
416    view.as_u128() as u64
417}
418
419/// The view head a needle of more than 4 bytes would have: its length plus first 4 bytes.
420fn needle_head(needle: &[u8]) -> u64 {
421    let prefix: [u8; 4] = [needle[0], needle[1], needle[2], needle[3]];
422    (needle.len() as u64) | (u64::from(u32::from_le_bytes(prefix)) << 32)
423}
424
425/// The first 4 value bytes stored inline in any view (zero-padded for values shorter than
426/// 4 bytes), as a raw little-endian `u32` in memory order.
427#[inline]
428#[expect(clippy::cast_possible_truncation, reason = "intentional bit slicing")]
429fn view_prefix(view: &BinaryView) -> u32 {
430    (view.as_u128() >> 32) as u32
431}
432
433/// Variants of the LIKE filter that we know how to turn into a stats pruning predicate.
434#[derive(Debug, PartialEq)]
435pub(crate) enum LikeVariant<'a> {
436    Exact(Cow<'a, str>),
437    Prefix(Cow<'a, str>),
438}
439
440impl<'a> LikeVariant<'a> {
441    /// Parse a LIKE pattern string into its relevant variant
442    pub(crate) fn from_str(string: &'a str) -> Option<LikeVariant<'a>> {
443        let mut literal = None;
444        let mut chars = string.char_indices();
445
446        while let Some((idx, c)) = chars.next() {
447            match c {
448                '\\' => {
449                    let literal = literal.get_or_insert_with(|| string[..idx].to_string());
450                    match chars.next() {
451                        Some((_, escaped)) => literal.push(escaped),
452                        None => literal.push('\\'),
453                    }
454                }
455                '%' | '_' => {
456                    return match literal {
457                        Some(literal) => (!literal.is_empty())
458                            .then_some(LikeVariant::Prefix(Cow::Owned(literal))),
459                        None => {
460                            (idx != 0).then_some(LikeVariant::Prefix(Cow::Borrowed(&string[..idx])))
461                        }
462                    };
463                }
464                c => {
465                    if let Some(literal) = &mut literal {
466                        literal.push(c);
467                    }
468                }
469            }
470        }
471
472        Some(match literal {
473            Some(literal) => LikeVariant::Exact(Cow::Owned(literal)),
474            None => LikeVariant::Exact(Cow::Borrowed(string)),
475        })
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use std::borrow::Cow;
482
483    use rstest::rstest;
484
485    use crate::IntoArray;
486    use crate::VortexSessionExecute;
487    use crate::array_session;
488    use crate::arrays::BoolArray;
489    use crate::arrays::ConstantArray;
490    use crate::arrays::VarBinArray;
491    use crate::arrays::VarBinViewArray;
492    use crate::arrays::scalar_fn::ScalarFnFactoryExt;
493    use crate::assert_arrays_eq;
494    use crate::dtype::DType;
495    use crate::dtype::Nullability;
496    use crate::expr::get_item;
497    use crate::expr::like;
498    use crate::expr::lit;
499    use crate::expr::not;
500    use crate::expr::not_ilike;
501    use crate::expr::root;
502    use crate::scalar::Scalar;
503    use crate::scalar_fn::fns::like::Like;
504    use crate::scalar_fn::fns::like::LikeOptions;
505    use crate::scalar_fn::fns::like::LikeVariant;
506
507    fn run_like(
508        array: crate::ArrayRef,
509        pattern: crate::ArrayRef,
510        options: LikeOptions,
511    ) -> crate::ArrayRef {
512        let len = array.len();
513        Like.try_new_array(len, options, [array, pattern]).unwrap()
514    }
515
516    #[rstest]
517    // Exact, prefix, suffix, contains fast paths.
518    #[case("hello", [true, false, false, false])]
519    #[case("he%", [true, false, true, false])]
520    #[case("%llo", [true, false, false, true])]
521    #[case("%ell%", [true, false, false, true])]
522    // Wildcards that require the regex path.
523    #[case("h_llo", [true, false, false, false])]
524    #[case("h%o", [true, false, false, false])]
525    #[case("%", [true, true, true, true])]
526    #[case("_____", [true, true, false, true])]
527    fn test_like_patterns(#[case] pattern: &str, #[case] expected: [bool; 4]) {
528        let mut ctx = array_session().create_execution_ctx();
529        let array =
530            VarBinViewArray::from_iter_str(["hello", "world", "help", "jello"]).into_array();
531        let result = run_like(
532            array,
533            ConstantArray::new(pattern, 4).into_array(),
534            LikeOptions::default(),
535        );
536        assert_arrays_eq!(result, BoolArray::from_iter(expected), &mut ctx);
537    }
538
539    #[test]
540    fn test_like_escapes() {
541        let mut ctx = array_session().create_execution_ctx();
542        let array = VarBinViewArray::from_iter_str(["100%", "100x", "a_b", "axb"]).into_array();
543
544        let result = run_like(
545            array.clone(),
546            ConstantArray::new(r"100\%", 4).into_array(),
547            LikeOptions::default(),
548        );
549        assert_arrays_eq!(
550            result,
551            BoolArray::from_iter([true, false, false, false]),
552            &mut ctx
553        );
554
555        let result = run_like(
556            array,
557            ConstantArray::new(r"a\_b", 4).into_array(),
558            LikeOptions::default(),
559        );
560        assert_arrays_eq!(
561            result,
562            BoolArray::from_iter([false, false, true, false]),
563            &mut ctx
564        );
565    }
566
567    #[test]
568    fn test_like_regex_meta_characters_are_literal() {
569        let mut ctx = array_session().create_execution_ctx();
570        let array = VarBinViewArray::from_iter_str(["a.c", "abc", "a$c"]).into_array();
571        let result = run_like(
572            array,
573            ConstantArray::new("a.%", 3).into_array(),
574            LikeOptions::default(),
575        );
576        assert_arrays_eq!(result, BoolArray::from_iter([true, false, false]), &mut ctx);
577    }
578
579    #[test]
580    fn test_like_unicode() {
581        let mut ctx = array_session().create_execution_ctx();
582        let array =
583            VarBinViewArray::from_iter_str(["h\u{00a3}llo", "hxllo", "h\u{00a3}xllo"]).into_array();
584        // `_` matches exactly one character of any byte width.
585        let result = run_like(
586            array,
587            ConstantArray::new("h_llo", 3).into_array(),
588            LikeOptions::default(),
589        );
590        assert_arrays_eq!(result, BoolArray::from_iter([true, true, false]), &mut ctx);
591    }
592
593    #[test]
594    fn test_nlike() {
595        let mut ctx = array_session().create_execution_ctx();
596        let array = VarBinViewArray::from_iter_str(["hello", "world"]).into_array();
597        let result = run_like(
598            array,
599            ConstantArray::new("he%", 2).into_array(),
600            LikeOptions {
601                negated: true,
602                case_insensitive: false,
603            },
604        );
605        assert_arrays_eq!(result, BoolArray::from_iter([false, true]), &mut ctx);
606    }
607
608    #[test]
609    fn test_ilike() {
610        let mut ctx = array_session().create_execution_ctx();
611        let ilike = LikeOptions {
612            negated: false,
613            case_insensitive: true,
614        };
615
616        // Pure-ASCII data takes the ASCII fast paths.
617        let array = VarBinViewArray::from_iter_str(["HELLO", "world", "Help"]).into_array();
618        let result = run_like(array, ConstantArray::new("he%", 3).into_array(), ilike);
619        assert_arrays_eq!(result, BoolArray::from_iter([true, false, true]), &mut ctx);
620
621        // Non-ASCII data requires full case folding: `k` matches U+212A KELVIN SIGN.
622        let array = VarBinViewArray::from_iter_str(["\u{212a}", "k", "x"]).into_array();
623        let result = run_like(array, ConstantArray::new("k", 3).into_array(), ilike);
624        assert_arrays_eq!(result, BoolArray::from_iter([true, true, false]), &mut ctx);
625    }
626
627    #[test]
628    fn test_nilike() {
629        let mut ctx = array_session().create_execution_ctx();
630        let array = VarBinViewArray::from_iter_str(["HELLO", "world"]).into_array();
631        let result = run_like(
632            array,
633            ConstantArray::new("he%", 2).into_array(),
634            LikeOptions {
635                negated: true,
636                case_insensitive: true,
637            },
638        );
639        assert_arrays_eq!(result, BoolArray::from_iter([false, true]), &mut ctx);
640    }
641
642    #[test]
643    fn test_like_nullable_input() {
644        let mut ctx = array_session().create_execution_ctx();
645        let array = VarBinViewArray::from_iter_nullable_str([Some("hello"), None, Some("help")])
646            .into_array();
647        let result = run_like(
648            array,
649            ConstantArray::new("he%", 3).into_array(),
650            LikeOptions::default(),
651        );
652        assert_arrays_eq!(
653            result,
654            BoolArray::from_iter([Some(true), None, Some(true)]),
655            &mut ctx
656        );
657    }
658
659    #[test]
660    fn test_like_null_pattern() {
661        let mut ctx = array_session().create_execution_ctx();
662        let array = VarBinViewArray::from_iter_str(["hello", "world"]).into_array();
663        let result = run_like(
664            array,
665            ConstantArray::new(Scalar::null(DType::Utf8(Nullability::Nullable)), 2).into_array(),
666            LikeOptions::default(),
667        );
668        assert_arrays_eq!(result, BoolArray::from_iter([None, None]), &mut ctx);
669    }
670
671    #[test]
672    fn test_like_per_row_patterns() {
673        let mut ctx = array_session().create_execution_ctx();
674        let array = VarBinViewArray::from_iter_str(["hello", "hello", "hello"]).into_array();
675        let patterns = VarBinViewArray::from_iter_str(["he%", "%world", "h_llo"]).into_array();
676        let result = run_like(array, patterns, LikeOptions::default());
677        assert_arrays_eq!(result, BoolArray::from_iter([true, false, true]), &mut ctx);
678    }
679
680    #[test]
681    fn test_like_non_canonical_input() {
682        let mut ctx = array_session().create_execution_ctx();
683        // VarBin (not the canonical VarBinView) is canonicalized before evaluation.
684        let array = VarBinArray::from_iter(
685            [Some("hello"), Some("world")],
686            DType::Utf8(Nullability::Nullable),
687        )
688        .into_array();
689        let result = run_like(
690            array,
691            ConstantArray::new("he%", 2).into_array(),
692            LikeOptions::default(),
693        );
694        assert_arrays_eq!(
695            result,
696            BoolArray::from_iter([Some(true), Some(false)]),
697            &mut ctx
698        );
699    }
700
701    #[test]
702    fn invert_booleans() {
703        let not_expr = not(root());
704        let bools = BoolArray::from_iter([false, true, false, false, true, true]);
705        let mut ctx = array_session().create_execution_ctx();
706        assert_arrays_eq!(
707            bools.into_array().apply(&not_expr).unwrap(),
708            BoolArray::from_iter([true, false, true, true, false, false]),
709            &mut ctx
710        );
711    }
712
713    #[test]
714    fn dtype() {
715        let dtype = DType::Utf8(Nullability::NonNullable);
716        let like_expr = like(root(), lit("%test%"));
717        assert_eq!(
718            like_expr.return_dtype(&dtype).unwrap(),
719            DType::Bool(Nullability::NonNullable)
720        );
721    }
722
723    #[test]
724    fn signature() {
725        let like_expr = like(root(), lit("%test%"));
726        assert!(!like_expr.signature().is_null_sensitive());
727        assert!(!like_expr.signature().is_fallible());
728    }
729
730    #[test]
731    fn test_display() {
732        let expr = like(get_item("name", root()), lit("%john%"));
733        assert_eq!(expr.to_string(), "$.name like \"%john%\"");
734
735        let expr2 = not_ilike(root(), lit("test*"));
736        assert_eq!(expr2.to_string(), "$ not ilike \"test*\"");
737    }
738
739    fn assert_borrowed_exact(pattern: &str, expected: &str) {
740        let Some(LikeVariant::Exact(actual)) = LikeVariant::from_str(pattern) else {
741            panic!("expected borrowed exact pattern");
742        };
743        assert!(matches!(actual, Cow::Borrowed(_)));
744        assert_eq!(actual.as_ref(), expected);
745    }
746
747    fn assert_owned_exact(pattern: &str, expected: &str) {
748        let Some(LikeVariant::Exact(actual)) = LikeVariant::from_str(pattern) else {
749            panic!("expected owned exact pattern");
750        };
751        assert!(matches!(actual, Cow::Owned(_)));
752        assert_eq!(actual.as_ref(), expected);
753    }
754
755    fn assert_borrowed_prefix(pattern: &str, expected: &str) {
756        let Some(LikeVariant::Prefix(actual)) = LikeVariant::from_str(pattern) else {
757            panic!("expected borrowed prefix pattern");
758        };
759        assert!(matches!(actual, Cow::Borrowed(_)));
760        assert_eq!(actual.as_ref(), expected);
761    }
762
763    fn assert_owned_prefix(pattern: &str, expected: &str) {
764        let Some(LikeVariant::Prefix(actual)) = LikeVariant::from_str(pattern) else {
765            panic!("expected owned prefix pattern");
766        };
767        assert!(matches!(actual, Cow::Owned(_)));
768        assert_eq!(actual.as_ref(), expected);
769    }
770
771    #[test]
772    fn test_like_variant_borrowed_patterns() {
773        assert_borrowed_exact("simple", "simple");
774        assert_borrowed_prefix("prefix%", "prefix");
775        assert_borrowed_prefix("first%rest_stuff", "first");
776    }
777
778    #[test]
779    fn test_like_variant_escaped_patterns() {
780        assert_owned_prefix(r"\%%", "%");
781        assert_owned_prefix(r"\_%", "_");
782        assert_owned_prefix(r"\\%", "\\");
783        assert_owned_exact(r"\%", "%");
784        assert_owned_exact("trailing\\", "trailing\\");
785    }
786
787    #[test]
788    fn test_like_variant_unsupported_patterns() {
789        assert_eq!(LikeVariant::from_str("%suffix"), None);
790        assert_eq!(LikeVariant::from_str(r"%\%%"), None);
791        assert_eq!(LikeVariant::from_str("_pattern"), None);
792    }
793}