Skip to main content

vortex_decimal_byte_parts/decimal_byte_parts/
array.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Decimal byte-parts array types, validation, and VTable implementations.
5
6use std::fmt::Display;
7use std::fmt::Formatter;
8use std::hash::Hasher;
9
10use vortex_array::Array;
11use vortex_array::ArrayEq;
12use vortex_array::ArrayHash;
13use vortex_array::ArrayId;
14use vortex_array::ArrayParts;
15use vortex_array::ArrayRef;
16use vortex_array::ArrayView;
17use vortex_array::EqMode;
18use vortex_array::ExecutionCtx;
19use vortex_array::ExecutionResult;
20use vortex_array::TypedArrayRef;
21use vortex_array::array_slots;
22use vortex_array::arrays::Primitive;
23use vortex_array::buffer::BufferHandle;
24use vortex_array::dtype::DType;
25use vortex_array::dtype::DecimalDType;
26use vortex_array::require_child;
27use vortex_array::scalar::DecimalValue;
28use vortex_array::scalar::Scalar;
29use vortex_array::scalar::ScalarValue;
30use vortex_array::serde::ArrayChildren;
31use vortex_array::vtable::OperationsVTable;
32use vortex_array::vtable::VTable;
33use vortex_array::vtable::ValidityChild;
34use vortex_array::vtable::ValidityVTableFromChild;
35use vortex_error::VortexExpect;
36use vortex_error::VortexResult;
37use vortex_error::vortex_bail;
38use vortex_error::vortex_ensure;
39use vortex_error::vortex_ensure_eq;
40use vortex_error::vortex_panic;
41use vortex_session::VortexSession;
42
43use super::MAX_LOWER_PARTS;
44use super::assemble::assemble_decimal;
45use super::assemble::assemble_wide_decimal_value;
46use super::decimal_byte_parts_v2_id;
47use super::rules::PARENT_RULES;
48
49/// A [`DecimalByteParts`]-encoded Vortex array.
50pub type DecimalBytePartsArray = Array<DecimalByteParts>;
51
52/// This array encodes decimals by splitting them between 1-4 columns of primitive typed children.
53///
54/// The most significant part (MSP) stores the most significant decimal bits. It is signed and is
55/// nullable iff the decimal is nullable.
56///
57/// Every lower part is a non-nullable unsigned integer holding a 64-bit window of the value.
58/// Parts may have narrower integer dtypes when their values fit; their positions stay 64 bits apart.
59///
60/// e.g. for a decimal i128 \[ 127..64 | 63..0 \] msp = 127..64 and lower_part\[0\] = 63..0
61///
62/// All parts live in slots, so the array carries no additional data.
63#[derive(Clone, Debug)]
64pub struct DecimalBytePartsData;
65
66impl Display for DecimalBytePartsData {
67    fn fmt(&self, _f: &mut Formatter<'_>) -> std::fmt::Result {
68        Ok(())
69    }
70}
71
72impl ArrayHash for DecimalBytePartsData {
73    fn array_hash<H: Hasher>(&self, _state: &mut H, _accuracy: EqMode) {}
74}
75
76impl ArrayEq for DecimalBytePartsData {
77    fn array_eq(&self, _other: &Self, _accuracy: EqMode) -> bool {
78        true
79    }
80}
81
82impl DecimalBytePartsData {
83    /// Validate the parts of a [`DecimalBytePartsArray`].
84    ///
85    /// # Errors
86    ///
87    /// Returns an error if the MSP is not a signed integer array of length `len`, if `dtype`
88    /// does not match the MSP's nullability, if there are more than `MAX_LOWER_PARTS`
89    /// lower parts, or if any lower part is not a non-nullable unsigned integer array of length `len`.
90    pub fn validate<'a>(
91        msp: &ArrayRef,
92        lower_parts: impl ExactSizeIterator<Item = &'a ArrayRef>,
93        decimal_dtype: DecimalDType,
94        dtype: &DType,
95        len: usize,
96    ) -> VortexResult<()> {
97        if !msp.dtype().is_signed_int() {
98            vortex_bail!("msp must be a signed integer array")
99        }
100
101        let expected_dtype = DType::Decimal(decimal_dtype, msp.dtype().nullability());
102        vortex_ensure_eq!(dtype, &expected_dtype);
103        vortex_ensure_eq!(msp.len(), len);
104
105        let lower_part_count = lower_parts.len();
106
107        vortex_ensure!(
108            lower_part_count <= MAX_LOWER_PARTS,
109            "at most {MAX_LOWER_PARTS} lower parts are supported, got {lower_part_count}"
110        );
111        for (idx, part) in lower_parts.enumerate() {
112            vortex_ensure!(
113                part.dtype().is_unsigned_int() && !part.dtype().is_nullable(),
114                "lower part {idx} must have a non-nullable unsigned integer dtype, got {}",
115                part.dtype()
116            );
117            vortex_ensure_eq!(part.len(), len, "lower part {idx} length mismatch");
118        }
119        Ok(())
120    }
121}
122
123#[derive(Clone, Debug)]
124pub struct DecimalByteParts;
125
126impl DecimalByteParts {
127    /// Construct a new [`DecimalBytePartsArray`] from an MSP array and decimal dtype.
128    ///
129    /// # Errors
130    ///
131    /// Returns an error if the MSP is not a signed integer array.
132    pub fn try_new(
133        msp: ArrayRef,
134        decimal_dtype: DecimalDType,
135    ) -> VortexResult<DecimalBytePartsArray> {
136        Self::try_new_with_lower_parts(msp, Vec::new(), decimal_dtype)
137    }
138
139    /// Construct a new [`DecimalBytePartsArray`] from an MSP array, its lower parts, and a
140    /// decimal dtype.
141    ///
142    /// Lower parts are ordered most significant first and must each be a non-nullable unsigned integer
143    /// array of the same length as the MSP. See [`super::split_decimal`] for producing them from a
144    /// decimal array.
145    ///
146    /// # Errors
147    ///
148    /// Returns an error if the parts do not describe a valid decimal, see
149    /// [`DecimalBytePartsData::validate`].
150    pub fn try_new_with_lower_parts(
151        msp: ArrayRef,
152        lower_parts: Vec<ArrayRef>,
153        decimal_dtype: DecimalDType,
154    ) -> VortexResult<DecimalBytePartsArray> {
155        let len = msp.len();
156        let dtype = DType::Decimal(decimal_dtype, msp.dtype().nullability());
157        let slots = DecimalBytePartsSlots { msp, lower_parts }.into_slots();
158        Array::try_from_parts(ArrayParts::new(
159            DecimalByteParts,
160            dtype,
161            len,
162            DecimalBytePartsData,
163            slots,
164        ))
165    }
166
167    /// Construct a [`DecimalBytePartsArray`] from parts whose invariants are already established.
168    ///
169    /// # Safety
170    ///
171    /// The MSP must have a signed integer dtype (`i8`, `i16`, `i32`, or `i64`). There must be
172    /// at most `MAX_LOWER_PARTS` lower parts, each a non-nullable unsigned integer array with the same
173    /// length as the MSP. Lower parts are ordered most significant first.
174    pub(super) unsafe fn new_unchecked(
175        msp: ArrayRef,
176        lower_parts: Vec<ArrayRef>,
177        decimal_dtype: DecimalDType,
178    ) -> DecimalBytePartsArray {
179        let len = msp.len();
180        let dtype = DType::Decimal(decimal_dtype, msp.dtype().nullability());
181        let slots = DecimalBytePartsSlots { msp, lower_parts }.into_slots();
182        // SAFETY: the caller guarantees the part types, lengths, and count. The slot builder
183        // fills every required slot, and the length and nullability come from the MSP.
184        unsafe {
185            Array::from_parts_unchecked(ArrayParts::new(
186                DecimalByteParts,
187                dtype,
188                len,
189                DecimalBytePartsData,
190                slots,
191            ))
192        }
193    }
194}
195
196impl VTable for DecimalByteParts {
197    type TypedArrayData = DecimalBytePartsData;
198
199    type OperationsVTable = Self;
200    type ValidityVTable = ValidityVTableFromChild;
201
202    fn id(&self) -> ArrayId {
203        decimal_byte_parts_v2_id()
204    }
205
206    fn validate(
207        &self,
208        _data: &Self::TypedArrayData,
209        dtype: &DType,
210        len: usize,
211        slots: &[Option<ArrayRef>],
212    ) -> VortexResult<()> {
213        let Some(decimal_dtype) = dtype.as_decimal_opt() else {
214            vortex_bail!("expected decimal dtype, got {}", dtype)
215        };
216
217        let min_slots = DecimalBytePartsSlots::FIXED_COUNT;
218        let max_slots = min_slots + MAX_LOWER_PARTS;
219        vortex_ensure!(
220            (min_slots..=max_slots).contains(&slots.len()),
221            "expected {min_slots}..={max_slots} slots, got {}",
222            slots.len()
223        );
224        for (idx, slot) in slots.iter().enumerate() {
225            vortex_ensure!(slot.is_some(), "missing required slot {idx}");
226        }
227
228        let slots = DecimalBytePartsSlotsView::from_slots(slots);
229        DecimalBytePartsData::validate(
230            slots.msp,
231            slots.lower_parts.iter(),
232            *decimal_dtype,
233            dtype,
234            len,
235        )
236    }
237
238    fn nbuffers(_array: ArrayView<'_, Self>) -> usize {
239        0
240    }
241
242    fn buffer(_array: ArrayView<'_, Self>, idx: usize) -> BufferHandle {
243        vortex_panic!("DecimalBytePartsArray buffer index {idx} out of bounds")
244    }
245
246    fn buffer_name(_array: ArrayView<'_, Self>, idx: usize) -> Option<String> {
247        vortex_panic!("DecimalBytePartsArray buffer_name index {idx} out of bounds")
248    }
249
250    fn with_buffers(
251        &self,
252        array: ArrayView<'_, Self>,
253        buffers: &[BufferHandle],
254    ) -> VortexResult<ArrayParts<Self>> {
255        vortex_array::vtable::with_empty_buffers(self, array, buffers)
256    }
257
258    fn serialize(
259        _array: ArrayView<'_, Self>,
260        _session: &VortexSession,
261    ) -> VortexResult<Option<Vec<u8>>> {
262        vortex_bail!("DecimalByteParts serialization requires DecimalBytePartsPlugin")
263    }
264
265    fn deserialize(
266        &self,
267        _dtype: &DType,
268        _len: usize,
269        _metadata: &[u8],
270        _buffers: &[BufferHandle],
271        _children: &dyn ArrayChildren,
272        _session: &VortexSession,
273    ) -> VortexResult<ArrayParts<Self>> {
274        vortex_bail!("DecimalByteParts deserialization requires DecimalBytePartsPlugin")
275    }
276
277    fn slot_name(_array: ArrayView<'_, Self>, idx: usize) -> String {
278        DecimalBytePartsSlots::slot_name(idx)
279    }
280
281    fn reduce_parent(
282        array: ArrayView<'_, Self>,
283        parent: &ArrayRef,
284        child_idx: usize,
285    ) -> VortexResult<Option<ArrayRef>> {
286        PARENT_RULES.evaluate(array, parent, child_idx)
287    }
288
289    fn execute(array: Array<Self>, ctx: &mut ExecutionCtx) -> VortexResult<ExecutionResult> {
290        let mut array = require_child!(array, array.msp(), DecimalBytePartsSlots::MSP => Primitive);
291        for idx in 0..array.lower_parts().len() {
292            array = require_child!(
293                array,
294                array.lower_parts()[idx],
295                DecimalBytePartsSlots::LOWER_PARTS_OFFSET + idx => Primitive
296            );
297        }
298        let lower_parts = array.lower_parts().to_vec();
299        let assembled = assemble_decimal(array.msp(), &lower_parts, array.decimal_dtype(), ctx)?;
300
301        Ok(ExecutionResult::done(assembled))
302    }
303}
304
305#[array_slots(DecimalByteParts)]
306pub struct DecimalBytePartsSlots {
307    /// The most significant parts of the decimal values.
308    #[slot(0)]
309    pub msp: ArrayRef,
310    /// The remaining 64-bit windows of the decimal values, most significant first.
311    #[slot(1..)]
312    pub lower_parts: Vec<ArrayRef>,
313}
314
315pub(crate) trait DecimalBytePartsArrayExt: DecimalBytePartsArraySlotsExt {
316    /// The decimal dtype of this array.
317    fn decimal_dtype(&self) -> DecimalDType {
318        *self
319            .as_ref()
320            .dtype()
321            .as_decimal_opt()
322            .vortex_expect("must be a decimal dtype")
323    }
324
325    /// Rebuild the array by applying `f` to the MSP and every lower part, in slot order.
326    ///
327    /// This applies row operations such as slicing and filtering to all parts together,
328    /// preserving the decimal precision and scale.
329    fn map_parts(
330        &self,
331        mut f: impl FnMut(&ArrayRef) -> VortexResult<ArrayRef>,
332    ) -> VortexResult<DecimalBytePartsArray> {
333        let msp = f(self.msp())?;
334        let lower_parts = self
335            .lower_parts()
336            .iter()
337            .map(&mut f)
338            .collect::<VortexResult<Vec<_>>>()?;
339        DecimalByteParts::try_new_with_lower_parts(msp, lower_parts, self.decimal_dtype())
340    }
341
342    /// Rebuild the array with a replacement MSP, preserving its lower parts, precision and scale.
343    ///
344    /// Use this for operations such as masking and nullability casts that only affect the MSP.
345    /// The replacement MSP determines the result's nullability.
346    fn with_msp(&self, msp: ArrayRef) -> VortexResult<DecimalBytePartsArray> {
347        DecimalByteParts::try_new_with_lower_parts(
348            msp,
349            self.lower_parts().to_vec(),
350            self.decimal_dtype(),
351        )
352    }
353}
354
355impl<T: TypedArrayRef<DecimalByteParts>> DecimalBytePartsArrayExt for T {}
356
357impl OperationsVTable<DecimalByteParts> for DecimalByteParts {
358    type ProbeState = ();
359
360    fn scalar_at(
361        array: ArrayView<'_, DecimalByteParts>,
362        index: usize,
363        ctx: &mut ExecutionCtx,
364    ) -> VortexResult<Scalar> {
365        let scalar = array.msp().execute_scalar(index, ctx)?;
366
367        // Widen the MSP's signed value (i8/i16/i32/i64) to i64 for scalar reconstruction.
368        // The array retains its original MSP storage type.
369        let primitive_scalar = scalar.as_primitive();
370        let msp = primitive_scalar.as_::<i64>().vortex_expect("non-null");
371
372        // Zero-extend each narrowed lower value to its 64-bit window.
373        let lower_parts = array
374            .lower_parts()
375            .iter()
376            .map(|part| {
377                Ok(part
378                    .execute_scalar(index, ctx)?
379                    .as_primitive()
380                    .as_::<u64>()
381                    .vortex_expect("lower parts are non-nullable"))
382            })
383            .collect::<VortexResult<Vec<_>>>()?;
384
385        let value = match lower_parts.as_slice() {
386            [] => DecimalValue::I64(msp),
387            [first] => DecimalValue::I128(assemble_wide_decimal_value(msp, [*first])),
388            [first, second] => {
389                DecimalValue::I256(assemble_wide_decimal_value(msp, [*first, *second]))
390            }
391            [first, second, third] => {
392                DecimalValue::I256(assemble_wide_decimal_value(msp, [*first, *second, *third]))
393            }
394            _ => vortex_bail!(
395                "at most {MAX_LOWER_PARTS} lower parts are supported, got {}",
396                lower_parts.len()
397            ),
398        };
399
400        Scalar::try_new(array.dtype().clone(), Some(ScalarValue::Decimal(value)))
401    }
402}
403
404impl ValidityChild<DecimalByteParts> for DecimalByteParts {
405    fn validity_child(array: ArrayView<'_, DecimalByteParts>) -> ArrayRef {
406        // validity stored in 0th child
407        array.msp().clone()
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use rstest::rstest;
414    use vortex_array::Array;
415    use vortex_array::ArrayParts;
416    use vortex_array::ArrayRef;
417    use vortex_array::IntoArray;
418    use vortex_array::VortexSessionExecute;
419    use vortex_array::array_session;
420    use vortex_array::arrays::BoolArray;
421    use vortex_array::arrays::DecimalArray;
422    use vortex_array::arrays::PrimitiveArray;
423    use vortex_array::assert_arrays_eq;
424    use vortex_array::builtins::ArrayBuiltins;
425    use vortex_array::dtype::DType;
426    use vortex_array::dtype::DecimalDType;
427    use vortex_array::dtype::DecimalType;
428    use vortex_array::dtype::Nullability;
429    use vortex_array::dtype::PType;
430    use vortex_array::dtype::i256;
431    use vortex_array::scalar::DecimalValue;
432    use vortex_array::scalar::Scalar;
433    use vortex_array::scalar::ScalarValue;
434    use vortex_array::validity::Validity;
435    use vortex_buffer::buffer;
436    use vortex_error::VortexResult;
437
438    use super::DecimalByteParts;
439    use super::DecimalBytePartsArraySlotsExt;
440    use super::DecimalBytePartsData;
441    use crate::decimal_byte_parts::LOWER_PART_DTYPE;
442    use crate::decimal_byte_parts::MAX_LOWER_PARTS;
443    use crate::decimal_byte_parts::testing::i128_parts;
444    use crate::decimal_byte_parts::testing::i256_parts;
445
446    #[test]
447    fn test_scalar_at_decimal_parts() {
448        let decimal_dtype = DecimalDType::new(8, 2);
449        let dtype = DType::Decimal(decimal_dtype, Nullability::Nullable);
450        let array = DecimalByteParts::try_new(
451            PrimitiveArray::new(
452                buffer![100i32, 200i32, 400i32],
453                Validity::Array(BoolArray::from_iter(vec![false, true, true]).into_array()),
454            )
455            .into_array(),
456            decimal_dtype,
457        )
458        .unwrap()
459        .into_array();
460
461        assert_eq!(
462            Scalar::null(dtype.clone()),
463            array
464                .execute_scalar(0, &mut array_session().create_execution_ctx())
465                .unwrap()
466        );
467        assert_eq!(
468            Scalar::try_new(
469                dtype.clone(),
470                Some(ScalarValue::Decimal(DecimalValue::I64(200)))
471            )
472            .unwrap(),
473            array
474                .execute_scalar(1, &mut array_session().create_execution_ctx())
475                .unwrap()
476        );
477        assert_eq!(
478            Scalar::try_new(dtype, Some(ScalarValue::Decimal(DecimalValue::I64(400)))).unwrap(),
479            array
480                .execute_scalar(2, &mut array_session().create_execution_ctx())
481                .unwrap()
482        );
483    }
484
485    #[test]
486    fn test_lower_part_layout_i128() -> VortexResult<()> {
487        let array = i128_parts(vec![(3i128 << 64) | 7], Validity::NonNullable);
488        assert_eq!(array.lower_parts().len(), 1);
489        assert_eq!(array.msp().dtype().as_ptype(), PType::I64);
490        assert_eq!(array.lower_parts()[0].dtype(), &LOWER_PART_DTYPE);
491
492        let mut ctx = array_session().create_execution_ctx();
493        let msp = array.msp().clone().execute::<PrimitiveArray>(&mut ctx)?;
494        let lower = array.lower_parts()[0]
495            .clone()
496            .execute::<PrimitiveArray>(&mut ctx)?;
497        assert_eq!(msp.as_slice::<i64>(), &[3]);
498        assert_eq!(lower.as_slice::<u64>(), &[7]);
499        Ok(())
500    }
501
502    #[test]
503    fn test_lower_part_layout_i256() -> VortexResult<()> {
504        let array = i256_parts(
505            vec![i256::from_parts((7u128 << 64) | 8, (5i128 << 64) | 6)],
506            Validity::NonNullable,
507        );
508        assert_eq!(array.lower_parts().len(), MAX_LOWER_PARTS);
509
510        let mut ctx = array_session().create_execution_ctx();
511        let msp = array.msp().clone().execute::<PrimitiveArray>(&mut ctx)?;
512        assert_eq!(msp.as_slice::<i64>(), &[5]);
513        for (part, expected) in array.lower_parts().iter().zip([6u64, 7, 8]) {
514            let part = part.clone().execute::<PrimitiveArray>(&mut ctx)?;
515            assert_eq!(part.as_slice::<u64>(), &[expected]);
516        }
517        Ok(())
518    }
519
520    #[rstest]
521    fn test_scalar_at_matches_canonical_for_each_part_count(
522        #[values(false, true)] narrow_msp: bool,
523        #[values(0, 1, 2, 3)] lower_count: usize,
524    ) -> VortexResult<()> {
525        let validity = Validity::from_iter([false, true, true]);
526        let msp = if narrow_msp {
527            PrimitiveArray::new(buffer![0i8, 3, -3], validity)
528        } else {
529            PrimitiveArray::new(buffer![0i64, 3, -3], validity)
530        };
531        let lower = [4u64, 1, 2]
532            .into_iter()
533            .take(lower_count)
534            .map(|word| PrimitiveArray::new(buffer![word; 3], Validity::NonNullable).into_array())
535            .collect();
536        let dtype = DecimalDType::new(if lower_count <= 1 { 38 } else { 76 }, 0);
537        let array = DecimalByteParts::try_new_with_lower_parts(msp.into_array(), lower, dtype)?;
538        let mut ctx = array_session().create_execution_ctx();
539        let canonical = array
540            .clone()
541            .into_array()
542            .execute::<DecimalArray>(&mut ctx)?;
543        for row in 0..array.len() {
544            assert_eq!(
545                array.execute_scalar(row, &mut ctx)?,
546                canonical.execute_scalar(row, &mut ctx)?
547            );
548        }
549        Ok(())
550    }
551
552    #[test]
553    fn test_scalar_at_null_with_lower_parts() -> VortexResult<()> {
554        let array = i128_parts(
555            vec![1i128 << 100, 2, 3],
556            Validity::Array(BoolArray::from_iter([false, true, true]).into_array()),
557        )
558        .into_array();
559        let mut ctx = array_session().create_execution_ctx();
560        assert_eq!(
561            array.execute_scalar(0, &mut ctx)?,
562            Scalar::null(array.dtype().clone())
563        );
564        assert_eq!(
565            array.execute_scalar(1, &mut ctx)?,
566            Scalar::decimal(
567                DecimalValue::I128(2),
568                DecimalDType::new(38, 2),
569                Nullability::Nullable
570            )
571        );
572        Ok(())
573    }
574
575    #[rstest]
576    #[case([PType::U8, PType::U16, PType::U32])]
577    #[case([PType::U16, PType::U32, PType::U64])]
578    #[case([PType::U32, PType::U64, PType::U8])]
579    #[case([PType::U64, PType::U8, PType::U16])]
580    fn test_independently_narrowed_parts(
581        #[case] lower_ptypes: [PType; 3],
582        #[values(PType::I8, PType::I16, PType::I32, PType::I64)] msp_ptype: PType,
583        #[values(1, 2, 3)] lower_count: usize,
584    ) -> VortexResult<()> {
585        let mut ctx = array_session().create_execution_ctx();
586        let validity = Validity::from_iter([false, true, true]);
587        let msp = PrimitiveArray::new(buffer![0i64, 1, -1], validity.clone())
588            .into_array()
589            .cast(DType::Primitive(msp_ptype, Nullability::Nullable))?;
590        // Set the unsigned type's highest bit to catch accidental sign extension.
591        let words = lower_ptypes.map(|ptype| 1u64 << (ptype.byte_width() * 8 - 1));
592        let lower = lower_ptypes
593            .into_iter()
594            .zip(words)
595            .take(lower_count)
596            .map(|(ptype, word)| {
597                buffer![0u64, word, word]
598                    .into_array()
599                    .cast(DType::Primitive(ptype, Nullability::NonNullable))
600            })
601            .collect::<VortexResult<Vec<_>>>()?;
602        let dtype = DecimalDType::new(if lower_count == 1 { 38 } else { 76 }, 0);
603        let encoded = DecimalByteParts::try_new_with_lower_parts(msp, lower, dtype)?;
604        let expected = match lower_count {
605            1 => DecimalArray::new(
606                buffer![
607                    0i128,
608                    (1i128 << 64) | i128::from(words[0]),
609                    (-1i128 << 64) | i128::from(words[0])
610                ],
611                dtype,
612                validity,
613            ),
614            2 => {
615                let low = (u128::from(words[0]) << 64) | u128::from(words[1]);
616                DecimalArray::new(
617                    buffer![
618                        i256::ZERO,
619                        i256::from_parts(low, 1),
620                        i256::from_parts(low, -1)
621                    ],
622                    dtype,
623                    validity,
624                )
625            }
626            _ => {
627                let low = (u128::from(words[1]) << 64) | u128::from(words[2]);
628                DecimalArray::new(
629                    buffer![
630                        i256::ZERO,
631                        i256::from_parts(low, (1i128 << 64) | i128::from(words[0])),
632                        i256::from_parts(low, (-1i128 << 64) | i128::from(words[0])),
633                    ],
634                    dtype,
635                    validity,
636                )
637            }
638        };
639        assert_arrays_eq!(encoded, expected, &mut ctx);
640        for row in 0..expected.len() {
641            assert_eq!(
642                encoded.execute_scalar(row, &mut ctx)?,
643                expected.execute_scalar(row, &mut ctx)?
644            );
645        }
646        Ok(())
647    }
648
649    fn msp() -> ArrayRef {
650        buffer![1i64, 2, 3].into_array()
651    }
652
653    fn lower_part() -> ArrayRef {
654        buffer![1u64, 2, 3].into_array()
655    }
656
657    #[rstest]
658    #[case::signed_lower_part(vec![buffer![1i64, 2, 3].into_array()], DecimalDType::new(38, 2))]
659    #[case::nullable_lower_part(
660        vec![PrimitiveArray::new(buffer![1u64, 2, 3], Validity::AllValid).into_array()],
661        DecimalDType::new(38, 2)
662    )]
663    #[case::mismatched_length(vec![buffer![1u64, 2].into_array()], DecimalDType::new(38, 2))]
664    #[case::too_many_parts(
665        vec![lower_part(), lower_part(), lower_part(), lower_part()],
666        DecimalDType::new(76, 2)
667    )]
668    fn test_rejects_invalid_parts(
669        #[case] lower_parts: Vec<ArrayRef>,
670        #[case] decimal_dtype: DecimalDType,
671    ) {
672        assert!(
673            DecimalByteParts::try_new_with_lower_parts(msp(), lower_parts, decimal_dtype).is_err()
674        );
675    }
676
677    #[rstest]
678    #[case::no_slots(vec![])]
679    #[case::missing_msp(vec![None])]
680    #[case::missing_lower(vec![Some(msp()), None])]
681    #[case::gap_in_lower(vec![Some(msp()), None, Some(lower_part())])]
682    fn test_rejects_missing_slots(#[case] slots: Vec<Option<ArrayRef>>) {
683        let parts = ArrayParts::new(
684            DecimalByteParts,
685            DType::Decimal(DecimalDType::new(76, 2), Nullability::NonNullable),
686            3,
687            DecimalBytePartsData,
688            slots.into_iter().collect(),
689        );
690        assert!(Array::try_from_parts(parts).is_err());
691    }
692
693    #[test]
694    fn test_wide_decimal_buffer_types() -> VortexResult<()> {
695        let mut ctx = array_session().create_execution_ctx();
696
697        let i128_array = i128_parts(vec![1i128 << 100], Validity::NonNullable);
698        let canonical = i128_array.into_array().execute::<DecimalArray>(&mut ctx)?;
699        assert_eq!(canonical.values_type(), DecimalType::I128);
700
701        let i256_array = i256_parts(vec![i256::from_parts(0, 1 << 100)], Validity::NonNullable);
702        let canonical = i256_array.into_array().execute::<DecimalArray>(&mut ctx)?;
703        assert_eq!(canonical.values_type(), DecimalType::I256);
704
705        // A narrow MSP with a single lower part still fits 128 bits.
706        let array = DecimalByteParts::try_new_with_lower_parts(
707            buffer![1i8, -1, 0].into_array(),
708            vec![buffer![7u64, 7, 7].into_array()],
709            DecimalDType::new(38, 2),
710        )?;
711        let canonical = array.into_array().execute::<DecimalArray>(&mut ctx)?;
712        assert_eq!(canonical.values_type(), DecimalType::I128);
713        assert_eq!(
714            canonical.buffer::<i128>().as_slice(),
715            &[(1i128 << 64) | 7, (-1i128 << 64) | 7, 7]
716        );
717
718        // Two lower parts under a narrow MSP overflow 128 bits, so the value widens.
719        let array = DecimalByteParts::try_new_with_lower_parts(
720            buffer![1i8].into_array(),
721            vec![buffer![0u64].into_array(), buffer![9u64].into_array()],
722            DecimalDType::new(76, 2),
723        )?;
724        let canonical = array.into_array().execute::<DecimalArray>(&mut ctx)?;
725        assert_eq!(canonical.values_type(), DecimalType::I256);
726        assert_eq!(
727            canonical.buffer::<i256>().as_slice(),
728            &[i256::from_parts(9, 1)]
729        );
730        Ok(())
731    }
732
733    #[test]
734    fn test_unused_buffer_of_values_is_ignored_for_null_rows() -> VortexResult<()> {
735        // Null rows may hold arbitrary bits in the lower parts; they must stay null.
736        let array = DecimalByteParts::try_new_with_lower_parts(
737            PrimitiveArray::new(
738                buffer![0i64, 0, 0],
739                Validity::Array(BoolArray::from_iter([false, false, true]).into_array()),
740            )
741            .into_array(),
742            vec![buffer![7u64, 9, 11].into_array()],
743            DecimalDType::new(38, 2),
744        )?
745        .into_array();
746
747        let mut ctx = array_session().create_execution_ctx();
748        assert_eq!(
749            array.execute_scalar(0, &mut ctx)?,
750            Scalar::null(array.dtype().clone())
751        );
752        assert_arrays_eq!(
753            array,
754            DecimalArray::new(
755                buffer![0i128, 0, 11],
756                DecimalDType::new(38, 2),
757                Validity::from_iter([false, false, true]),
758            ),
759            &mut ctx
760        );
761        Ok(())
762    }
763}