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

vortex-data / vortex / 16935267080

13 Aug 2025 11:00AM UTC coverage: 24.312% (-63.3%) from 87.658%
16935267080

Pull #4226

github

web-flow
Merge 81b48c7fb into baa6ea202
Pull Request #4226: Support converting TimestampTZ to and from duckdb

0 of 2 new or added lines in 1 file covered. (0.0%)

20666 existing lines in 469 files now uncovered.

8726 of 35892 relevant lines covered (24.31%)

147.74 hits per line

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

0.0
/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

UNCOV
20
static BETWEEN_FN: LazyLock<ComputeFn> = LazyLock::new(|| {
×
UNCOV
21
    let compute = ComputeFn::new("between".into(), ArcRef::new_ref(&Between));
×
UNCOV
22
    for kernel in inventory::iter::<BetweenKernelRef> {
×
UNCOV
23
        compute.register_kernel(kernel.0.clone());
×
UNCOV
24
    }
×
UNCOV
25
    compute
×
UNCOV
26
});
×
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).
UNCOV
34
pub fn between(
×
UNCOV
35
    arr: &dyn Array,
×
UNCOV
36
    lower: &dyn Array,
×
UNCOV
37
    upper: &dyn Array,
×
UNCOV
38
    options: &BetweenOptions,
×
UNCOV
39
) -> VortexResult<ArrayRef> {
×
UNCOV
40
    BETWEEN_FN
×
UNCOV
41
        .invoke(&InvocationArgs {
×
UNCOV
42
            inputs: &[arr.into(), lower.into(), upper.into()],
×
UNCOV
43
            options,
×
UNCOV
44
        })?
×
UNCOV
45
        .unwrap_array()
×
UNCOV
46
}
×
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> {
UNCOV
71
    fn invoke(&self, args: &InvocationArgs) -> VortexResult<Option<Output>> {
×
UNCOV
72
        let inputs = BetweenArgs::try_from(args)?;
×
UNCOV
73
        let Some(array) = inputs.array.as_opt::<V>() else {
×
UNCOV
74
            return Ok(None);
×
75
        };
76
        Ok(
UNCOV
77
            V::between(&self.0, array, inputs.lower, inputs.upper, inputs.options)?
×
UNCOV
78
                .map(|array| array.into()),
×
79
        )
UNCOV
80
    }
×
81
}
82

83
struct Between;
84

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

UNCOV
98
        let return_dtype = self.return_dtype(args)?;
×
99

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

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

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

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

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

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

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

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

UNCOV
190
    fn is_elementwise(&self) -> bool {
×
UNCOV
191
        true
×
UNCOV
192
    }
×
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

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

UNCOV
224
        Ok(BetweenArgs {
×
UNCOV
225
            array,
×
UNCOV
226
            lower,
×
UNCOV
227
            upper,
×
UNCOV
228
            options,
×
UNCOV
229
        })
×
UNCOV
230
    }
×
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 {
UNCOV
240
    fn as_any(&self) -> &dyn Any {
×
UNCOV
241
        self
×
UNCOV
242
    }
×
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 {
UNCOV
255
    pub const fn to_operator(&self) -> Operator {
×
UNCOV
256
        match self {
×
UNCOV
257
            StrictComparison::Strict => Operator::Lt,
×
UNCOV
258
            StrictComparison::NonStrict => Operator::Lte,
×
259
        }
UNCOV
260
    }
×
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() {
306
        let lower = PrimitiveArray::from_iter([0, 0, 2, 0, 2]);
307
        let array = PrimitiveArray::from_iter([1, 0, 1, 0, 1]);
308

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

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

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

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

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

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