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