• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vortex-data / vortex / 16593958537

29 Jul 2025 10:48AM UTC coverage: 82.285% (+0.5%) from 81.796%
16593958537

Pull #4036

github

web-flow
Merge 04147cb0f into 348079fc3
Pull Request #4036: varbinview builder buffer deduplication

146 of 154 new or added lines in 2 files covered. (94.81%)

348 existing lines in 26 files now uncovered.

44470 of 54044 relevant lines covered (82.28%)

169522.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.11
/vortex-array/src/compute/between.rs
1
// SPDX-License-Identifier: Apache-2.0
2
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3

4
use std::any::Any;
5
use std::sync::LazyLock;
6

7
use arcref::ArcRef;
8
use vortex_dtype::DType;
9
use vortex_error::{VortexError, VortexExpect, VortexResult, vortex_bail, vortex_err};
10
use vortex_scalar::Scalar;
11

12
use crate::arrays::ConstantArray;
13
use crate::compute::{
14
    BooleanOperator, ComputeFn, ComputeFnVTable, InvocationArgs, Kernel, Operator, Options, Output,
15
    boolean, compare,
16
};
17
use crate::vtable::VTable;
18
use crate::{Array, ArrayRef, Canonical, IntoArray};
19

20
static BETWEEN_FN: LazyLock<ComputeFn> = LazyLock::new(|| {
84✔
21
    let compute = ComputeFn::new("between".into(), ArcRef::new_ref(&Between));
84✔
22
    for kernel in inventory::iter::<BetweenKernelRef> {
408✔
23
        compute.register_kernel(kernel.0.clone());
324✔
24
    }
324✔
25
    compute
84✔
26
});
84✔
27

28
/// Compute between (a <= x <= b).
29
///
30
/// This is an optimized implementation that is equivalent to `(a <= x) AND (x <= b)`.
31
///
32
/// The `BetweenOptions` defines if the lower or upper bounds are strict (exclusive) or non-strict
33
/// (inclusive).
34
pub fn between(
383✔
35
    arr: &dyn Array,
383✔
36
    lower: &dyn Array,
383✔
37
    upper: &dyn Array,
383✔
38
    options: &BetweenOptions,
383✔
39
) -> VortexResult<ArrayRef> {
383✔
40
    BETWEEN_FN
383✔
41
        .invoke(&InvocationArgs {
383✔
42
            inputs: &[arr.into(), lower.into(), upper.into()],
383✔
43
            options,
383✔
44
        })?
383✔
45
        .unwrap_array()
383✔
46
}
383✔
47

48
pub struct BetweenKernelRef(ArcRef<dyn Kernel>);
49
inventory::collect!(BetweenKernelRef);
50

51
pub trait BetweenKernel: VTable {
52
    fn between(
53
        &self,
54
        arr: &Self::Array,
55
        lower: &dyn Array,
56
        upper: &dyn Array,
57
        options: &BetweenOptions,
58
    ) -> VortexResult<Option<ArrayRef>>;
59
}
60

61
#[derive(Debug)]
62
pub struct BetweenKernelAdapter<V: VTable>(pub V);
63

64
impl<V: VTable + BetweenKernel> BetweenKernelAdapter<V> {
UNCOV
65
    pub const fn lift(&'static self) -> BetweenKernelRef {
×
UNCOV
66
        BetweenKernelRef(ArcRef::new_ref(self))
×
UNCOV
67
    }
×
68
}
69

70
impl<V: VTable + BetweenKernel> Kernel for BetweenKernelAdapter<V> {
71
    fn invoke(&self, args: &InvocationArgs) -> VortexResult<Option<Output>> {
1,055✔
72
        let inputs = BetweenArgs::try_from(args)?;
1,055✔
73
        let Some(array) = inputs.array.as_opt::<V>() else {
1,055✔
74
            return Ok(None);
819✔
75
        };
76
        Ok(
77
            V::between(&self.0, array, inputs.lower, inputs.upper, inputs.options)?
236✔
78
                .map(|array| array.into()),
236✔
79
        )
80
    }
1,055✔
81
}
82

83
struct Between;
84

85
impl ComputeFnVTable for Between {
86
    fn invoke(
383✔
87
        &self,
383✔
88
        args: &InvocationArgs,
383✔
89
        kernels: &[ArcRef<dyn Kernel>],
383✔
90
    ) -> VortexResult<Output> {
383✔
91
        let BetweenArgs {
92
            array,
383✔
93
            lower,
383✔
94
            upper,
383✔
95
            options,
383✔
96
        } = BetweenArgs::try_from(args)?;
383✔
97

98
        let return_dtype = self.return_dtype(args)?;
383✔
99

100
        // Bail early if the array is empty.
101
        if array.is_empty() {
383✔
UNCOV
102
            return Ok(Canonical::empty(&return_dtype).into_array().into());
×
103
        }
383✔
104

105
        // A quick check to see if either array might is a null constant array.
106
        // Note: Depends on returning early if array is empty for is_invalid check.
107
        if lower.is_invalid(0)? || upper.is_invalid(0)? {
383✔
108
            if let (Some(c_lower), Some(c_upper)) = (lower.as_constant(), upper.as_constant()) {
1✔
UNCOV
109
                if c_lower.is_null() || c_upper.is_null() {
×
UNCOV
110
                    return Ok(ConstantArray::new(Scalar::null(return_dtype), array.len())
×
UNCOV
111
                        .into_array()
×
UNCOV
112
                        .into());
×
UNCOV
113
                }
×
114
            }
1✔
115
        }
382✔
116

117
        if lower.as_constant().is_some_and(|v| v.is_null())
383✔
118
            || upper.as_constant().is_some_and(|v| v.is_null())
383✔
119
        {
120
            return Ok(ConstantArray::new(Scalar::null(return_dtype), array.len())
1✔
121
                .into_array()
1✔
122
                .into());
1✔
123
        }
382✔
124

125
        // Try each kernel
126
        for kernel in kernels {
1,206✔
127
            if let Some(output) = kernel.invoke(args)? {
1,055✔
128
                return Ok(output);
231✔
129
            }
824✔
130
        }
131
        if let Some(output) = array.invoke(&BETWEEN_FN, args)? {
151✔
132
            return Ok(output);
×
133
        }
151✔
134

135
        // Otherwise, fall back to the default Arrow implementation
136
        // TODO(joe): should we try to canonicalize the array and try between
137
        Ok(boolean(
151✔
138
            &compare(lower, array, options.lower_strict.to_operator())?,
151✔
139
            &compare(array, upper, options.upper_strict.to_operator())?,
151✔
140
            BooleanOperator::And,
151✔
UNCOV
141
        )?
×
142
        .into())
151✔
143
    }
383✔
144

145
    fn return_dtype(&self, args: &InvocationArgs) -> VortexResult<DType> {
766✔
146
        let BetweenArgs {
147
            array,
766✔
148
            lower,
766✔
149
            upper,
766✔
150
            options: _,
151
        } = BetweenArgs::try_from(args)?;
766✔
152

153
        if !array.dtype().eq_ignore_nullability(lower.dtype()) {
766✔
UNCOV
154
            vortex_bail!(
×
UNCOV
155
                "Array and lower bound types do not match: {:?} != {:?}",
×
UNCOV
156
                array.dtype(),
×
UNCOV
157
                lower.dtype()
×
158
            );
159
        }
766✔
160
        if !array.dtype().eq_ignore_nullability(upper.dtype()) {
766✔
161
            vortex_bail!(
×
UNCOV
162
                "Array and upper bound types do not match: {:?} != {:?}",
×
UNCOV
163
                array.dtype(),
×
UNCOV
164
                upper.dtype()
×
165
            );
166
        }
766✔
167

168
        Ok(DType::Bool(
766✔
169
            array.dtype().nullability() | lower.dtype().nullability() | upper.dtype().nullability(),
766✔
170
        ))
766✔
171
    }
766✔
172

173
    fn return_len(&self, args: &InvocationArgs) -> VortexResult<usize> {
383✔
174
        let BetweenArgs {
175
            array,
383✔
176
            lower,
383✔
177
            upper,
383✔
178
            options: _,
179
        } = BetweenArgs::try_from(args)?;
383✔
180
        if array.len() != lower.len() || array.len() != upper.len() {
383✔
181
            vortex_bail!(
×
182
                "Array lengths do not match: array:{} lower:{} upper:{}",
×
183
                array.len(),
×
184
                lower.len(),
×
UNCOV
185
                upper.len()
×
186
            );
187
        }
383✔
188
        Ok(array.len())
383✔
189
    }
383✔
190

191
    fn is_elementwise(&self) -> bool {
383✔
192
        true
383✔
193
    }
383✔
194
}
195

196
struct BetweenArgs<'a> {
197
    array: &'a dyn Array,
198
    lower: &'a dyn Array,
199
    upper: &'a dyn Array,
200
    options: &'a BetweenOptions,
201
}
202

203
impl<'a> TryFrom<&InvocationArgs<'a>> for BetweenArgs<'a> {
204
    type Error = VortexError;
205

206
    fn try_from(value: &InvocationArgs<'a>) -> VortexResult<Self> {
2,587✔
207
        if value.inputs.len() != 3 {
2,587✔
UNCOV
208
            vortex_bail!("Expected 3 inputs, found {}", value.inputs.len());
×
209
        }
2,587✔
210
        let array = value.inputs[0]
2,587✔
211
            .array()
2,587✔
212
            .ok_or_else(|| vortex_err!("Expected input 0 to be an array"))?;
2,587✔
213
        let lower = value.inputs[1]
2,587✔
214
            .array()
2,587✔
215
            .ok_or_else(|| vortex_err!("Expected input 1 to be an array"))?;
2,587✔
216
        let upper = value.inputs[2]
2,587✔
217
            .array()
2,587✔
218
            .ok_or_else(|| vortex_err!("Expected input 2 to be an array"))?;
2,587✔
219
        let options = value
2,587✔
220
            .options
2,587✔
221
            .as_any()
2,587✔
222
            .downcast_ref::<BetweenOptions>()
2,587✔
223
            .vortex_expect("Expected options to be an operator");
2,587✔
224

225
        Ok(BetweenArgs {
2,587✔
226
            array,
2,587✔
227
            lower,
2,587✔
228
            upper,
2,587✔
229
            options,
2,587✔
230
        })
2,587✔
231
    }
2,587✔
232
}
233

234
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
235
pub struct BetweenOptions {
236
    pub lower_strict: StrictComparison,
237
    pub upper_strict: StrictComparison,
238
}
239

240
impl Options for BetweenOptions {
241
    fn as_any(&self) -> &dyn Any {
2,587✔
242
        self
2,587✔
243
    }
2,587✔
244
}
245

246
/// Strictness of the comparison.
247
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
248
pub enum StrictComparison {
249
    /// Strict bound (`<`)
250
    Strict,
251
    /// Non-strict bound (`<=`)
252
    NonStrict,
253
}
254

255
impl StrictComparison {
256
    pub const fn to_operator(&self) -> Operator {
302✔
257
        match self {
302✔
258
            StrictComparison::Strict => Operator::Lt,
106✔
259
            StrictComparison::NonStrict => Operator::Lte,
196✔
260
        }
261
    }
302✔
262
}
263

264
#[cfg(test)]
265
mod tests {
266
    use vortex_dtype::{Nullability, PType};
267

268
    use super::*;
269
    use crate::ToCanonical;
270
    use crate::arrays::PrimitiveArray;
271
    use crate::compute::conformance::search_sorted::rstest;
272
    use crate::test_harness::to_int_indices;
273

274
    #[rstest]
275
    #[case(StrictComparison::NonStrict, StrictComparison::NonStrict, vec![0, 1, 2, 3])]
276
    #[case(StrictComparison::NonStrict, StrictComparison::Strict, vec![0, 1])]
277
    #[case(StrictComparison::Strict, StrictComparison::NonStrict, vec![0, 2])]
278
    #[case(StrictComparison::Strict, StrictComparison::Strict, vec![0])]
279
    fn test_bounds(
280
        #[case] lower_strict: StrictComparison,
281
        #[case] upper_strict: StrictComparison,
282
        #[case] expected: Vec<u64>,
283
    ) {
284
        let lower = PrimitiveArray::from_iter([0, 0, 0, 0, 2]);
285
        let array = PrimitiveArray::from_iter([1, 0, 1, 0, 1]);
286
        let upper = PrimitiveArray::from_iter([2, 1, 1, 0, 0]);
287

288
        let matches = between(
289
            array.as_ref(),
290
            lower.as_ref(),
291
            upper.as_ref(),
292
            &BetweenOptions {
293
                lower_strict,
294
                upper_strict,
295
            },
296
        )
297
        .unwrap()
298
        .to_bool()
299
        .unwrap();
300

301
        let indices = to_int_indices(matches).unwrap();
302
        assert_eq!(indices, expected);
303
    }
304

305
    #[test]
306
    fn test_constants() {
1✔
307
        let lower = PrimitiveArray::from_iter([0, 0, 2, 0, 2]);
1✔
308
        let array = PrimitiveArray::from_iter([1, 0, 1, 0, 1]);
1✔
309

310
        // upper is null
311
        let upper = ConstantArray::new(
1✔
312
            Scalar::null(DType::Primitive(PType::I32, Nullability::Nullable)),
1✔
313
            5,
314
        );
315

316
        let matches = between(
1✔
317
            array.as_ref(),
1✔
318
            lower.as_ref(),
1✔
319
            upper.as_ref(),
1✔
320
            &BetweenOptions {
1✔
321
                lower_strict: StrictComparison::NonStrict,
1✔
322
                upper_strict: StrictComparison::NonStrict,
1✔
323
            },
1✔
324
        )
1✔
325
        .unwrap()
1✔
326
        .to_bool()
1✔
327
        .unwrap();
1✔
328

329
        let indices = to_int_indices(matches).unwrap();
1✔
330
        assert!(indices.is_empty());
1✔
331

332
        // upper is a fixed constant
333
        let upper = ConstantArray::new(Scalar::from(2), 5);
1✔
334
        let matches = between(
1✔
335
            array.as_ref(),
1✔
336
            lower.as_ref(),
1✔
337
            upper.as_ref(),
1✔
338
            &BetweenOptions {
1✔
339
                lower_strict: StrictComparison::NonStrict,
1✔
340
                upper_strict: StrictComparison::NonStrict,
1✔
341
            },
1✔
342
        )
1✔
343
        .unwrap()
1✔
344
        .to_bool()
1✔
345
        .unwrap();
1✔
346
        let indices = to_int_indices(matches).unwrap();
1✔
347
        assert_eq!(indices, vec![0, 1, 3]);
1✔
348

349
        // lower is also a constant
350
        let lower = ConstantArray::new(Scalar::from(0), 5);
1✔
351

352
        let matches = between(
1✔
353
            array.as_ref(),
1✔
354
            lower.as_ref(),
1✔
355
            upper.as_ref(),
1✔
356
            &BetweenOptions {
1✔
357
                lower_strict: StrictComparison::NonStrict,
1✔
358
                upper_strict: StrictComparison::NonStrict,
1✔
359
            },
1✔
360
        )
1✔
361
        .unwrap()
1✔
362
        .to_bool()
1✔
363
        .unwrap();
1✔
364
        let indices = to_int_indices(matches).unwrap();
1✔
365
        assert_eq!(indices, vec![0, 1, 2, 3, 4]);
1✔
366
    }
1✔
367
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc