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 { Array::from_parts_unchecked(ArrayParts::new(Decimal, dtype, len, data, slots)) }
430    }
431
432    /// Creates a new [`DecimalArray`] from a host-native buffer with validation.
433    pub fn try_new<T: NativeDecimalType>(
434        buffer: Buffer<T>,
435        decimal_dtype: DecimalDType,
436        validity: Validity,
437    ) -> VortexResult<Self> {
438        let dtype = DType::Decimal(decimal_dtype, validity.nullability());
439        let len = buffer.len();
440        let slots = DecimalData::make_slots(&validity, len);
441        let data = DecimalData::try_new(buffer, decimal_dtype)?;
442        Array::try_from_parts(ArrayParts::new(Decimal, dtype, len, data, slots))
443    }
444
445    /// Creates a new [`DecimalArray`] from an iterator of values.
446    #[expect(
447        clippy::same_name_method,
448        reason = "intentionally named from_iter like Iterator::from_iter"
449    )]
450    pub fn from_iter<T: NativeDecimalType, I: IntoIterator<Item = T>>(
451        iter: I,
452        decimal_dtype: DecimalDType,
453    ) -> Self {
454        Self::new(
455            BufferMut::from_iter(iter).freeze(),
456            decimal_dtype,
457            Validity::NonNullable,
458        )
459    }
460
461    /// Creates a new [`DecimalArray`] from an iterator of optional values.
462    pub fn from_option_iter<T: NativeDecimalType, I: IntoIterator<Item = Option<T>>>(
463        iter: I,
464        decimal_dtype: DecimalDType,
465    ) -> Self {
466        let iter = iter.into_iter();
467        let mut values = BufferMut::with_capacity(iter.size_hint().0);
468        let mut validity = BitBufferMut::with_capacity(values.capacity());
469
470        for value in iter {
471            match value {
472                Some(value) => {
473                    values.push(value);
474                    validity.append(true);
475                }
476                None => {
477                    values.push(T::default());
478                    validity.append(false);
479                }
480            }
481        }
482
483        Self::new(
484            values.freeze(),
485            decimal_dtype,
486            Validity::from(validity.freeze()),
487        )
488    }
489
490    /// Creates a new [`DecimalArray`] from a [`BufferHandle`].
491    pub fn new_handle(
492        values: BufferHandle,
493        values_type: DecimalType,
494        decimal_dtype: DecimalDType,
495        validity: Validity,
496    ) -> Self {
497        Self::try_new_handle(values, values_type, decimal_dtype, validity)
498            .vortex_expect("DecimalArray construction failed")
499    }
500
501    /// Creates a new [`DecimalArray`] from a [`BufferHandle`] with validation.
502    pub fn try_new_handle(
503        values: BufferHandle,
504        values_type: DecimalType,
505        decimal_dtype: DecimalDType,
506        validity: Validity,
507    ) -> VortexResult<Self> {
508        let dtype = DType::Decimal(decimal_dtype, validity.nullability());
509        let len = values.len() / values_type.byte_width();
510        let slots = DecimalData::make_slots(&validity, len);
511        let data = DecimalData::try_new_handle(values, values_type, decimal_dtype)?;
512        Array::try_from_parts(ArrayParts::new(Decimal, dtype, len, data, slots))
513    }
514
515    /// Creates a new [`DecimalArray`] without validation from a [`BufferHandle`].
516    ///
517    /// # Safety
518    ///
519    /// See [`DecimalData::new_unchecked_handle`].
520    pub unsafe fn new_unchecked_handle(
521        values: BufferHandle,
522        values_type: DecimalType,
523        decimal_dtype: DecimalDType,
524        validity: Validity,
525    ) -> Self {
526        let dtype = DType::Decimal(decimal_dtype, validity.nullability());
527        let len = values.len() / values_type.byte_width();
528        let slots = DecimalData::make_slots(&validity, len);
529        let data = unsafe { DecimalData::new_unchecked_handle(values, values_type, decimal_dtype) };
530        unsafe { Array::from_parts_unchecked(ArrayParts::new(Decimal, dtype, len, data, slots)) }
531    }
532
533    #[expect(
534        clippy::cognitive_complexity,
535        reason = "patching depends on both patch and value physical types"
536    )]
537    pub fn patch(self, patches: &Patches, ctx: &mut ExecutionCtx) -> VortexResult<Self> {
538        let offset = patches.offset();
539        let dtype = self.dtype().clone();
540        let len = self.len();
541        let patch_indices = patches.indices().clone().execute::<PrimitiveArray>(ctx)?;
542        let patch_values = patches.values().clone().execute::<DecimalArray>(ctx)?;
543
544        let patch_validity = patch_values.validity()?;
545        let patched_validity = self.validity()?.patch(
546            self.len(),
547            offset,
548            &patch_indices.clone().into_array(),
549            &patch_validity,
550            ctx,
551        )?;
552        assert_eq!(self.decimal_dtype(), patch_values.decimal_dtype());
553
554        let data = self.into_data();
555        // Patch indices are non-negative; reinterpret to unsigned so this dispatches over 4 widths
556        // instead of 8 (the decimal value-type dimensions are unaffected).
557        let patch_indices_unsigned =
558            patch_indices.reinterpret_cast(patch_indices.ptype().to_unsigned());
559        let data = match_each_unsigned_integer_ptype!(patch_indices_unsigned.ptype(), |I| {
560            let patch_indices = patch_indices_unsigned.as_slice::<I>();
561            match_each_decimal_value_type!(patch_values.values_type(), |PatchDVT| {
562                let patch_values = patch_values.buffer::<PatchDVT>();
563                match_each_decimal_value_type!(data.values_type(), |ValuesDVT| {
564                    let buffer = data.buffer::<ValuesDVT>().into_mut();
565                    patch_typed(
566                        buffer,
567                        data.decimal_dtype(),
568                        patch_indices,
569                        offset,
570                        patch_values,
571                    )
572                })
573            })
574        });
575        let slots = DecimalData::make_slots(&patched_validity, len);
576        Ok(unsafe {
577            Array::from_parts_unchecked(ArrayParts::new(Decimal, dtype, len, data, slots))
578        })
579    }
580}
581
582fn patch_typed<I, ValuesDVT, PatchDVT>(
583    mut buffer: BufferMut<ValuesDVT>,
584    decimal_dtype: DecimalDType,
585    patch_indices: &[I],
586    patch_indices_offset: usize,
587    patch_values: Buffer<PatchDVT>,
588) -> DecimalData
589where
590    I: IntegerPType,
591    PatchDVT: NativeDecimalType,
592    ValuesDVT: NativeDecimalType,
593{
594    if !ValuesDVT::DECIMAL_TYPE.is_compatible_decimal_value_type(decimal_dtype) {
595        vortex_panic!(
596            "patch_typed: {:?} cannot represent every value in {}.",
597            ValuesDVT::DECIMAL_TYPE,
598            decimal_dtype
599        )
600    }
601
602    for (idx, value) in patch_indices.iter().zip_eq(patch_values) {
603        buffer[idx.as_() - patch_indices_offset] = <ValuesDVT as BigCast>::from(value).vortex_expect(
604            "values of a given DecimalDType are representable in all compatible NativeDecimalType",
605        );
606    }
607
608    DecimalData::new(buffer.freeze(), decimal_dtype)
609}