Skip to main content

vortex_array/arrays/decimal/
array.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Display;
5use std::fmt::Formatter;
6
7use itertools::Itertools;
8use smallvec::smallvec;
9use vortex_buffer::Alignment;
10use vortex_buffer::BitBufferMut;
11use vortex_buffer::Buffer;
12use vortex_buffer::BufferMut;
13use vortex_buffer::ByteBuffer;
14use vortex_error::VortexExpect;
15use vortex_error::VortexResult;
16use vortex_error::vortex_ensure;
17use vortex_error::vortex_panic;
18
19use crate::ArrayRef;
20use crate::ArraySlots;
21use crate::ExecutionCtx;
22use crate::IntoArray;
23use crate::array::Array;
24use crate::array::ArrayParts;
25use crate::array::TypedArrayRef;
26use crate::array::child_to_validity;
27use crate::array::validity_to_child;
28use crate::array_slots;
29use crate::arrays::Decimal;
30use crate::arrays::DecimalArray;
31use crate::arrays::PrimitiveArray;
32use crate::arrays::primitive::PrimitiveArrayExt;
33use crate::buffer::BufferHandle;
34use crate::dtype::BigCast;
35use crate::dtype::DType;
36use crate::dtype::DecimalDType;
37use crate::dtype::DecimalType;
38use crate::dtype::IntegerPType;
39use crate::dtype::NativeDecimalType;
40use crate::dtype::Nullability;
41use crate::match_each_decimal_value_type;
42use crate::match_each_unsigned_integer_ptype;
43use crate::patches::Patches;
44use crate::validity::Validity;
45
46#[array_slots(Decimal)]
47pub struct DecimalSlots {
48    /// The validity bitmap indicating which elements are non-null.
49    #[slot(0)]
50    pub validity: Option<ArrayRef>,
51}
52
53/// A decimal array that stores fixed-precision decimal numbers with configurable scale.
54///
55/// This mirrors the Apache Arrow Decimal encoding and provides exact arithmetic for
56/// financial and scientific computations where floating-point precision loss is unacceptable.
57///
58/// ## Storage Format
59///
60/// Decimals are stored as scaled integers in a supported scalar value type.
61///
62/// The precisions supported for each scalar type are:
63/// - **i8**: precision 1-2 digits
64/// - **i16**: precision 3-4 digits
65/// - **i32**: precision 5-9 digits
66/// - **i64**: precision 10-18 digits
67/// - **i128**: precision 19-38 digits
68/// - **i256**: precision 39-76 digits
69///
70/// These are just the maximal ranges for each scalar type, but it is perfectly legal to store
71/// values with precision that does not match this exactly. For example, a valid DecimalArray with
72/// precision=39 may store its values in an `i8` if all of the actual values fit into it.
73///
74/// Similarly, a `DecimalArray` can be built that stores a set of precision=2 values in a
75/// `Buffer<i256>`.
76///
77/// ## Precision and Scale
78///
79/// - **Precision**: Total number of significant digits (1-76, u8 range)
80/// - **Scale**: Number of digits after the decimal point (-128 to 127, i8 range)
81/// - **Value**: `stored_integer / 10^scale`
82///
83/// For example, with precision=5 and scale=2:
84/// - Stored value 12345 represents 123.45
85/// - Range: -999.99 to 999.99
86///
87/// ## Valid Scalar Types
88///
89/// The underlying storage uses these native types based on precision:
90/// - `DecimalType::I8`, `I16`, `I32`, `I64`, `I128`, `I256`
91/// - Type selection is automatic based on the required precision
92///
93/// # Examples
94///
95/// ```
96/// use vortex_array::arrays::DecimalArray;
97/// use vortex_array::dtype::DecimalDType;
98/// use vortex_buffer::{buffer, Buffer};
99/// use vortex_array::validity::Validity;
100///
101/// // Create a decimal array with precision=5, scale=2 (e.g., 123.45)
102/// let decimal_dtype = DecimalDType::new(5, 2);
103/// let values = buffer![12345i32, 67890i32, -12300i32]; // 123.45, 678.90, -123.00
104/// let array = DecimalArray::new(values, decimal_dtype, Validity::NonNullable);
105///
106/// assert_eq!(array.precision(), 5);
107/// assert_eq!(array.scale(), 2);
108/// assert_eq!(array.len(), 3);
109/// ```
110#[derive(Clone, Debug)]
111pub struct DecimalData {
112    pub(super) decimal_dtype: DecimalDType,
113    pub(super) values: BufferHandle,
114    pub(super) values_type: DecimalType,
115}
116
117impl Display for DecimalData {
118    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
119        write!(
120            f,
121            "decimal_dtype: {}, values_type: {}",
122            self.decimal_dtype, self.values_type
123        )
124    }
125}
126
127pub struct DecimalDataParts {
128    pub decimal_dtype: DecimalDType,
129    pub values: BufferHandle,
130    pub values_type: DecimalType,
131    pub validity: Validity,
132}
133
134pub trait DecimalArrayExt: TypedArrayRef<Decimal> {
135    fn decimal_dtype(&self) -> DecimalDType {
136        match self.as_ref().dtype() {
137            DType::Decimal(decimal_dtype, _) => *decimal_dtype,
138            _ => unreachable!("DecimalArrayExt requires a decimal dtype"),
139        }
140    }
141
142    fn nullability(&self) -> Nullability {
143        match self.as_ref().dtype() {
144            DType::Decimal(_, nullability) => *nullability,
145            _ => unreachable!("DecimalArrayExt requires a decimal dtype"),
146        }
147    }
148
149    fn validity_child(&self) -> Option<&ArrayRef> {
150        self.as_ref().slots()[DecimalSlots::VALIDITY].as_ref()
151    }
152
153    fn validity(&self) -> Validity {
154        child_to_validity(
155            self.as_ref().slots()[DecimalSlots::VALIDITY].as_ref(),
156            self.nullability(),
157        )
158    }
159
160    fn values_type(&self) -> DecimalType {
161        self.values_type
162    }
163
164    fn precision(&self) -> u8 {
165        self.decimal_dtype().precision()
166    }
167
168    fn scale(&self) -> i8 {
169        self.decimal_dtype().scale()
170    }
171
172    fn buffer_handle(&self) -> &BufferHandle {
173        &self.values
174    }
175
176    fn buffer<T: NativeDecimalType>(&self) -> Buffer<T> {
177        DecimalData::buffer::<T>(self)
178    }
179}
180impl<T: TypedArrayRef<Decimal>> DecimalArrayExt for T {}
181
182impl DecimalData {
183    /// Build the slots vector for this array.
184    pub(super) fn make_slots(validity: &Validity, len: usize) -> ArraySlots {
185        smallvec![validity_to_child(validity, len)]
186    }
187
188    /// Creates a new [`DecimalArray`] using a host-native buffer.
189    ///
190    /// # Panics
191    ///
192    /// Panics if the provided components do not satisfy the invariants documented in
193    /// [`DecimalArray::new_unchecked`].
194    pub fn new<T: NativeDecimalType>(buffer: Buffer<T>, decimal_dtype: DecimalDType) -> Self {
195        Self::try_new(buffer, decimal_dtype).vortex_expect("DecimalArray construction failed")
196    }
197
198    /// Creates a new [`DecimalArray`] from a [`BufferHandle`] of values that may live in
199    /// host or device memory.
200    ///
201    /// # Panics
202    ///
203    /// Panics if the provided components do not satisfy the invariants documented in
204    /// [`DecimalArray::new_unchecked`].
205    pub fn new_handle(
206        values: BufferHandle,
207        values_type: DecimalType,
208        decimal_dtype: DecimalDType,
209    ) -> Self {
210        Self::try_new_handle(values, values_type, decimal_dtype)
211            .vortex_expect("DecimalArray construction failed")
212    }
213
214    /// Constructs a new `DecimalArray`.
215    ///
216    /// See [`DecimalArray::new_unchecked`] for more information.
217    ///
218    /// # Errors
219    ///
220    /// Returns an error if the provided components do not satisfy the invariants documented in
221    /// [`DecimalArray::new_unchecked`].
222    pub fn try_new<T: NativeDecimalType>(
223        buffer: Buffer<T>,
224        decimal_dtype: DecimalDType,
225    ) -> VortexResult<Self> {
226        let values = BufferHandle::new_host(buffer.into_byte_buffer());
227        let values_type = T::DECIMAL_TYPE;
228
229        Self::try_new_handle(values, values_type, decimal_dtype)
230    }
231
232    /// Constructs a new `DecimalArray` with validation from a [`BufferHandle`].
233    ///
234    /// This pathway allows building new decimal arrays that may come from host or device memory.
235    ///
236    /// # Errors
237    ///
238    /// See [`DecimalArray::new_unchecked`] for invariants that are checked.
239    pub fn try_new_handle(
240        values: BufferHandle,
241        values_type: DecimalType,
242        decimal_dtype: DecimalDType,
243    ) -> VortexResult<Self> {
244        Self::validate(&values, values_type)?;
245
246        // SAFETY: validate ensures all invariants are met.
247        Ok(unsafe { Self::new_unchecked_handle(values, values_type, decimal_dtype) })
248    }
249
250    /// Creates a new [`DecimalArray`] without validation from these components:
251    ///
252    /// * `buffer` is a typed buffer containing the decimal values.
253    /// * `decimal_dtype` specifies the decimal precision and scale.
254    /// * `validity` holds the null values.
255    ///
256    /// # Safety
257    ///
258    /// The caller must ensure all of the following invariants are satisfied:
259    ///
260    /// - All non-null values in `buffer` must be representable within the specified precision.
261    /// - For example, with precision=5 and scale=2, all values must be in range [-999.99, 999.99].
262    /// - If `validity` is [`Validity::Array`], its length must exactly equal `buffer.len()`.
263    pub unsafe fn new_unchecked<T: NativeDecimalType>(
264        buffer: Buffer<T>,
265        decimal_dtype: DecimalDType,
266    ) -> Self {
267        // SAFETY: new_unchecked_handle inherits the safety guarantees of new_unchecked
268        unsafe {
269            Self::new_unchecked_handle(
270                BufferHandle::new_host(buffer.into_byte_buffer()),
271                T::DECIMAL_TYPE,
272                decimal_dtype,
273            )
274        }
275    }
276
277    /// Create a new array with decimal values backed by the given buffer handle.
278    ///
279    /// # Safety
280    ///
281    /// The caller must ensure all of the following invariants are satisfied:
282    ///
283    /// - All non-null values in `values` must be representable within the specified precision.
284    /// - For example, with precision=5 and scale=2, all values must be in range [-999.99, 999.99].
285    /// - If `validity` is [`Validity::Array`], its length must exactly equal `buffer.len()`.
286    pub unsafe fn new_unchecked_handle(
287        values: BufferHandle,
288        values_type: DecimalType,
289        decimal_dtype: DecimalDType,
290    ) -> Self {
291        Self {
292            decimal_dtype,
293            values,
294            values_type,
295        }
296    }
297
298    /// Validates the components that would be used to create a [`DecimalArray`] from a byte buffer.
299    ///
300    /// This function checks all the invariants required by [`DecimalArray::new_unchecked`].
301    fn validate(buffer: &BufferHandle, values_type: DecimalType) -> VortexResult<()> {
302        let byte_width = values_type.byte_width();
303        vortex_ensure!(
304            buffer.len().is_multiple_of(byte_width),
305            InvalidArgument: "decimal buffer size {} is not divisible by element width {}",
306            buffer.len(),
307            byte_width,
308        );
309        match_each_decimal_value_type!(values_type, |D| {
310            vortex_ensure!(
311                buffer.is_aligned_to(Alignment::of::<D>()),
312                InvalidArgument: "decimal buffer alignment {:?} is invalid for values type {:?}",
313                buffer.alignment(),
314                D::DECIMAL_TYPE,
315            );
316            Ok::<(), vortex_error::VortexError>(())
317        })?;
318        Ok(())
319    }
320
321    /// Creates a new [`DecimalArray`] from a raw byte buffer without validation.
322    ///
323    /// # Safety
324    ///
325    /// The caller must ensure:
326    /// - The `byte_buffer` contains valid data for the specified `values_type`
327    /// - The buffer length is compatible with the `values_type` (i.e., divisible by the type size)
328    /// - All non-null values are representable within the specified precision
329    /// - If `validity` is [`Validity::Array`], its length must equal the number of elements
330    pub unsafe fn new_unchecked_from_byte_buffer(
331        byte_buffer: ByteBuffer,
332        values_type: DecimalType,
333        decimal_dtype: DecimalDType,
334    ) -> Self {
335        // SAFETY: inherits the same safety contract as `new_unchecked_from_byte_buffer`
336        unsafe {
337            Self::new_unchecked_handle(
338                BufferHandle::new_host(byte_buffer),
339                values_type,
340                decimal_dtype,
341            )
342        }
343    }
344
345    /// Returns the length of this array.
346    pub fn len(&self) -> usize {
347        self.values.len() / self.values_type.byte_width()
348    }
349
350    /// Returns `true` if this array is empty.
351    pub fn is_empty(&self) -> bool {
352        self.len() == 0
353    }
354
355    /// Returns the underlying [`ByteBuffer`] of the array.
356    pub fn buffer_handle(&self) -> &BufferHandle {
357        &self.values
358    }
359
360    pub fn buffer<T: NativeDecimalType>(&self) -> Buffer<T> {
361        if self.values_type != T::DECIMAL_TYPE {
362            vortex_panic!(
363                "Cannot extract Buffer<{:?}> for DecimalArray with values_type {:?}",
364                T::DECIMAL_TYPE,
365                self.values_type,
366            );
367        }
368        Buffer::<T>::from_byte_buffer(self.values.as_host().clone())
369    }
370
371    /// Return the `DecimalType` used to represent the values in the array.
372    pub fn values_type(&self) -> DecimalType {
373        self.values_type
374    }
375
376    /// Returns the decimal type information.
377    pub fn decimal_dtype(&self) -> DecimalDType {
378        self.decimal_dtype
379    }
380
381    pub fn precision(&self) -> u8 {
382        self.decimal_dtype.precision()
383    }
384
385    pub fn scale(&self) -> i8 {
386        self.decimal_dtype.scale()
387    }
388}
389
390impl Array<Decimal> {
391    pub fn into_data_parts(self) -> DecimalDataParts {
392        let validity = DecimalArrayExt::validity(&self);
393        let decimal_dtype = DecimalArrayExt::decimal_dtype(&self);
394        let data = self.into_data();
395        DecimalDataParts {
396            decimal_dtype,
397            values: data.values,
398            values_type: data.values_type,
399            validity,
400        }
401    }
402}
403
404impl Array<Decimal> {
405    /// Creates a new [`DecimalArray`] using a host-native buffer.
406    pub fn new<T: NativeDecimalType>(
407        buffer: Buffer<T>,
408        decimal_dtype: DecimalDType,
409        validity: Validity,
410    ) -> Self {
411        Self::try_new(buffer, decimal_dtype, validity)
412            .vortex_expect("DecimalArray construction failed")
413    }
414
415    /// Creates a new [`DecimalArray`] without validation.
416    ///
417    /// # Safety
418    ///
419    /// See [`DecimalData::new_unchecked`].
420    pub unsafe fn new_unchecked<T: NativeDecimalType>(
421        buffer: Buffer<T>,
422        decimal_dtype: DecimalDType,
423        validity: Validity,
424    ) -> Self {
425        let dtype = DType::Decimal(decimal_dtype, validity.nullability());
426        let len = buffer.len();
427        let slots = DecimalData::make_slots(&validity, len);
428        let data = unsafe { DecimalData::new_unchecked(buffer, decimal_dtype) };
429        unsafe {
430            Array::from_parts_unchecked(
431                ArrayParts::new(Decimal, dtype, len, data).with_slots(slots),
432            )
433        }
434    }
435
436    /// Creates a new [`DecimalArray`] from a host-native buffer with validation.
437    pub fn try_new<T: NativeDecimalType>(
438        buffer: Buffer<T>,
439        decimal_dtype: DecimalDType,
440        validity: Validity,
441    ) -> VortexResult<Self> {
442        let dtype = DType::Decimal(decimal_dtype, validity.nullability());
443        let len = buffer.len();
444        let slots = DecimalData::make_slots(&validity, len);
445        let data = DecimalData::try_new(buffer, decimal_dtype)?;
446        Array::try_from_parts(ArrayParts::new(Decimal, dtype, len, data).with_slots(slots))
447    }
448
449    /// Creates a new [`DecimalArray`] from an iterator of values.
450    #[expect(
451        clippy::same_name_method,
452        reason = "intentionally named from_iter like Iterator::from_iter"
453    )]
454    pub fn from_iter<T: NativeDecimalType, I: IntoIterator<Item = T>>(
455        iter: I,
456        decimal_dtype: DecimalDType,
457    ) -> Self {
458        Self::new(
459            BufferMut::from_iter(iter).freeze(),
460            decimal_dtype,
461            Validity::NonNullable,
462        )
463    }
464
465    /// Creates a new [`DecimalArray`] from an iterator of optional values.
466    pub fn from_option_iter<T: NativeDecimalType, I: IntoIterator<Item = Option<T>>>(
467        iter: I,
468        decimal_dtype: DecimalDType,
469    ) -> Self {
470        let iter = iter.into_iter();
471        let mut values = BufferMut::with_capacity(iter.size_hint().0);
472        let mut validity = BitBufferMut::with_capacity(values.capacity());
473
474        for value in iter {
475            match value {
476                Some(value) => {
477                    values.push(value);
478                    validity.append(true);
479                }
480                None => {
481                    values.push(T::default());
482                    validity.append(false);
483                }
484            }
485        }
486
487        Self::new(
488            values.freeze(),
489            decimal_dtype,
490            Validity::from(validity.freeze()),
491        )
492    }
493
494    /// Creates a new [`DecimalArray`] from a [`BufferHandle`].
495    pub fn new_handle(
496        values: BufferHandle,
497        values_type: DecimalType,
498        decimal_dtype: DecimalDType,
499        validity: Validity,
500    ) -> Self {
501        Self::try_new_handle(values, values_type, decimal_dtype, validity)
502            .vortex_expect("DecimalArray construction failed")
503    }
504
505    /// Creates a new [`DecimalArray`] from a [`BufferHandle`] with validation.
506    pub fn try_new_handle(
507        values: BufferHandle,
508        values_type: DecimalType,
509        decimal_dtype: DecimalDType,
510        validity: Validity,
511    ) -> VortexResult<Self> {
512        let dtype = DType::Decimal(decimal_dtype, validity.nullability());
513        let len = values.len() / values_type.byte_width();
514        let slots = DecimalData::make_slots(&validity, len);
515        let data = DecimalData::try_new_handle(values, values_type, decimal_dtype)?;
516        Array::try_from_parts(ArrayParts::new(Decimal, dtype, len, data).with_slots(slots))
517    }
518
519    /// Creates a new [`DecimalArray`] without validation from a [`BufferHandle`].
520    ///
521    /// # Safety
522    ///
523    /// See [`DecimalData::new_unchecked_handle`].
524    pub unsafe fn new_unchecked_handle(
525        values: BufferHandle,
526        values_type: DecimalType,
527        decimal_dtype: DecimalDType,
528        validity: Validity,
529    ) -> Self {
530        let dtype = DType::Decimal(decimal_dtype, validity.nullability());
531        let len = values.len() / values_type.byte_width();
532        let slots = DecimalData::make_slots(&validity, len);
533        let data = unsafe { DecimalData::new_unchecked_handle(values, values_type, decimal_dtype) };
534        unsafe {
535            Array::from_parts_unchecked(
536                ArrayParts::new(Decimal, dtype, len, data).with_slots(slots),
537            )
538        }
539    }
540
541    #[expect(
542        clippy::cognitive_complexity,
543        reason = "patching depends on both patch and value physical types"
544    )]
545    pub fn patch(self, patches: &Patches, ctx: &mut ExecutionCtx) -> VortexResult<Self> {
546        let offset = patches.offset();
547        let dtype = self.dtype().clone();
548        let len = self.len();
549        let patch_indices = patches.indices().clone().execute::<PrimitiveArray>(ctx)?;
550        let patch_values = patches.values().clone().execute::<DecimalArray>(ctx)?;
551
552        let patch_validity = patch_values.validity()?;
553        let patched_validity = self.validity()?.patch(
554            self.len(),
555            offset,
556            &patch_indices.clone().into_array(),
557            &patch_validity,
558            ctx,
559        )?;
560        assert_eq!(self.decimal_dtype(), patch_values.decimal_dtype());
561
562        let data = self.into_data();
563        // Patch indices are non-negative; reinterpret to unsigned so this dispatches over 4 widths
564        // instead of 8 (the decimal value-type dimensions are unaffected).
565        let patch_indices_unsigned =
566            patch_indices.reinterpret_cast(patch_indices.ptype().to_unsigned());
567        let data = match_each_unsigned_integer_ptype!(patch_indices_unsigned.ptype(), |I| {
568            let patch_indices = patch_indices_unsigned.as_slice::<I>();
569            match_each_decimal_value_type!(patch_values.values_type(), |PatchDVT| {
570                let patch_values = patch_values.buffer::<PatchDVT>();
571                match_each_decimal_value_type!(data.values_type(), |ValuesDVT| {
572                    let buffer = data.buffer::<ValuesDVT>().into_mut();
573                    patch_typed(
574                        buffer,
575                        data.decimal_dtype(),
576                        patch_indices,
577                        offset,
578                        patch_values,
579                    )
580                })
581            })
582        });
583        let slots = DecimalData::make_slots(&patched_validity, len);
584        Ok(unsafe {
585            Array::from_parts_unchecked(
586                ArrayParts::new(Decimal, dtype, len, data).with_slots(slots),
587            )
588        })
589    }
590}
591
592fn patch_typed<I, ValuesDVT, PatchDVT>(
593    mut buffer: BufferMut<ValuesDVT>,
594    decimal_dtype: DecimalDType,
595    patch_indices: &[I],
596    patch_indices_offset: usize,
597    patch_values: Buffer<PatchDVT>,
598) -> DecimalData
599where
600    I: IntegerPType,
601    PatchDVT: NativeDecimalType,
602    ValuesDVT: NativeDecimalType,
603{
604    if !ValuesDVT::DECIMAL_TYPE.is_compatible_decimal_value_type(decimal_dtype) {
605        vortex_panic!(
606            "patch_typed: {:?} cannot represent every value in {}.",
607            ValuesDVT::DECIMAL_TYPE,
608            decimal_dtype
609        )
610    }
611
612    for (idx, value) in patch_indices.iter().zip_eq(patch_values.into_iter()) {
613        buffer[idx.as_() - patch_indices_offset] = <ValuesDVT as BigCast>::from(value).vortex_expect(
614            "values of a given DecimalDType are representable in all compatible NativeDecimalType",
615        );
616    }
617
618    DecimalData::new(buffer.freeze(), decimal_dtype)
619}