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

vortex-data / vortex / 16595455362

29 Jul 2025 12:00PM UTC coverage: 82.182% (+0.4%) from 81.796%
16595455362

Pull #4036

github

web-flow
Merge 28b120788 into 261aabd6a
Pull Request #4036: varbinview builder buffer deduplication

147 of 155 new or added lines in 2 files covered. (94.84%)

404 existing lines in 34 files now uncovered.

44415 of 54045 relevant lines covered (82.18%)

167869.47 hits per line

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

82.86
/vortex-array/src/compute/numeric.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, VortexResult, vortex_bail, vortex_err};
10
use vortex_scalar::{NumericOperator, Scalar};
11

12
use crate::arrays::ConstantArray;
13
use crate::arrow::{Datum, from_arrow_array_with_len};
14
use crate::compute::{ComputeFn, ComputeFnVTable, InvocationArgs, Kernel, Options, Output};
15
use crate::vtable::VTable;
16
use crate::{Array, ArrayRef, IntoArray};
17

18
static NUMERIC_FN: LazyLock<ComputeFn> = LazyLock::new(|| {
1,077✔
19
    let compute = ComputeFn::new("numeric".into(), ArcRef::new_ref(&Numeric));
1,077✔
20
    for kernel in inventory::iter::<NumericKernelRef> {
3,446✔
21
        compute.register_kernel(kernel.0.clone());
2,369✔
22
    }
2,369✔
23
    compute
1,077✔
24
});
1,077✔
25

26
/// Point-wise add two numeric arrays.
27
///
28
/// Errs at runtime if the sum would overflow or underflow.
29
///
30
/// The result is null at any index that either input is null.
UNCOV
31
pub fn add(lhs: &dyn Array, rhs: &dyn Array) -> VortexResult<ArrayRef> {
×
UNCOV
32
    numeric(lhs, rhs, NumericOperator::Add)
×
UNCOV
33
}
×
34

35
/// Point-wise add a scalar value to this array on the right-hand-side.
36
pub fn add_scalar(lhs: &dyn Array, rhs: Scalar) -> VortexResult<ArrayRef> {
442✔
37
    numeric(
442✔
38
        lhs,
442✔
39
        &ConstantArray::new(rhs, lhs.len()).into_array(),
442✔
40
        NumericOperator::Add,
442✔
41
    )
42
}
442✔
43

44
/// Point-wise subtract two numeric arrays.
UNCOV
45
pub fn sub(lhs: &dyn Array, rhs: &dyn Array) -> VortexResult<ArrayRef> {
×
UNCOV
46
    numeric(lhs, rhs, NumericOperator::Sub)
×
UNCOV
47
}
×
48

49
/// Point-wise subtract a scalar value from this array on the right-hand-side.
50
pub fn sub_scalar(lhs: &dyn Array, rhs: Scalar) -> VortexResult<ArrayRef> {
1,629✔
51
    numeric(
1,629✔
52
        lhs,
1,629✔
53
        &ConstantArray::new(rhs, lhs.len()).into_array(),
1,629✔
54
        NumericOperator::Sub,
1,629✔
55
    )
56
}
1,629✔
57

58
/// Point-wise multiply two numeric arrays.
59
pub fn mul(lhs: &dyn Array, rhs: &dyn Array) -> VortexResult<ArrayRef> {
×
60
    numeric(lhs, rhs, NumericOperator::Mul)
×
UNCOV
61
}
×
62

63
/// Point-wise multiply a scalar value into this array on the right-hand-side.
UNCOV
64
pub fn mul_scalar(lhs: &dyn Array, rhs: Scalar) -> VortexResult<ArrayRef> {
×
65
    numeric(
×
66
        lhs,
×
67
        &ConstantArray::new(rhs, lhs.len()).into_array(),
×
UNCOV
68
        NumericOperator::Mul,
×
69
    )
70
}
×
71

72
/// Point-wise divide two numeric arrays.
73
pub fn div(lhs: &dyn Array, rhs: &dyn Array) -> VortexResult<ArrayRef> {
×
74
    numeric(lhs, rhs, NumericOperator::Div)
×
UNCOV
75
}
×
76

77
/// Point-wise divide a scalar value into this array on the right-hand-side.
UNCOV
78
pub fn div_scalar(lhs: &dyn Array, rhs: Scalar) -> VortexResult<ArrayRef> {
×
UNCOV
79
    numeric(
×
UNCOV
80
        lhs,
×
UNCOV
81
        &ConstantArray::new(rhs, lhs.len()).into_array(),
×
UNCOV
82
        NumericOperator::Mul,
×
83
    )
UNCOV
84
}
×
85

86
/// Point-wise numeric operation between two arrays of the same type and length.
87
pub fn numeric(lhs: &dyn Array, rhs: &dyn Array, op: NumericOperator) -> VortexResult<ArrayRef> {
5,278✔
88
    NUMERIC_FN
5,278✔
89
        .invoke(&InvocationArgs {
5,278✔
90
            inputs: &[lhs.into(), rhs.into()],
5,278✔
91
            options: &op,
5,278✔
92
        })?
5,278✔
93
        .unwrap_array()
5,277✔
94
}
5,278✔
95

96
pub struct NumericKernelRef(ArcRef<dyn Kernel>);
97
inventory::collect!(NumericKernelRef);
98

99
pub trait NumericKernel: VTable {
100
    fn numeric(
101
        &self,
102
        array: &Self::Array,
103
        other: &dyn Array,
104
        op: NumericOperator,
105
    ) -> VortexResult<Option<ArrayRef>>;
106
}
107

108
#[derive(Debug)]
109
pub struct NumericKernelAdapter<V: VTable>(pub V);
110

111
impl<V: VTable + NumericKernel> NumericKernelAdapter<V> {
UNCOV
112
    pub const fn lift(&'static self) -> NumericKernelRef {
×
UNCOV
113
        NumericKernelRef(ArcRef::new_ref(self))
×
UNCOV
114
    }
×
115
}
116

117
impl<V: VTable + NumericKernel> Kernel for NumericKernelAdapter<V> {
118
    fn invoke(&self, args: &InvocationArgs) -> VortexResult<Option<Output>> {
10,084✔
119
        let inputs = NumericArgs::try_from(args)?;
10,084✔
120
        let Some(lhs) = inputs.lhs.as_opt::<V>() else {
10,084✔
121
            return Ok(None);
5,414✔
122
        };
123
        Ok(V::numeric(&self.0, lhs, inputs.rhs, inputs.operator)?.map(|array| array.into()))
4,670✔
124
    }
10,084✔
125
}
126

127
struct Numeric;
128

129
impl ComputeFnVTable for Numeric {
130
    fn invoke(
5,316✔
131
        &self,
5,316✔
132
        args: &InvocationArgs,
5,316✔
133
        kernels: &[ArcRef<dyn Kernel>],
5,316✔
134
    ) -> VortexResult<Output> {
5,316✔
135
        let NumericArgs { lhs, rhs, operator } = NumericArgs::try_from(args)?;
5,316✔
136

137
        // Check if LHS supports the operation directly.
138
        for kernel in kernels {
14,546✔
139
            if let Some(output) = kernel.invoke(args)? {
10,332✔
140
                return Ok(output);
1,102✔
141
            }
9,230✔
142
        }
143
        if let Some(output) = lhs.invoke(&NUMERIC_FN, args)? {
4,214✔
144
            return Ok(output);
7✔
145
        }
4,207✔
146

147
        // Check if RHS supports the operation directly.
148
        let inverted_args = InvocationArgs {
4,207✔
149
            inputs: &[rhs.into(), lhs.into()],
4,207✔
150
            options: &operator.swap(),
4,207✔
151
        };
4,207✔
152
        for kernel in kernels {
10,998✔
153
            if let Some(output) = kernel.invoke(&inverted_args)? {
7,969✔
154
                return Ok(output);
1,178✔
155
            }
6,791✔
156
        }
157
        if let Some(output) = rhs.invoke(&NUMERIC_FN, &inverted_args)? {
3,029✔
158
            return Ok(output);
6✔
159
        }
3,023✔
160

161
        log::debug!(
3,023✔
162
            "No numeric implementation found for LHS {}, RHS {}, and operator {:?}",
×
163
            lhs.encoding_id(),
×
164
            rhs.encoding_id(),
×
165
            operator,
166
        );
167

168
        // If neither side implements the trait, then we delegate to Arrow compute.
169
        Ok(arrow_numeric(lhs, rhs, operator)?.into())
3,023✔
170
    }
5,316✔
171

172
    fn return_dtype(&self, args: &InvocationArgs) -> VortexResult<DType> {
5,330✔
173
        let NumericArgs { lhs, rhs, .. } = NumericArgs::try_from(args)?;
5,330✔
174
        if !matches!(
×
175
            (lhs.dtype(), rhs.dtype()),
5,330✔
176
            (DType::Primitive(..), DType::Primitive(..)) | (DType::Decimal(..), DType::Decimal(..))
177
        ) || !lhs.dtype().eq_ignore_nullability(rhs.dtype())
5,330✔
178
        {
179
            vortex_bail!(
1✔
180
                "Numeric operations are only supported on two arrays sharing the same numeric type: {} {}",
1✔
181
                lhs.dtype(),
1✔
182
                rhs.dtype()
1✔
183
            )
184
        }
5,329✔
185
        Ok(lhs.dtype().union_nullability(rhs.dtype().nullability()))
5,329✔
186
    }
5,330✔
187

188
    fn return_len(&self, args: &InvocationArgs) -> VortexResult<usize> {
5,316✔
189
        let NumericArgs { lhs, rhs, .. } = NumericArgs::try_from(args)?;
5,316✔
190
        if lhs.len() != rhs.len() {
5,316✔
191
            vortex_bail!(
×
192
                "Numeric operations aren't supported on arrays of different lengths {} {}",
×
193
                lhs.len(),
×
194
                rhs.len()
×
195
            )
196
        }
5,316✔
197
        Ok(lhs.len())
5,316✔
198
    }
5,316✔
199

200
    fn is_elementwise(&self) -> bool {
5,343✔
201
        true
5,343✔
202
    }
5,343✔
203
}
204

205
struct NumericArgs<'a> {
206
    lhs: &'a dyn Array,
207
    rhs: &'a dyn Array,
208
    operator: NumericOperator,
209
}
210

211
impl<'a> TryFrom<&InvocationArgs<'a>> for NumericArgs<'a> {
212
    type Error = VortexError;
213

214
    fn try_from(args: &InvocationArgs<'a>) -> VortexResult<Self> {
34,263✔
215
        if args.inputs.len() != 2 {
34,263✔
216
            vortex_bail!("Numeric operations require exactly 2 inputs");
×
217
        }
34,263✔
218
        let lhs = args.inputs[0]
34,263✔
219
            .array()
34,263✔
220
            .ok_or_else(|| vortex_err!("LHS is not an array"))?;
34,263✔
221
        let rhs = args.inputs[1]
34,263✔
222
            .array()
34,263✔
223
            .ok_or_else(|| vortex_err!("RHS is not an array"))?;
34,263✔
224
        let operator = *args
34,263✔
225
            .options
34,263✔
226
            .as_any()
34,263✔
227
            .downcast_ref::<NumericOperator>()
34,263✔
228
            .ok_or_else(|| vortex_err!("Operator is not a numeric operator"))?;
34,263✔
229
        Ok(Self { lhs, rhs, operator })
34,263✔
230
    }
34,263✔
231
}
232

233
impl Options for NumericOperator {
234
    fn as_any(&self) -> &dyn Any {
34,263✔
235
        self
34,263✔
236
    }
34,263✔
237
}
238

239
/// Implementation of `BinaryNumericFn` using the Arrow crate.
240
///
241
/// Note that other encodings should handle a constant RHS value, so we can assume here that
242
/// the RHS is not constant and expand to a full array.
243
fn arrow_numeric(
3,023✔
244
    lhs: &dyn Array,
3,023✔
245
    rhs: &dyn Array,
3,023✔
246
    operator: NumericOperator,
3,023✔
247
) -> VortexResult<ArrayRef> {
3,023✔
248
    let nullable = lhs.dtype().is_nullable() || rhs.dtype().is_nullable();
3,023✔
249
    let len = lhs.len();
3,023✔
250

251
    let left = Datum::try_new(lhs)?;
3,023✔
252
    let right = Datum::try_new(rhs)?;
3,023✔
253

254
    let array = match operator {
3,023✔
255
        NumericOperator::Add => arrow_arith::numeric::add(&left, &right)?,
334✔
256
        NumericOperator::Sub => arrow_arith::numeric::sub(&left, &right)?,
1,449✔
257
        NumericOperator::RSub => arrow_arith::numeric::sub(&right, &left)?,
310✔
258
        NumericOperator::Mul => arrow_arith::numeric::mul(&left, &right)?,
310✔
259
        NumericOperator::Div => arrow_arith::numeric::div(&left, &right)?,
310✔
260
        NumericOperator::RDiv => arrow_arith::numeric::div(&right, &left)?,
310✔
261
    };
262

263
    from_arrow_array_with_len(array.as_ref(), len, nullable)
3,023✔
264
}
3,023✔
265

266
#[cfg(test)]
267
mod test {
268
    use vortex_buffer::buffer;
269
    use vortex_scalar::Scalar;
270

271
    use crate::IntoArray;
272
    use crate::arrays::PrimitiveArray;
273
    use crate::canonical::ToCanonical;
274
    use crate::compute::sub_scalar;
275

276
    #[test]
277
    fn test_scalar_subtract_unsigned() {
1✔
278
        let values = buffer![1u16, 2, 3].into_array();
1✔
279
        let results = sub_scalar(&values, 1u16.into())
1✔
280
            .unwrap()
1✔
281
            .to_primitive()
1✔
282
            .unwrap()
1✔
283
            .as_slice::<u16>()
1✔
284
            .to_vec();
1✔
285
        assert_eq!(results, &[0u16, 1, 2]);
1✔
286
    }
1✔
287

288
    #[test]
289
    fn test_scalar_subtract_signed() {
1✔
290
        let values = buffer![1i64, 2, 3].into_array();
1✔
291
        let results = sub_scalar(&values, (-1i64).into())
1✔
292
            .unwrap()
1✔
293
            .to_primitive()
1✔
294
            .unwrap()
1✔
295
            .as_slice::<i64>()
1✔
296
            .to_vec();
1✔
297
        assert_eq!(results, &[2i64, 3, 4]);
1✔
298
    }
1✔
299

300
    #[test]
301
    fn test_scalar_subtract_nullable() {
1✔
302
        let values = PrimitiveArray::from_option_iter([Some(1u16), Some(2), None, Some(3)]);
1✔
303
        let result = sub_scalar(values.as_ref(), Some(1u16).into())
1✔
304
            .unwrap()
1✔
305
            .to_primitive()
1✔
306
            .unwrap();
1✔
307

308
        let actual = (0..result.len())
1✔
309
            .map(|index| result.scalar_at(index).unwrap())
4✔
310
            .collect::<Vec<_>>();
1✔
311
        assert_eq!(
1✔
312
            actual,
313
            vec![
1✔
314
                Scalar::from(Some(0u16)),
1✔
315
                Scalar::from(Some(1u16)),
1✔
316
                Scalar::from(None::<u16>),
1✔
317
                Scalar::from(Some(2u16))
1✔
318
            ]
319
        );
320
    }
1✔
321

322
    #[test]
323
    fn test_scalar_subtract_float() {
1✔
324
        let values = buffer![1.0f64, 2.0, 3.0].into_array();
1✔
325
        let to_subtract = -1f64;
1✔
326
        let results = sub_scalar(&values, to_subtract.into())
1✔
327
            .unwrap()
1✔
328
            .to_primitive()
1✔
329
            .unwrap()
1✔
330
            .as_slice::<f64>()
1✔
331
            .to_vec();
1✔
332
        assert_eq!(results, &[2.0f64, 3.0, 4.0]);
1✔
333
    }
1✔
334

335
    #[test]
336
    fn test_scalar_subtract_float_underflow_is_ok() {
1✔
337
        let values = buffer![f32::MIN, 2.0, 3.0].into_array();
1✔
338
        let _results = sub_scalar(&values, 1.0f32.into()).unwrap();
1✔
339
        let _results = sub_scalar(&values, f32::MAX.into()).unwrap();
1✔
340
    }
1✔
341

342
    #[test]
343
    fn test_scalar_subtract_type_mismatch_fails() {
1✔
344
        let values = buffer![1u64, 2, 3].into_array();
1✔
345
        // Subtracting incompatible dtypes should fail
346
        let _results =
1✔
347
            sub_scalar(&values, 1.5f64.into()).expect_err("Expected type mismatch error");
1✔
348
    }
1✔
349
}
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