Skip to main content

vortex_array/arrays/varbin/compute/
compare.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::cmp::Ordering;
5
6use vortex_buffer::BitBuffer;
7use vortex_error::VortexExpect as _;
8use vortex_error::VortexResult;
9use vortex_error::vortex_bail;
10
11use crate::ArrayRef;
12use crate::ExecutionCtx;
13use crate::IntoArray;
14use crate::array::ArrayView;
15use crate::arrays::BoolArray;
16use crate::arrays::PrimitiveArray;
17use crate::arrays::VarBin;
18use crate::arrays::varbin::VarBinArraySlotsExt;
19use crate::dtype::DType;
20use crate::dtype::IntegerPType;
21use crate::match_each_integer_ptype;
22use crate::scalar_fn::fns::binary::CompareKernel;
23use crate::scalar_fn::fns::operators::CompareOperator;
24
25// This implementation exists so we can compare against a constant in encoded space, without
26// canonicalizing the VarBin array to VarBinView.
27impl CompareKernel for VarBin {
28    fn compare(
29        lhs: ArrayView<'_, VarBin>,
30        rhs: &ArrayRef,
31        operator: CompareOperator,
32        ctx: &mut ExecutionCtx,
33    ) -> VortexResult<Option<ArrayRef>> {
34        let Some(rhs_const) = rhs.as_constant() else {
35            return Ok(None);
36        };
37
38        let len = lhs.len();
39
40        // The compare adaptor resolves null constants before dispatching to this kernel, so
41        // the scalar always carries a value.
42        let rhs_bytes: &[u8] = match rhs_const.dtype() {
43            DType::Binary(_) => rhs_const
44                .as_binary()
45                .value()
46                .vortex_expect("RHS should not be null")
47                .as_slice(),
48            DType::Utf8(_) => rhs_const
49                .as_utf8()
50                .value()
51                .vortex_expect("RHS should not be null")
52                .as_str()
53                .as_bytes(),
54            _ => vortex_bail!("VarBinArray can only have type of Binary or Utf8"),
55        };
56
57        let buffer = if rhs_bytes.is_empty() {
58            // Comparisons against "" only need the value lengths, i.e. the offset deltas.
59            match operator {
60                CompareOperator::Gte => BitBuffer::new_set(len), /* Every possible value is >= "" */
61                CompareOperator::Lt => BitBuffer::new_unset(len), // No value is < ""
62                CompareOperator::Eq | CompareOperator::Lte => {
63                    let lhs_offsets = lhs.offsets().clone().execute::<PrimitiveArray>(ctx)?;
64                    match_each_integer_ptype!(lhs_offsets.ptype(), |P| {
65                        compare_offsets_to_empty::<P>(lhs_offsets, true)
66                    })
67                }
68                CompareOperator::NotEq | CompareOperator::Gt => {
69                    let lhs_offsets = lhs.offsets().clone().execute::<PrimitiveArray>(ctx)?;
70                    match_each_integer_ptype!(lhs_offsets.ptype(), |P| {
71                        compare_offsets_to_empty::<P>(lhs_offsets, false)
72                    })
73                }
74            }
75        } else {
76            let lhs_offsets = lhs.offsets().clone().execute::<PrimitiveArray>(ctx)?;
77            match_each_integer_ptype!(lhs_offsets.ptype(), |P| {
78                compare_bytes_to_constant(
79                    lhs_offsets.as_slice::<P>(),
80                    lhs.bytes().as_slice(),
81                    rhs_bytes,
82                    operator,
83                )
84            })
85        };
86
87        Ok(Some(
88            BoolArray::new(
89                buffer,
90                lhs.validity()?.union_nullability(rhs.dtype().nullability()),
91            )
92            .into_array(),
93        ))
94    }
95}
96
97fn compare_offsets_to_empty<P: IntegerPType>(offsets: PrimitiveArray, eq: bool) -> BitBuffer {
98    let fn_ = if eq { P::eq } else { P::ne };
99    let offsets = offsets.as_slice::<P>();
100    BitBuffer::collect_bool(offsets.len() - 1, |idx| {
101        let left = unsafe { offsets.get_unchecked(idx) };
102        let right = unsafe { offsets.get_unchecked(idx + 1) };
103        fn_(left, right)
104    })
105}
106
107/// Compare every value in a VarBin array against a constant, resolving values through the
108/// offsets. Dispatches the operator outside the lane loop so each predicate inlines into its
109/// own loop.
110fn compare_bytes_to_constant<P: IntegerPType>(
111    offsets: &[P],
112    bytes: &[u8],
113    constant: &[u8],
114    operator: CompareOperator,
115) -> BitBuffer {
116    match operator {
117        CompareOperator::Eq => {
118            collect_lane_bits(offsets, |start, end| value_eq(bytes, start, end, constant))
119        }
120        CompareOperator::NotEq => {
121            collect_lane_bits(offsets, |start, end| !value_eq(bytes, start, end, constant))
122        }
123        CompareOperator::Gt => collect_lane_bits(offsets, |start, end| {
124            value_cmp(bytes, start, end, constant).is_gt()
125        }),
126        CompareOperator::Gte => collect_lane_bits(offsets, |start, end| {
127            value_cmp(bytes, start, end, constant).is_ge()
128        }),
129        CompareOperator::Lt => collect_lane_bits(offsets, |start, end| {
130            value_cmp(bytes, start, end, constant).is_lt()
131        }),
132        CompareOperator::Lte => collect_lane_bits(offsets, |start, end| {
133            value_cmp(bytes, start, end, constant).is_le()
134        }),
135    }
136}
137
138/// Bit-pack `predicate(offsets[i], offsets[i + 1])` over each lane of a VarBin array.
139fn collect_lane_bits<P: IntegerPType>(
140    offsets: &[P],
141    predicate: impl Fn(usize, usize) -> bool,
142) -> BitBuffer {
143    BitBuffer::collect_bool(offsets.len() - 1, |idx| {
144        // SAFETY: `collect_bool` yields idx < offsets.len() - 1.
145        let start = unsafe { offsets.get_unchecked(idx) }.as_();
146        let end = unsafe { offsets.get_unchecked(idx + 1) }.as_();
147        predicate(start, end)
148    })
149}
150
151/// Whether `bytes[start..end]` equals `constant`, comparing lengths first so lanes of a
152/// different length never touch the value bytes.
153///
154/// Offsets at null positions are not validated, so an out-of-bounds or inverted range is
155/// possible there; such lanes answer `false`, and validity masks them out of the result anyway.
156#[allow(clippy::inline_always)]
157#[inline(always)]
158fn value_eq(bytes: &[u8], start: usize, end: usize, constant: &[u8]) -> bool {
159    // A lane can only match when its length equals the constant's, so lanes of a different
160    // length answer without touching the value bytes. An inverted garbage range (start > end)
161    // wraps to a huge value that never equals `constant.len()`.
162    end.wrapping_sub(start) == constant.len()
163        && bytes.get(start..end).is_some_and(|value| value == constant)
164}
165
166/// Order `bytes[start..end]` against `constant`, treating the unvalidated garbage ranges that
167/// can appear at null positions as empty; validity masks those lanes out of the result anyway.
168#[allow(clippy::inline_always)]
169#[inline(always)]
170fn value_cmp(bytes: &[u8], start: usize, end: usize, constant: &[u8]) -> Ordering {
171    bytes.get(start..end).unwrap_or_default().cmp(constant)
172}
173
174#[cfg(test)]
175mod test {
176    use vortex_buffer::BitBuffer;
177    use vortex_buffer::ByteBuffer;
178
179    use crate::IntoArray;
180    use crate::VortexSessionExecute;
181    use crate::array_session;
182    use crate::arrays::BoolArray;
183    use crate::arrays::ConstantArray;
184    use crate::arrays::VarBinArray;
185    use crate::arrays::VarBinViewArray;
186    use crate::arrays::bool::BoolArrayExt;
187    use crate::builtins::ArrayBuiltins;
188    use crate::dtype::DType;
189    use crate::dtype::Nullability;
190    use crate::scalar::Scalar;
191    use crate::scalar_fn::fns::operators::Operator;
192
193    #[test]
194    fn test_binary_compare() {
195        let mut ctx = array_session().create_execution_ctx();
196        let array = VarBinArray::from_iter(
197            [Some(b"abc".to_vec()), None, Some(b"def".to_vec())],
198            DType::Binary(Nullability::Nullable),
199        );
200        let result = array
201            .into_array()
202            .binary(
203                ConstantArray::new(
204                    Scalar::binary(ByteBuffer::copy_from(b"abc"), Nullability::Nullable),
205                    3,
206                )
207                .into_array(),
208                Operator::Eq,
209            )
210            .unwrap()
211            .execute::<BoolArray>(&mut ctx)
212            .unwrap();
213
214        assert_eq!(
215            &result
216                .as_ref()
217                .validity()
218                .unwrap()
219                .execute_mask(result.as_ref().len(), &mut ctx)
220                .unwrap()
221                .to_bit_buffer(),
222            &BitBuffer::from_iter([true, false, true])
223        );
224        assert_eq!(
225            result.to_bit_buffer(),
226            BitBuffer::from_iter([true, false, false])
227        );
228    }
229
230    #[test]
231    fn varbinview_compare() {
232        let mut ctx = array_session().create_execution_ctx();
233        let array = VarBinArray::from_iter(
234            [Some(b"abc".to_vec()), None, Some(b"def".to_vec())],
235            DType::Binary(Nullability::Nullable),
236        );
237        let vbv = VarBinViewArray::from_iter(
238            [None, None, Some(b"def".to_vec())],
239            DType::Binary(Nullability::Nullable),
240        );
241        let result = array
242            .into_array()
243            .binary(vbv.into_array(), Operator::Eq)
244            .unwrap()
245            .execute::<BoolArray>(&mut ctx)
246            .unwrap();
247
248        assert_eq!(
249            result
250                .as_ref()
251                .validity()
252                .unwrap()
253                .execute_mask(result.as_ref().len(), &mut ctx)
254                .unwrap()
255                .to_bit_buffer(),
256            BitBuffer::from_iter([false, false, true])
257        );
258        assert_eq!(
259            result.to_bit_buffer(),
260            BitBuffer::from_iter([false, true, true])
261        );
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use vortex_buffer::ByteBuffer;
268
269    use crate::IntoArray;
270    use crate::VortexSessionExecute;
271    use crate::array_session;
272    use crate::arrays::BoolArray;
273    use crate::arrays::ConstantArray;
274    use crate::arrays::VarBinArray;
275    use crate::arrays::varbin::builder::VarBinBuilder;
276    use crate::assert_arrays_eq;
277    use crate::builtins::ArrayBuiltins;
278    use crate::dtype::DType;
279    use crate::dtype::Nullability;
280    use crate::scalar::Scalar;
281    use crate::scalar_fn::fns::operators::Operator;
282
283    #[test]
284    fn test_null_compare() {
285        let arr = VarBinArray::from_iter([Some("h")], DType::Utf8(Nullability::NonNullable));
286
287        let const_ = ConstantArray::new(Scalar::utf8("", Nullability::Nullable), 1);
288
289        assert_eq!(
290            arr.into_array()
291                .binary(const_.into_array(), Operator::Eq)
292                .unwrap()
293                .dtype(),
294            &DType::Bool(Nullability::Nullable)
295        );
296    }
297
298    /// Regression: [`CompareKernel`] must handle every offset width; a `VarBinArray` built with
299    /// `i64` offsets once failed the constant comparison. Triggering this only requires `i64`
300    /// offsets, not large data.
301    ///
302    /// [`CompareKernel`]: super::CompareKernel
303    #[test]
304    fn varbin_i64_offsets_compare_constant() {
305        let mut ctx = array_session().create_execution_ctx();
306        let mut builder = VarBinBuilder::<i64>::with_capacity_in(
307            DType::Utf8(Nullability::NonNullable),
308            3,
309            vortex_buffer::BufferAllocatorRef::static_ref(),
310        );
311        builder.append_value(b"abc");
312        builder.append_value(b"xyz");
313        builder.append_value(b"abc");
314        let array = builder.finish_into_varbin();
315
316        let result = array
317            .into_array()
318            .binary(
319                ConstantArray::new(Scalar::utf8("abc", Nullability::NonNullable), 3).into_array(),
320                Operator::Eq,
321            )
322            .unwrap();
323
324        let expected = BoolArray::from_iter([true, false, true]);
325        assert_arrays_eq!(result, expected, &mut ctx);
326    }
327
328    #[test]
329    fn varbin_i64_offsets_compare_constant_binary() {
330        let mut ctx = array_session().create_execution_ctx();
331        let mut builder = VarBinBuilder::<i64>::with_capacity_in(
332            DType::Binary(Nullability::NonNullable),
333            3,
334            vortex_buffer::BufferAllocatorRef::static_ref(),
335        );
336        builder.append_value(b"abc");
337        builder.append_value(b"xyz");
338        builder.append_value(b"abc");
339        let array = builder.finish_into_varbin();
340
341        let result = array
342            .into_array()
343            .binary(
344                ConstantArray::new(
345                    Scalar::binary(ByteBuffer::copy_from(b"abc"), Nullability::NonNullable),
346                    3,
347                )
348                .into_array(),
349                Operator::Eq,
350            )
351            .unwrap();
352
353        let expected = BoolArray::from_iter([true, false, true]);
354        assert_arrays_eq!(result, expected, &mut ctx);
355    }
356}