Skip to main content

vortex_alp/alp/
array.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Debug;
5use std::fmt::Display;
6use std::fmt::Formatter;
7use std::hash::Hash;
8use std::hash::Hasher;
9
10use prost::Message;
11use vortex_array::Array;
12use vortex_array::ArrayEq;
13use vortex_array::ArrayHash;
14use vortex_array::ArrayId;
15use vortex_array::ArrayParts;
16use vortex_array::ArrayRef;
17use vortex_array::ArraySlots;
18use vortex_array::ArrayView;
19use vortex_array::EqMode;
20use vortex_array::ExecutionCtx;
21use vortex_array::ExecutionResult;
22use vortex_array::IntoArray;
23use vortex_array::TypedArrayRef;
24use vortex_array::array_slots;
25use vortex_array::arrays::Primitive;
26use vortex_array::buffer::BufferHandle;
27use vortex_array::dtype::DType;
28use vortex_array::dtype::PType;
29use vortex_array::patches::PatchSlotIndices;
30use vortex_array::patches::Patches;
31use vortex_array::patches::PatchesData;
32use vortex_array::patches::PatchesMetadata;
33use vortex_array::require_child;
34use vortex_array::require_patches;
35use vortex_array::serde::ArrayChildren;
36use vortex_array::smallvec::smallvec;
37use vortex_array::vtable::VTable;
38use vortex_array::vtable::ValidityChild;
39use vortex_array::vtable::ValidityVTableFromChild;
40use vortex_error::VortexExpect;
41use vortex_error::VortexResult;
42use vortex_error::vortex_bail;
43use vortex_error::vortex_ensure;
44use vortex_error::vortex_panic;
45use vortex_session::VortexSession;
46use vortex_session::registry::CachedId;
47
48use crate::ALPFloat;
49use crate::alp::Exponents;
50use crate::alp::decompress::execute_decompress;
51use crate::alp::rules::RULES;
52
53/// A [`ALP`]-encoded Vortex array.
54pub type ALPArray = Array<ALP>;
55
56impl ArrayHash for ALPData {
57    fn array_hash<H: Hasher>(&self, state: &mut H, _accuracy: EqMode) {
58        self.exponents.hash(state);
59        self.patches_data.hash(state);
60    }
61}
62
63impl ArrayEq for ALPData {
64    fn array_eq(&self, other: &Self, _accuracy: EqMode) -> bool {
65        self.exponents == other.exponents && self.patches_data == other.patches_data
66    }
67}
68
69impl VTable for ALP {
70    type TypedArrayData = ALPData;
71
72    type OperationsVTable = Self;
73    type ValidityVTable = ValidityVTableFromChild;
74
75    fn id(&self) -> ArrayId {
76        static ID: CachedId = CachedId::new("vortex.alp");
77        *ID
78    }
79
80    fn validate(
81        &self,
82        data: &ALPData,
83        dtype: &DType,
84        len: usize,
85        slots: &[Option<ArrayRef>],
86    ) -> VortexResult<()> {
87        let alp_slots = ALPSlotsView::from_slots(slots);
88        let patches =
89            PatchesData::patches_from_slots(data.patches_data.as_ref(), len, slots, PATCH_SLOTS);
90        validate_parts(dtype, len, data.exponents, alp_slots.encoded, patches)
91    }
92
93    fn nbuffers(_array: ArrayView<'_, Self>) -> usize {
94        0
95    }
96
97    fn buffer(_array: ArrayView<'_, Self>, idx: usize) -> BufferHandle {
98        vortex_panic!("ALPArray buffer index {idx} out of bounds")
99    }
100
101    fn buffer_name(_array: ArrayView<'_, Self>, _idx: usize) -> Option<String> {
102        None
103    }
104
105    fn with_buffers(
106        &self,
107        array: ArrayView<'_, Self>,
108        buffers: &[BufferHandle],
109    ) -> VortexResult<ArrayParts<Self>> {
110        vortex_array::vtable::with_empty_buffers(self, array, buffers)
111    }
112
113    fn serialize(
114        array: ArrayView<'_, Self>,
115        _session: &VortexSession,
116    ) -> VortexResult<Option<Vec<u8>>> {
117        let exponents = array.exponents();
118        Ok(Some(
119            ALPMetadata {
120                exp_e: exponents.e as u32,
121                exp_f: exponents.f as u32,
122                patches: array
123                    .patches()
124                    .map(|p| p.to_metadata(array.len(), array.dtype()))
125                    .transpose()?,
126            }
127            .encode_to_vec(),
128        ))
129    }
130
131    fn deserialize(
132        &self,
133        dtype: &DType,
134        len: usize,
135        metadata: &[u8],
136        _buffers: &[BufferHandle],
137        children: &dyn ArrayChildren,
138        _session: &VortexSession,
139    ) -> VortexResult<ArrayParts<Self>> {
140        let metadata = ALPMetadata::decode(metadata)?;
141        let encoded_ptype = match &dtype {
142            DType::Primitive(PType::F32, n) => DType::Primitive(PType::I32, *n),
143            DType::Primitive(PType::F64, n) => DType::Primitive(PType::I64, *n),
144            d => vortex_bail!(MismatchedTypes: "f32 or f64", d),
145        };
146        let encoded = children.get(0, &encoded_ptype, len)?;
147
148        let patches = metadata
149            .patches
150            .map(|p| {
151                let indices = children.get(1, &p.indices_dtype()?, p.len()?)?;
152                let values = children.get(2, dtype, p.len()?)?;
153                let chunk_offsets = p
154                    .chunk_offsets_dtype()?
155                    .map(|dtype| children.get(3, &dtype, usize::try_from(p.chunk_offsets_len())?))
156                    .transpose()?;
157
158                Patches::new(len, p.offset()?, indices, values, chunk_offsets)
159            })
160            .transpose()?;
161
162        let slots = ALPData::make_slots(&encoded, patches.as_ref());
163        let data = ALPData::new(
164            Exponents {
165                e: u8::try_from(metadata.exp_e)?,
166                f: u8::try_from(metadata.exp_f)?,
167            },
168            patches,
169        );
170        Ok(ArrayParts::new(self.clone(), dtype.clone(), len, data).with_slots(slots))
171    }
172
173    fn slot_name(_array: ArrayView<'_, Self>, idx: usize) -> String {
174        ALPSlots::NAMES[idx].to_string()
175    }
176
177    fn execute(array: Array<Self>, ctx: &mut ExecutionCtx) -> VortexResult<ExecutionResult> {
178        let array = require_child!(array, array.encoded(), ALPSlots::ENCODED => Primitive);
179        require_patches!(
180            array,
181            ALPSlots::PATCH_INDICES,
182            ALPSlots::PATCH_VALUES,
183            ALPSlots::PATCH_CHUNK_OFFSETS
184        );
185
186        Ok(ExecutionResult::done(
187            execute_decompress(array, ctx)?.into_array(),
188        ))
189    }
190
191    fn reduce_parent(
192        array: ArrayView<'_, Self>,
193        parent: &ArrayRef,
194        child_idx: usize,
195    ) -> VortexResult<Option<ArrayRef>> {
196        RULES.evaluate(array, parent, child_idx)
197    }
198}
199
200#[array_slots(ALP)]
201pub struct ALPSlots {
202    /// The ALP-encoded values array.
203    pub encoded: ArrayRef,
204    /// The indices of exception values that could not be ALP-encoded.
205    pub patch_indices: Option<ArrayRef>,
206    /// The exception values that could not be ALP-encoded.
207    pub patch_values: Option<ArrayRef>,
208    /// Chunk offsets for the patch indices/values.
209    pub patch_chunk_offsets: Option<ArrayRef>,
210}
211
212const PATCH_SLOTS: PatchSlotIndices = PatchSlotIndices {
213    indices: ALPSlots::PATCH_INDICES,
214    values: ALPSlots::PATCH_VALUES,
215    chunk_offsets: ALPSlots::PATCH_CHUNK_OFFSETS,
216};
217
218#[derive(Clone, Debug)]
219pub struct ALPData {
220    patches_data: Option<PatchesData>,
221    exponents: Exponents,
222}
223
224impl Display for ALPData {
225    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
226        write!(f, "exponents: {}", self.exponents)?;
227        if let Some(pd) = &self.patches_data {
228            write!(f, ", patch_offset: {}", pd.offset())?;
229        }
230        Ok(())
231    }
232}
233
234#[derive(Clone, Debug)]
235pub struct ALP;
236
237#[derive(Clone, prost::Message)]
238pub struct ALPMetadata {
239    #[prost(uint32, tag = "1")]
240    pub(crate) exp_e: u32,
241    #[prost(uint32, tag = "2")]
242    pub(crate) exp_f: u32,
243    #[prost(message, optional, tag = "3")]
244    pub(crate) patches: Option<PatchesMetadata>,
245}
246
247impl ALPData {
248    fn validate_components(
249        encoded: &ArrayRef,
250        exponents: Exponents,
251        patches: Option<&Patches>,
252    ) -> VortexResult<()> {
253        vortex_ensure!(
254            matches!(
255                encoded.dtype(),
256                DType::Primitive(PType::I32 | PType::I64, _)
257            ),
258            "ALP encoded ints have invalid DType {}",
259            encoded.dtype(),
260        );
261
262        // Validate exponents are in-bounds for the float, and that patches have the proper
263        // length and type.
264        let Exponents { e, f } = exponents;
265        match encoded.dtype().as_ptype() {
266            PType::I32 => {
267                vortex_ensure!(exponents.e <= f32::MAX_EXPONENT, "e out of bounds: {e}");
268                vortex_ensure!(exponents.f <= f32::MAX_EXPONENT, "f out of bounds: {f}");
269                if let Some(patches) = patches {
270                    Self::validate_patches::<f32>(patches, encoded)?;
271                }
272            }
273            PType::I64 => {
274                vortex_ensure!(e <= f64::MAX_EXPONENT, "e out of bounds: {e}");
275                vortex_ensure!(f <= f64::MAX_EXPONENT, "f out of bounds: {f}");
276
277                if let Some(patches) = patches {
278                    Self::validate_patches::<f64>(patches, encoded)?;
279                }
280            }
281            _ => unreachable!(),
282        }
283
284        // Validate patches
285        if let Some(patches) = patches {
286            vortex_ensure!(
287                patches.array_len() == encoded.len(),
288                "patches array_len != encoded len: {} != {}",
289                patches.array_len(),
290                encoded.len()
291            );
292
293            // Verify that the patches DType are of the proper DType.
294        }
295
296        Ok(())
297    }
298
299    fn logical_dtype(encoded: &ArrayRef) -> VortexResult<DType> {
300        match encoded.dtype() {
301            DType::Primitive(PType::I32, nullability) => {
302                Ok(DType::Primitive(PType::F32, *nullability))
303            }
304            DType::Primitive(PType::I64, nullability) => {
305                Ok(DType::Primitive(PType::F64, *nullability))
306            }
307            _ => vortex_bail!("ALP encoded ints have invalid DType {}", encoded.dtype(),),
308        }
309    }
310
311    /// Validate that any patches provided are valid for the ALPArray.
312    fn validate_patches<T: ALPFloat>(patches: &Patches, encoded: &ArrayRef) -> VortexResult<()> {
313        vortex_ensure!(
314            patches.array_len() == encoded.len(),
315            "patches array_len != encoded len: {} != {}",
316            patches.array_len(),
317            encoded.len()
318        );
319
320        let expected_type = DType::Primitive(T::PTYPE, encoded.dtype().nullability());
321        vortex_ensure!(
322            patches.dtype() == &expected_type,
323            "Expected patches type {expected_type}, actual {}",
324            patches.dtype(),
325        );
326
327        Ok(())
328    }
329}
330
331impl ALPData {
332    /// Build a new `ALPArray` from components, panicking on validation failure.
333    ///
334    /// See [`ALP::try_new`] for reference on preconditions that must pass before
335    /// calling this method.
336    pub fn new(exponents: Exponents, patches: Option<Patches>) -> Self {
337        Self {
338            patches_data: patches.as_ref().map(PatchesData::from_patches),
339            exponents,
340        }
341    }
342
343    /// Build a new `ALPArray` from components:
344    ///
345    /// * `encoded` contains the ALP-encoded ints. Any null values are replaced with placeholders
346    /// * `exponents` are the ALP exponents, valid range depends on the data type
347    /// * `patches` are any patch values that don't cleanly encode using the ALP conversion function
348    ///
349    /// Build a new `ALPArray` from components without validation.
350    ///
351    /// See [`ALP::try_new`] for information about the preconditions that should be checked
352    /// **before** calling this method.
353    pub(crate) unsafe fn new_unchecked(exponents: Exponents, patches: Option<Patches>) -> Self {
354        Self::new(exponents, patches)
355    }
356}
357
358/// Constructors for [`ALPArray`].
359impl ALP {
360    pub fn new(encoded: ArrayRef, exponents: Exponents, patches: Option<Patches>) -> ALPArray {
361        let dtype = ALPData::logical_dtype(&encoded).vortex_expect("ALP encoded dtype");
362        let len = encoded.len();
363        let slots = ALPData::make_slots(&encoded, patches.as_ref());
364        unsafe {
365            Array::from_parts_unchecked(
366                ArrayParts::new(ALP, dtype, len, ALPData::new(exponents, patches))
367                    .with_slots(slots),
368            )
369        }
370    }
371
372    pub fn try_new(
373        encoded: ArrayRef,
374        exponents: Exponents,
375        patches: Option<Patches>,
376    ) -> VortexResult<ALPArray> {
377        let dtype = ALPData::logical_dtype(&encoded)?;
378        let len = encoded.len();
379        let slots = ALPData::make_slots(&encoded, patches.as_ref());
380        let data = ALPData::new(exponents, patches);
381        Array::try_from_parts(ArrayParts::new(ALP, dtype, len, data).with_slots(slots))
382    }
383
384    /// # Safety
385    /// See [`ALP::try_new`] for preconditions.
386    pub unsafe fn new_unchecked(
387        encoded: ArrayRef,
388        exponents: Exponents,
389        patches: Option<Patches>,
390    ) -> ALPArray {
391        let dtype = ALPData::logical_dtype(&encoded).vortex_expect("ALP encoded dtype");
392        let len = encoded.len();
393        let slots = ALPData::make_slots(&encoded, patches.as_ref());
394        let data = unsafe { ALPData::new_unchecked(exponents, patches) };
395        unsafe {
396            Array::from_parts_unchecked(ArrayParts::new(ALP, dtype, len, data).with_slots(slots))
397        }
398    }
399}
400
401impl ALPData {
402    fn make_slots(encoded: &ArrayRef, patches: Option<&Patches>) -> ArraySlots {
403        let mut slots: ArraySlots = smallvec![Some(encoded.clone())];
404        PatchesData::push_slots(&mut slots, patches);
405        slots
406    }
407
408    #[inline]
409    pub fn exponents(&self) -> Exponents {
410        self.exponents
411    }
412}
413
414pub trait ALPArrayExt: ALPArraySlotsExt {
415    fn exponents(&self) -> Exponents {
416        self.exponents
417    }
418
419    fn patches(&self) -> Option<Patches> {
420        PatchesData::patches_from_slots(
421            self.patches_data.as_ref(),
422            self.as_ref().len(),
423            self.as_ref().slots(),
424            PATCH_SLOTS,
425        )
426    }
427}
428
429fn validate_parts(
430    dtype: &DType,
431    len: usize,
432    exponents: Exponents,
433    encoded: &ArrayRef,
434    patches: Option<Patches>,
435) -> VortexResult<()> {
436    let logical_dtype = ALPData::logical_dtype(encoded)?;
437    ALPData::validate_components(encoded, exponents, patches.as_ref())?;
438    vortex_ensure!(
439        encoded.len() == len,
440        "ALP encoded len {} != outer len {len}",
441        encoded.len(),
442    );
443    vortex_ensure!(
444        &logical_dtype == dtype,
445        "ALP dtype {} does not match encoded logical dtype {}",
446        dtype,
447        logical_dtype,
448    );
449    Ok(())
450}
451
452impl<T: TypedArrayRef<ALP>> ALPArrayExt for T {}
453
454pub trait ALPArrayOwnedExt {
455    fn into_parts(self) -> (ArrayRef, Exponents, Option<Patches>);
456}
457
458impl ALPArrayOwnedExt for Array<ALP> {
459    #[inline]
460    fn into_parts(self) -> (ArrayRef, Exponents, Option<Patches>) {
461        let patches = self.patches();
462        let exponents = self.exponents();
463        let encoded = self.encoded().clone();
464        (encoded, exponents, patches)
465    }
466}
467
468impl ValidityChild<ALP> for ALP {
469    fn validity_child(array: ArrayView<'_, ALP>) -> ArrayRef {
470        array.encoded().clone()
471    }
472}
473
474#[cfg(test)]
475mod tests {
476    use std::f64::consts::PI;
477    use std::sync::LazyLock;
478
479    use rstest::rstest;
480    use vortex_array::Canonical;
481    use vortex_array::IntoArray;
482    use vortex_array::VortexSessionExecute;
483    use vortex_array::arrays::PrimitiveArray;
484    use vortex_array::assert_arrays_eq;
485    use vortex_error::VortexExpect;
486    use vortex_session::VortexSession;
487
488    use super::*;
489    use crate::alp_encode;
490    use crate::decompress_into_array;
491
492    static SESSION: LazyLock<VortexSession> = LazyLock::new(|| {
493        let session = vortex_array::array_session();
494        crate::initialize(&session);
495        session
496    });
497
498    #[rstest]
499    #[case(0)]
500    #[case(1)]
501    #[case(100)]
502    #[case(1023)]
503    #[case(1024)]
504    #[case(1025)]
505    #[case(2047)]
506    #[case(2048)]
507    #[case(2049)]
508    fn test_execute_f32(#[case] size: usize) {
509        let mut ctx = SESSION.create_execution_ctx();
510        let values = PrimitiveArray::from_iter((0..size).map(|i| i as f32));
511        let encoded = alp_encode(values.as_view(), None, &mut ctx).unwrap();
512
513        let result_canonical = {
514            encoded
515                .clone()
516                .into_array()
517                .execute::<Canonical>(&mut ctx)
518                .unwrap()
519        };
520        // Compare against the traditional array-based decompress path
521        let expected = decompress_into_array(encoded, &mut SESSION.create_execution_ctx()).unwrap();
522
523        assert_arrays_eq!(result_canonical.into_array(), expected, &mut ctx);
524    }
525
526    #[rstest]
527    #[case(0)]
528    #[case(1)]
529    #[case(100)]
530    #[case(1023)]
531    #[case(1024)]
532    #[case(1025)]
533    #[case(2047)]
534    #[case(2048)]
535    #[case(2049)]
536    fn test_execute_f64(#[case] size: usize) {
537        let values = PrimitiveArray::from_iter((0..size).map(|i| i as f64));
538        let encoded =
539            alp_encode(values.as_view(), None, &mut SESSION.create_execution_ctx()).unwrap();
540
541        let mut ctx = SESSION.create_execution_ctx();
542        let result_canonical = encoded
543            .clone()
544            .into_array()
545            .execute::<Canonical>(&mut ctx)
546            .unwrap();
547        // Compare against the traditional array-based decompress path
548        let expected = decompress_into_array(encoded, &mut SESSION.create_execution_ctx()).unwrap();
549
550        assert_arrays_eq!(result_canonical.into_array(), expected, &mut ctx);
551    }
552
553    #[rstest]
554    #[case(100)]
555    #[case(1023)]
556    #[case(1024)]
557    #[case(1025)]
558    #[case(2047)]
559    #[case(2048)]
560    #[case(2049)]
561    fn test_execute_with_patches(#[case] size: usize) {
562        let values: Vec<f64> = (0..size)
563            .map(|i| match i % 4 {
564                0..=2 => 1.0,
565                _ => PI,
566            })
567            .collect();
568
569        let array = PrimitiveArray::from_iter(values);
570        let encoded =
571            alp_encode(array.as_view(), None, &mut SESSION.create_execution_ctx()).unwrap();
572        assert!(encoded.patches().unwrap().array_len() > 0);
573
574        let mut ctx = SESSION.create_execution_ctx();
575        let result_canonical = encoded
576            .clone()
577            .into_array()
578            .execute::<Canonical>(&mut ctx)
579            .unwrap();
580        // Compare against the traditional array-based decompress path
581        let expected = decompress_into_array(encoded, &mut SESSION.create_execution_ctx()).unwrap();
582
583        assert_arrays_eq!(result_canonical.into_array(), expected, &mut ctx);
584    }
585
586    #[rstest]
587    #[case(0)]
588    #[case(1)]
589    #[case(100)]
590    #[case(1023)]
591    #[case(1024)]
592    #[case(1025)]
593    #[case(2047)]
594    #[case(2048)]
595    #[case(2049)]
596    fn test_execute_with_validity(#[case] size: usize) {
597        let values: Vec<Option<f32>> = (0..size)
598            .map(|i| if i % 2 == 1 { None } else { Some(1.0) })
599            .collect();
600
601        let array = PrimitiveArray::from_option_iter(values);
602        let encoded =
603            alp_encode(array.as_view(), None, &mut SESSION.create_execution_ctx()).unwrap();
604
605        let mut ctx = SESSION.create_execution_ctx();
606        let result_canonical = encoded
607            .clone()
608            .into_array()
609            .execute::<Canonical>(&mut ctx)
610            .unwrap();
611        // Compare against the traditional array-based decompress path
612        let expected = decompress_into_array(encoded, &mut SESSION.create_execution_ctx()).unwrap();
613
614        assert_arrays_eq!(result_canonical.into_array(), expected, &mut ctx);
615    }
616
617    #[rstest]
618    #[case(100)]
619    #[case(1023)]
620    #[case(1024)]
621    #[case(1025)]
622    #[case(2047)]
623    #[case(2048)]
624    #[case(2049)]
625    fn test_execute_with_patches_and_validity(#[case] size: usize) {
626        let values: Vec<Option<f64>> = (0..size)
627            .map(|idx| match idx % 3 {
628                0 => Some(1.0),
629                1 => None,
630                _ => Some(PI),
631            })
632            .collect();
633
634        let array = PrimitiveArray::from_option_iter(values);
635        let encoded =
636            alp_encode(array.as_view(), None, &mut SESSION.create_execution_ctx()).unwrap();
637        assert!(encoded.patches().unwrap().array_len() > 0);
638
639        let mut ctx = SESSION.create_execution_ctx();
640        let result_canonical = encoded
641            .clone()
642            .into_array()
643            .execute::<Canonical>(&mut ctx)
644            .unwrap();
645        // Compare against the traditional array-based decompress path
646        let expected = decompress_into_array(encoded, &mut SESSION.create_execution_ctx()).unwrap();
647
648        assert_arrays_eq!(result_canonical.into_array(), expected, &mut ctx);
649    }
650
651    #[rstest]
652    #[case(500, 100)]
653    #[case(1000, 200)]
654    #[case(2048, 512)]
655    fn test_execute_sliced_vector(#[case] size: usize, #[case] slice_start: usize) {
656        let values: Vec<Option<f64>> = (0..size)
657            .map(|i| {
658                if i % 5 == 0 {
659                    None
660                } else if i % 4 == 3 {
661                    Some(PI)
662                } else {
663                    Some(1.0)
664                }
665            })
666            .collect();
667
668        let mut ctx = SESSION.create_execution_ctx();
669        let array = PrimitiveArray::from_option_iter(values.clone());
670        let encoded = alp_encode(array.as_view(), None, &mut ctx).unwrap();
671
672        let slice_end = size - slice_start;
673        let slice_len = slice_end - slice_start;
674        let sliced_encoded = encoded.slice(slice_start..slice_end).unwrap();
675
676        let result_canonical = sliced_encoded.execute::<Canonical>(&mut ctx).unwrap();
677        let result_primitive = result_canonical.into_primitive();
678
679        for idx in 0..slice_len {
680            let expected_value = values[slice_start + idx];
681
682            let result_valid = result_primitive
683                .validity()
684                .vortex_expect("result validity should be derivable")
685                .execute_is_valid(idx, &mut ctx)
686                .unwrap();
687            assert_eq!(
688                result_valid,
689                expected_value.is_some(),
690                "Validity mismatch at idx={idx}",
691            );
692
693            if let Some(expected_val) = expected_value {
694                let result_val = result_primitive.as_slice::<f64>()[idx];
695                assert_eq!(result_val, expected_val, "Value mismatch at idx={idx}",);
696            }
697        }
698    }
699
700    #[rstest]
701    #[case(500, 100)]
702    #[case(1000, 200)]
703    #[case(2048, 512)]
704    fn test_sliced_to_primitive(#[case] size: usize, #[case] slice_start: usize) {
705        let mut ctx = SESSION.create_execution_ctx();
706        let values: Vec<Option<f64>> = (0..size)
707            .map(|i| {
708                if i % 5 == 0 {
709                    None
710                } else if i % 4 == 3 {
711                    Some(PI)
712                } else {
713                    Some(1.0)
714                }
715            })
716            .collect();
717
718        let array = PrimitiveArray::from_option_iter(values.clone());
719        let encoded = alp_encode(array.as_view(), None, &mut ctx).unwrap();
720
721        let slice_end = size - slice_start;
722        let slice_len = slice_end - slice_start;
723        let sliced_encoded = encoded.slice(slice_start..slice_end).unwrap();
724
725        let result_primitive = sliced_encoded.execute::<PrimitiveArray>(&mut ctx).unwrap();
726
727        for idx in 0..slice_len {
728            let expected_value = values[slice_start + idx];
729
730            let result_valid = result_primitive
731                .as_ref()
732                .validity()
733                .unwrap()
734                .execute_mask(result_primitive.as_ref().len(), &mut ctx)
735                .unwrap()
736                .value(idx);
737            assert_eq!(
738                result_valid,
739                expected_value.is_some(),
740                "Validity mismatch at idx={idx}",
741            );
742
743            if let Some(expected_val) = expected_value {
744                let buf = result_primitive.to_buffer::<f64>();
745                let result_val = buf.as_slice()[idx];
746                assert_eq!(result_val, expected_val, "Value mismatch at idx={idx}",);
747            }
748        }
749    }
750
751    /// Regression test for issue #5948: execute_decompress drops patches when chunk_offsets is
752    /// None.
753    ///
754    /// When patches exist but do NOT have chunk_offsets, the execute path incorrectly passes
755    /// `None` to `decompress_unchunked_core` instead of the actual patches.
756    ///
757    /// This can happen after file IO serialization/deserialization where chunk_offsets may not
758    /// be preserved, or when building ALPArrays manually without chunk_offsets.
759    #[test]
760    fn test_execute_decompress_with_patches_no_chunk_offsets_regression_5948() {
761        // Create an array with values that will produce patches. PI doesn't encode cleanly.
762        let values: Vec<f64> = vec![1.0, 2.0, PI, 4.0, 5.0];
763        let original = PrimitiveArray::from_iter(values);
764
765        // First encode normally to get a properly formed ALPArray with patches.
766        let normally_encoded = alp_encode(
767            original.as_view(),
768            None,
769            &mut SESSION.create_execution_ctx(),
770        )
771        .unwrap();
772        assert!(
773            normally_encoded.patches().is_some(),
774            "Test requires patches to be present"
775        );
776
777        let original_patches = normally_encoded.patches().unwrap();
778        assert!(
779            original_patches.chunk_offsets().is_some(),
780            "Normal encoding should have chunk_offsets"
781        );
782
783        // Rebuild the patches WITHOUT chunk_offsets to simulate deserialized patches.
784        let patches_without_chunk_offsets = Patches::new(
785            original_patches.array_len(),
786            original_patches.offset(),
787            original_patches.indices().clone(),
788            original_patches.values().clone(),
789            None, // NO chunk_offsets - this triggers the bug!
790        )
791        .unwrap();
792
793        // Build a new ALPArray with the same encoded data but patches without chunk_offsets.
794        let alp_without_chunk_offsets = ALP::new(
795            normally_encoded.encoded().clone(),
796            normally_encoded.exponents(),
797            Some(patches_without_chunk_offsets),
798        );
799
800        // The legacy decompress_into_array path should work correctly.
801        let result_legacy = decompress_into_array(
802            alp_without_chunk_offsets.clone(),
803            &mut SESSION.create_execution_ctx(),
804        )
805        .unwrap();
806        let legacy_slice = result_legacy.as_slice::<f64>();
807
808        // Verify the legacy path produces correct values.
809        assert!(
810            (legacy_slice[2] - PI).abs() < 1e-10,
811            "Legacy path should have PI at index 2, got {}",
812            legacy_slice[2]
813        );
814
815        // The execute path has the bug - it drops patches when chunk_offsets is None.
816        let result_execute = {
817            let mut ctx = SESSION.create_execution_ctx();
818            execute_decompress(alp_without_chunk_offsets, &mut ctx).unwrap()
819        };
820        let execute_slice = result_execute.as_slice::<f64>();
821
822        // This assertion FAILS until the bug is fixed because execute_decompress drops patches.
823        assert!(
824            (execute_slice[2] - PI).abs() < 1e-10,
825            "Execute path should have PI at index 2, but got {} (patches were dropped!)",
826            execute_slice[2]
827        );
828    }
829}