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

vortex-data / vortex / 16811793386

07 Aug 2025 05:45PM UTC coverage: 84.877% (+0.03%) from 84.847%
16811793386

push

github

web-flow
chore: Bump msrv to 1.89 (#4159)

Mostly motivated by wanting to remove the horrible `ResutExt::unnest` I
introduced a few months ago, but also let chains are cool.

Signed-off-by: Adam Gutglick <adam@spiraldb.com>

107 of 157 new or added lines in 34 files covered. (68.15%)

2 existing lines in 2 files now uncovered.

50629 of 59650 relevant lines covered (84.88%)

567844.45 hits per line

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

87.5
/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(|| {
87✔
21
    let compute = ComputeFn::new("between".into(), ArcRef::new_ref(&Between));
87✔
22
    for kernel in inventory::iter::<BetweenKernelRef> {
423✔
23
        compute.register_kernel(kernel.0.clone());
336✔
24
    }
336✔
25
    compute
87✔
26
});
87✔
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(
462✔
35
    arr: &dyn Array,
462✔
36
    lower: &dyn Array,
462✔
37
    upper: &dyn Array,
462✔
38
    options: &BetweenOptions,
462✔
39
) -> VortexResult<ArrayRef> {
462✔
40
    BETWEEN_FN
462✔
41
        .invoke(&InvocationArgs {
462✔
42
            inputs: &[arr.into(), lower.into(), upper.into()],
462✔
43
            options,
462✔
44
        })?
462✔
45
        .unwrap_array()
462✔
46
}
462✔
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> {
65
    pub const fn lift(&'static self) -> BetweenKernelRef {
×
66
        BetweenKernelRef(ArcRef::new_ref(self))
×
67
    }
×
68
}
69

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

83
struct Between;
84

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

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

100
        // Bail early if the array is empty.
101
        if array.is_empty() {
462✔
102
            return Ok(Canonical::empty(&return_dtype).into_array().into());
×
103
        }
462✔
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)?)
462✔
108
            && let (Some(c_lower), Some(c_upper)) = (lower.as_constant(), upper.as_constant())
1✔
NEW
109
            && (c_lower.is_null() || c_upper.is_null())
×
110
        {
NEW
111
            return Ok(ConstantArray::new(Scalar::null(return_dtype), array.len())
×
NEW
112
                .into_array()
×
NEW
113
                .into());
×
114
        }
462✔
115

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

124
        // Try each kernel
125
        for kernel in kernels {
1,583✔
126
            if let Some(output) = kernel.invoke(args)? {
1,359✔
127
                return Ok(output);
237✔
128
            }
1,122✔
129
        }
130
        if let Some(output) = array.invoke(&BETWEEN_FN, args)? {
224✔
131
            return Ok(output);
×
132
        }
224✔
133

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

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

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

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

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

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

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

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

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

224
        Ok(BetweenArgs {
3,207✔
225
            array,
3,207✔
226
            lower,
3,207✔
227
            upper,
3,207✔
228
            options,
3,207✔
229
        })
3,207✔
230
    }
3,207✔
231
}
232

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

239
impl Options for BetweenOptions {
240
    fn as_any(&self) -> &dyn Any {
3,207✔
241
        self
3,207✔
242
    }
3,207✔
243
}
244

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

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

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

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

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

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

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

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

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

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

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

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

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

351
        let matches = between(
1✔
352
            array.as_ref(),
1✔
353
            lower.as_ref(),
1✔
354
            upper.as_ref(),
1✔
355
            &BetweenOptions {
1✔
356
                lower_strict: StrictComparison::NonStrict,
1✔
357
                upper_strict: StrictComparison::NonStrict,
1✔
358
            },
1✔
359
        )
1✔
360
        .unwrap()
1✔
361
        .to_bool()
1✔
362
        .unwrap();
1✔
363
        let indices = to_int_indices(matches).unwrap();
1✔
364
        assert_eq!(indices, vec![0, 1, 2, 3, 4]);
1✔
365
    }
1✔
366
}
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