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