vortex_expr/exprs/
like.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::hash::Hash;
5
6use vortex_array::compute::{LikeOptions, like};
7use vortex_array::{ArrayRef, DeserializeMetadata, ProstMetadata};
8use vortex_dtype::DType;
9use vortex_error::{VortexResult, vortex_bail};
10use vortex_proto::expr as pb;
11
12use crate::display::{DisplayAs, DisplayFormat};
13use crate::{AnalysisExpr, ExprEncodingRef, ExprId, ExprRef, IntoExpr, Scope, VTable, vtable};
14
15vtable!(Like);
16
17#[allow(clippy::derived_hash_with_manual_eq)]
18#[derive(Clone, Debug, Hash, Eq)]
19pub struct LikeExpr {
20    child: ExprRef,
21    pattern: ExprRef,
22    negated: bool,
23    case_insensitive: bool,
24}
25
26impl PartialEq for LikeExpr {
27    fn eq(&self, other: &Self) -> bool {
28        self.child.eq(&other.child)
29            && self.pattern.eq(&other.pattern)
30            && self.negated == other.negated
31            && self.case_insensitive == other.case_insensitive
32    }
33}
34
35pub struct LikeExprEncoding;
36
37impl VTable for LikeVTable {
38    type Expr = LikeExpr;
39    type Encoding = LikeExprEncoding;
40    type Metadata = ProstMetadata<pb::LikeOpts>;
41
42    fn id(_encoding: &Self::Encoding) -> ExprId {
43        ExprId::new_ref("like")
44    }
45
46    fn encoding(_expr: &Self::Expr) -> ExprEncodingRef {
47        ExprEncodingRef::new_ref(LikeExprEncoding.as_ref())
48    }
49
50    fn metadata(expr: &Self::Expr) -> Option<Self::Metadata> {
51        Some(ProstMetadata(pb::LikeOpts {
52            negated: expr.negated,
53            case_insensitive: expr.case_insensitive,
54        }))
55    }
56
57    fn children(expr: &Self::Expr) -> Vec<&ExprRef> {
58        vec![&expr.child, &expr.pattern]
59    }
60
61    fn with_children(expr: &Self::Expr, children: Vec<ExprRef>) -> VortexResult<Self::Expr> {
62        Ok(LikeExpr::new(
63            children[0].clone(),
64            children[1].clone(),
65            expr.negated,
66            expr.case_insensitive,
67        ))
68    }
69
70    fn build(
71        _encoding: &Self::Encoding,
72        metadata: &<Self::Metadata as DeserializeMetadata>::Output,
73        children: Vec<ExprRef>,
74    ) -> VortexResult<Self::Expr> {
75        if children.len() != 2 {
76            vortex_bail!(
77                "Like expression must have exactly 2 children, got {}",
78                children.len()
79            );
80        }
81
82        Ok(LikeExpr::new(
83            children[0].clone(),
84            children[1].clone(),
85            metadata.negated,
86            metadata.case_insensitive,
87        ))
88    }
89
90    fn evaluate(expr: &Self::Expr, scope: &Scope) -> VortexResult<ArrayRef> {
91        let child = expr.child().unchecked_evaluate(scope)?;
92        let pattern = expr.pattern().unchecked_evaluate(scope)?;
93        like(
94            &child,
95            &pattern,
96            LikeOptions {
97                negated: expr.negated,
98                case_insensitive: expr.case_insensitive,
99            },
100        )
101    }
102
103    fn return_dtype(expr: &Self::Expr, scope: &DType) -> VortexResult<DType> {
104        let input = expr.child().return_dtype(scope)?;
105        let pattern = expr.pattern().return_dtype(scope)?;
106        Ok(DType::Bool(
107            (input.is_nullable() || pattern.is_nullable()).into(),
108        ))
109    }
110}
111
112impl LikeExpr {
113    pub fn new(child: ExprRef, pattern: ExprRef, negated: bool, case_insensitive: bool) -> Self {
114        Self {
115            child,
116            pattern,
117            negated,
118            case_insensitive,
119        }
120    }
121
122    pub fn new_expr(
123        child: ExprRef,
124        pattern: ExprRef,
125        negated: bool,
126        case_insensitive: bool,
127    ) -> ExprRef {
128        Self::new(child, pattern, negated, case_insensitive).into_expr()
129    }
130
131    pub fn child(&self) -> &ExprRef {
132        &self.child
133    }
134
135    pub fn pattern(&self) -> &ExprRef {
136        &self.pattern
137    }
138
139    pub fn negated(&self) -> bool {
140        self.negated
141    }
142
143    pub fn case_insensitive(&self) -> bool {
144        self.case_insensitive
145    }
146}
147
148impl DisplayAs for LikeExpr {
149    fn fmt_as(&self, df: DisplayFormat, f: &mut std::fmt::Formatter) -> std::fmt::Result {
150        match df {
151            DisplayFormat::Compact => {
152                write!(f, "{} LIKE {}", self.child(), self.pattern())
153            }
154            DisplayFormat::Tree => {
155                write!(f, "Like")
156            }
157        }
158    }
159
160    fn child_names(&self) -> Option<Vec<String>> {
161        Some(vec!["child".to_string(), "pattern".to_string()])
162    }
163}
164
165impl AnalysisExpr for LikeExpr {}
166
167#[cfg(test)]
168mod tests {
169    use vortex_array::ToCanonical;
170    use vortex_array::arrays::BoolArray;
171    use vortex_dtype::{DType, Nullability};
172
173    use crate::{LikeExpr, Scope, get_item, lit, not, root};
174
175    #[test]
176    fn invert_booleans() {
177        let not_expr = not(root());
178        let bools = BoolArray::from_iter([false, true, false, false, true, true]);
179        assert_eq!(
180            not_expr
181                .evaluate(&Scope::new(bools.to_array()))
182                .unwrap()
183                .to_bool()
184                .boolean_buffer()
185                .iter()
186                .collect::<Vec<_>>(),
187            vec![true, false, true, true, false, false]
188        );
189    }
190
191    #[test]
192    fn dtype() {
193        let dtype = DType::Utf8(Nullability::NonNullable);
194        let like_expr = LikeExpr::new(root(), lit("%test%"), false, false);
195        assert_eq!(
196            like_expr.return_dtype(&dtype).unwrap(),
197            DType::Bool(Nullability::NonNullable)
198        );
199    }
200
201    #[test]
202    fn test_display() {
203        let expr = LikeExpr::new(get_item("name", root()), lit("%john%"), false, false);
204        assert_eq!(expr.to_string(), "$.name LIKE \"%john%\"");
205
206        let expr2 = LikeExpr::new(root(), lit("test*"), true, true);
207        assert_eq!(expr2.to_string(), "$ LIKE \"test*\"");
208    }
209}