Skip to main content

vortex_alp/alp/
compress.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use ::alp::ENCODE_CHUNK_SIZE;
5use itertools::Itertools;
6use vortex_array::ArrayRef;
7use vortex_array::ArrayView;
8use vortex_array::ExecutionCtx;
9use vortex_array::IntoArray;
10use vortex_array::arrays::Primitive;
11use vortex_array::arrays::PrimitiveArray;
12use vortex_array::dtype::NativePType;
13use vortex_array::dtype::PType;
14use vortex_array::patches::Patches;
15use vortex_array::validity::Validity;
16use vortex_buffer::Buffer;
17use vortex_buffer::BufferMut;
18use vortex_error::VortexResult;
19use vortex_error::vortex_bail;
20use vortex_mask::Mask;
21
22use crate::ALP;
23use crate::Exponents;
24use crate::alp::ALPArray;
25use crate::alp::ALPFloat;
26
27#[macro_export]
28macro_rules! match_each_alp_float_ptype {
29    ($self:expr, | $enc:ident | $body:block) => {{
30        use vortex_array::dtype::PType;
31        use vortex_error::vortex_panic;
32        let ptype = $self;
33        match ptype {
34            PType::F32 => {
35                type $enc = f32;
36                $body
37            }
38            PType::F64 => {
39                type $enc = f64;
40                $body
41            }
42            _ => vortex_panic!("ALP can only encode f32 and f64, got {}", ptype),
43        }
44    }};
45}
46
47pub fn alp_encode(
48    parray: ArrayView<'_, Primitive>,
49    exponents: Option<Exponents>,
50    ctx: &mut ExecutionCtx,
51) -> VortexResult<ALPArray> {
52    let (exponents, encoded, patches) = match parray.ptype() {
53        PType::F32 => alp_encode_components_typed::<f32>(parray, exponents, ctx)?,
54        PType::F64 => alp_encode_components_typed::<f64>(parray, exponents, ctx)?,
55        _ => vortex_bail!("ALP can only encode f32 and f64"),
56    };
57
58    // SAFETY: alp_encode_components_typed must return well-formed components
59    unsafe { Ok(ALP::new_unchecked(encoded, exponents, patches)) }
60}
61
62#[expect(
63    clippy::cast_possible_truncation,
64    reason = "u64 index cast to usize is safe for reasonable array sizes"
65)]
66fn alp_encode_components_typed<T>(
67    values: ArrayView<'_, Primitive>,
68    exponents: Option<Exponents>,
69    ctx: &mut ExecutionCtx,
70) -> VortexResult<(Exponents, ArrayRef, Option<Patches>)>
71where
72    T: ALPFloat + NativePType,
73    T::ALPInt: NativePType,
74{
75    let values_slice = values.as_slice::<T>();
76
77    // Encode straight into a Vortex buffer, so that the encoded array owns its values rather than
78    // adopting a `Vec` allocated by the encoder.
79    let mut encoded = BufferMut::<T::ALPInt>::with_capacity(values_slice.len());
80
81    // Estimate capacity to be one patch per 32 values.
82    let mut exceptional_positions = Vec::with_capacity(values_slice.len() / 32);
83    let mut exceptional_values = Vec::with_capacity(values_slice.len() / 32);
84
85    // There's exactly one offset per chunk.
86    let mut chunk_offsets = Vec::with_capacity(values_slice.len().div_ceil(ENCODE_CHUNK_SIZE));
87    let exponents = ::alp::encode_into(
88        values_slice,
89        exponents,
90        &mut encoded.spare_capacity_mut()[..values_slice.len()],
91        &mut exceptional_positions,
92        &mut exceptional_values,
93        &mut chunk_offsets,
94    );
95    // SAFETY: `encode_into` initializes exactly one element per value.
96    unsafe { encoded.set_len(values_slice.len()) };
97
98    let encoded_array = PrimitiveArray::new(encoded.freeze(), values.validity()?).into_array();
99
100    let validity = values
101        .array()
102        .validity()?
103        .execute_mask(values.array().len(), ctx)?;
104    // exceptional_positions may contain exceptions at invalid positions (which contain garbage
105    // data). We remove null exceptions in order to keep the Patches small.
106    let (valid_exceptional_positions, valid_exceptional_values): (Vec<u64>, Vec<T>) = match validity
107    {
108        Mask::AllTrue(_) => (exceptional_positions, exceptional_values),
109        Mask::AllFalse(_) => {
110            // no valid positions, ergo nothing worth patching
111            (Vec::new(), Vec::new())
112        }
113        Mask::Values(is_valid) => exceptional_positions
114            .into_iter()
115            .zip_eq(exceptional_values)
116            .filter(|(index, _)| {
117                let is_valid = is_valid.value(*index as usize);
118                if !is_valid {
119                    let patch_chunk = *index as usize / ENCODE_CHUNK_SIZE;
120                    for chunk_idx in (patch_chunk + 1)..chunk_offsets.len() {
121                        chunk_offsets[chunk_idx] -= 1;
122                    }
123                }
124                is_valid
125            })
126            .unzip(),
127    };
128    let patches = if valid_exceptional_positions.is_empty() {
129        None
130    } else {
131        let patches_validity = if values.dtype().is_nullable() {
132            Validity::AllValid
133        } else {
134            Validity::NonNullable
135        };
136        let valid_exceptional_values =
137            PrimitiveArray::new(Buffer::from(valid_exceptional_values), patches_validity)
138                .into_array();
139
140        Some(Patches::new(
141            values_slice.len(),
142            0,
143            Buffer::from(valid_exceptional_positions).into_array(),
144            valid_exceptional_values,
145            Some(Buffer::from(chunk_offsets).into_array()),
146        )?)
147    };
148    Ok((exponents, encoded_array, patches))
149}
150
151#[cfg(test)]
152mod tests {
153    use core::f32;
154    use core::f64;
155    use std::sync::LazyLock;
156
157    use f64::consts::E;
158    use f64::consts::PI;
159    use vortex_array::VortexSessionExecute;
160    use vortex_array::assert_arrays_eq;
161    use vortex_array::dtype::NativePType;
162    use vortex_array::validity::Validity;
163    use vortex_buffer::Buffer;
164    use vortex_buffer::buffer;
165    use vortex_session::VortexSession;
166
167    use super::*;
168    use crate::alp::array::ALPArrayExt;
169    use crate::alp::array::ALPArraySlotsExt;
170    use crate::decompress_into_array;
171
172    static SESSION: LazyLock<VortexSession> = LazyLock::new(|| {
173        let session = vortex_array::array_session();
174        crate::initialize(&session);
175        session
176    });
177
178    #[test]
179    fn test_compress() {
180        let array = PrimitiveArray::new(buffer![1.234f32; 1025], Validity::NonNullable);
181        let encoded =
182            alp_encode(array.as_view(), None, &mut SESSION.create_execution_ctx()).unwrap();
183        assert!(encoded.patches().is_none());
184        let expected_encoded = PrimitiveArray::from_iter(vec![1234i32; 1025]);
185        assert_arrays_eq!(
186            encoded.encoded(),
187            expected_encoded,
188            &mut SESSION.create_execution_ctx()
189        );
190        assert_eq!(encoded.exponents(), Exponents { e: 9, f: 6 });
191
192        let decoded = decompress_into_array(encoded, &mut SESSION.create_execution_ctx()).unwrap();
193        assert_arrays_eq!(decoded, array, &mut SESSION.create_execution_ctx());
194    }
195
196    #[test]
197    fn test_nullable_compress() {
198        let array = PrimitiveArray::from_option_iter([None, Some(1.234f32), None]);
199        let encoded =
200            alp_encode(array.as_view(), None, &mut SESSION.create_execution_ctx()).unwrap();
201        assert!(encoded.patches().is_none());
202        let expected_encoded = PrimitiveArray::from_option_iter([None, Some(1234i32), None]);
203        assert_arrays_eq!(
204            encoded.encoded(),
205            expected_encoded,
206            &mut SESSION.create_execution_ctx()
207        );
208        assert_eq!(encoded.exponents(), Exponents { e: 9, f: 6 });
209
210        let decoded = decompress_into_array(encoded, &mut SESSION.create_execution_ctx()).unwrap();
211        let expected = PrimitiveArray::from_option_iter(vec![None, Some(1.234f32), None]);
212        assert_arrays_eq!(decoded, expected, &mut SESSION.create_execution_ctx());
213    }
214
215    #[test]
216    #[expect(clippy::approx_constant)] // Clippy objects to 2.718, an approximation of e, the base of the natural logarithm.
217    fn test_patched_compress() {
218        let values = buffer![1.234f64, 2.718, PI, 4.0];
219        let array = PrimitiveArray::new(values.clone(), Validity::NonNullable);
220        let encoded =
221            alp_encode(array.as_view(), None, &mut SESSION.create_execution_ctx()).unwrap();
222        assert!(encoded.patches().is_some());
223        let expected_encoded = PrimitiveArray::from_iter(vec![1234i64, 2718, 1234, 4000]);
224        assert_arrays_eq!(
225            encoded.encoded(),
226            expected_encoded,
227            &mut SESSION.create_execution_ctx()
228        );
229        assert_eq!(encoded.exponents(), Exponents { e: 16, f: 13 });
230
231        let decoded = decompress_into_array(encoded, &mut SESSION.create_execution_ctx()).unwrap();
232        let expected_decoded = PrimitiveArray::new(values, Validity::NonNullable);
233        assert_arrays_eq!(
234            decoded,
235            expected_decoded,
236            &mut SESSION.create_execution_ctx()
237        );
238    }
239
240    #[test]
241    #[expect(clippy::approx_constant)] // Clippy objects to 2.718, an approximation of e, the base of the natural logarithm.
242    fn test_compress_ignores_invalid_exceptional_values() {
243        let values = buffer![1.234f64, 2.718, PI, 4.0];
244        let array = PrimitiveArray::new(values, Validity::from_iter([true, true, false, true]));
245        let encoded =
246            alp_encode(array.as_view(), None, &mut SESSION.create_execution_ctx()).unwrap();
247        assert!(encoded.patches().is_none());
248        let expected_encoded =
249            PrimitiveArray::from_option_iter(buffer![Some(1234i64), Some(2718), None, Some(4000)]);
250        assert_arrays_eq!(
251            encoded.encoded(),
252            expected_encoded,
253            &mut SESSION.create_execution_ctx()
254        );
255        assert_eq!(encoded.exponents(), Exponents { e: 16, f: 13 });
256
257        let decoded = decompress_into_array(encoded, &mut SESSION.create_execution_ctx()).unwrap();
258        assert_arrays_eq!(decoded, array, &mut SESSION.create_execution_ctx());
259    }
260
261    #[test]
262    #[expect(clippy::approx_constant)] // ALP doesn't like E
263    fn test_nullable_patched_scalar_at() {
264        let array = PrimitiveArray::from_option_iter([
265            Some(1.234f64),
266            Some(2.718),
267            Some(PI),
268            Some(4.0),
269            None,
270        ]);
271        let encoded =
272            alp_encode(array.as_view(), None, &mut SESSION.create_execution_ctx()).unwrap();
273        assert!(encoded.patches().is_some());
274
275        assert_eq!(encoded.exponents(), Exponents { e: 16, f: 13 });
276
277        assert_arrays_eq!(encoded, array, &mut SESSION.create_execution_ctx());
278
279        let _decoded = decompress_into_array(encoded, &mut SESSION.create_execution_ctx()).unwrap();
280    }
281
282    #[test]
283    fn roundtrips_close_fractional() {
284        let original = PrimitiveArray::from_iter([195.26274f32, 195.27837, -48.815685]);
285        let alp_arr = alp_encode(
286            original.as_view(),
287            None,
288            &mut SESSION.create_execution_ctx(),
289        )
290        .unwrap();
291        assert_arrays_eq!(alp_arr, original, &mut SESSION.create_execution_ctx());
292    }
293
294    #[test]
295    fn roundtrips_all_null() {
296        let mut ctx = SESSION.create_execution_ctx();
297        let original =
298            PrimitiveArray::new(buffer![195.26274f64, PI, -48.815685], Validity::AllInvalid);
299        let alp_arr = alp_encode(original.as_view(), None, &mut ctx).unwrap();
300        let decompressed = alp_arr
301            .into_array()
302            .execute::<PrimitiveArray>(&mut ctx)
303            .unwrap();
304
305        assert_eq!(
306            // The second and third values become exceptions and are replaced
307            [195.26274, 195.26274, 195.26274],
308            decompressed.as_slice::<f64>()
309        );
310
311        assert_arrays_eq!(decompressed, original, &mut ctx);
312    }
313
314    #[test]
315    fn non_finite_numbers() {
316        let mut ctx = SESSION.create_execution_ctx();
317        let original = PrimitiveArray::new(
318            buffer![0.0f32, -0.0, f32::NAN, f32::NEG_INFINITY, f32::INFINITY],
319            Validity::NonNullable,
320        );
321        let encoded = alp_encode(original.as_view(), None, &mut ctx).unwrap();
322        let decoded = encoded
323            .as_array()
324            .clone()
325            .execute::<PrimitiveArray>(&mut ctx)
326            .unwrap();
327        for idx in 0..original.len() {
328            let decoded_val = decoded.as_slice::<f32>()[idx];
329            let original_val = original.as_slice::<f32>()[idx];
330            assert!(
331                NativePType::is_eq(decoded_val, original_val),
332                "Expected {original_val} but got {decoded_val}"
333            );
334        }
335    }
336
337    #[test]
338    fn test_chunk_offsets() {
339        let mut ctx = SESSION.create_execution_ctx();
340        let mut values = vec![1.0f64; 3072];
341
342        values[1023] = PI;
343        values[1024] = E;
344        values[1025] = PI;
345
346        let array = PrimitiveArray::new(Buffer::from(values), Validity::NonNullable);
347        let encoded = alp_encode(array.as_view(), None, &mut ctx).unwrap();
348        let patches = encoded.patches().unwrap();
349
350        let chunk_offsets = patches
351            .chunk_offsets()
352            .clone()
353            .unwrap()
354            .execute::<PrimitiveArray>(&mut ctx)
355            .unwrap();
356        let expected_offsets = PrimitiveArray::from_iter(vec![0u64, 1, 3]);
357        assert_arrays_eq!(chunk_offsets, expected_offsets, &mut ctx);
358
359        let patch_indices = patches
360            .indices()
361            .clone()
362            .execute::<PrimitiveArray>(&mut ctx)
363            .unwrap();
364        let expected_indices = PrimitiveArray::from_iter(vec![1023u64, 1024, 1025]);
365        assert_arrays_eq!(patch_indices, expected_indices, &mut ctx);
366
367        let patch_values = patches
368            .values()
369            .clone()
370            .execute::<PrimitiveArray>(&mut ctx)
371            .unwrap();
372        let expected_values = PrimitiveArray::from_iter(vec![PI, E, PI]);
373        assert_arrays_eq!(patch_values, expected_values, &mut ctx);
374    }
375
376    #[test]
377    fn test_chunk_offsets_no_patches_in_middle() {
378        let mut ctx = SESSION.create_execution_ctx();
379        let mut values = vec![1.0f64; 3072];
380        values[0] = PI;
381        values[2048] = E;
382
383        let array = PrimitiveArray::new(Buffer::from(values), Validity::NonNullable);
384        let encoded = alp_encode(array.as_view(), None, &mut ctx).unwrap();
385        let patches = encoded.patches().unwrap();
386
387        let chunk_offsets = patches
388            .chunk_offsets()
389            .clone()
390            .unwrap()
391            .execute::<PrimitiveArray>(&mut ctx)
392            .unwrap();
393        let expected_offsets = PrimitiveArray::from_iter(vec![0u64, 1, 1]);
394        assert_arrays_eq!(chunk_offsets, expected_offsets, &mut ctx);
395
396        let patch_indices = patches
397            .indices()
398            .clone()
399            .execute::<PrimitiveArray>(&mut ctx)
400            .unwrap();
401        let expected_indices = PrimitiveArray::from_iter(vec![0u64, 2048]);
402        assert_arrays_eq!(patch_indices, expected_indices, &mut ctx);
403
404        let patch_values = patches
405            .values()
406            .clone()
407            .execute::<PrimitiveArray>(&mut ctx)
408            .unwrap();
409        let expected_values = PrimitiveArray::from_iter(vec![PI, E]);
410        assert_arrays_eq!(patch_values, expected_values, &mut ctx);
411    }
412
413    #[test]
414    fn test_chunk_offsets_trailing_empty_chunks() {
415        let mut ctx = SESSION.create_execution_ctx();
416        let mut values = vec![1.0f64; 3072];
417        values[0] = PI;
418
419        let array = PrimitiveArray::new(Buffer::from(values), Validity::NonNullable);
420        let encoded = alp_encode(array.as_view(), None, &mut ctx).unwrap();
421        let patches = encoded.patches().unwrap();
422
423        let chunk_offsets = patches
424            .chunk_offsets()
425            .clone()
426            .unwrap()
427            .execute::<PrimitiveArray>(&mut ctx)
428            .unwrap();
429        let expected_offsets = PrimitiveArray::from_iter(vec![0u64, 1, 1]);
430        assert_arrays_eq!(chunk_offsets, expected_offsets, &mut ctx);
431
432        let patch_indices = patches
433            .indices()
434            .clone()
435            .execute::<PrimitiveArray>(&mut ctx)
436            .unwrap();
437        let expected_indices = PrimitiveArray::from_iter(vec![0u64]);
438        assert_arrays_eq!(patch_indices, expected_indices, &mut ctx);
439
440        let patch_values = patches
441            .values()
442            .clone()
443            .execute::<PrimitiveArray>(&mut ctx)
444            .unwrap();
445        let expected_values = PrimitiveArray::from_iter(vec![PI]);
446        assert_arrays_eq!(patch_values, expected_values, &mut ctx);
447    }
448
449    #[test]
450    fn test_chunk_offsets_single_chunk() {
451        let mut ctx = SESSION.create_execution_ctx();
452        let mut values = vec![1.0f64; 512];
453        values[0] = PI;
454        values[100] = E;
455
456        let array = PrimitiveArray::new(Buffer::from(values), Validity::NonNullable);
457        let encoded = alp_encode(array.as_view(), None, &mut ctx).unwrap();
458        let patches = encoded.patches().unwrap();
459
460        let chunk_offsets = patches
461            .chunk_offsets()
462            .clone()
463            .unwrap()
464            .execute::<PrimitiveArray>(&mut ctx)
465            .unwrap();
466        let expected_offsets = PrimitiveArray::from_iter(vec![0u64]);
467        assert_arrays_eq!(chunk_offsets, expected_offsets, &mut ctx);
468
469        let patch_indices = patches
470            .indices()
471            .clone()
472            .execute::<PrimitiveArray>(&mut ctx)
473            .unwrap();
474        let expected_indices = PrimitiveArray::from_iter(vec![0u64, 100]);
475        assert_arrays_eq!(patch_indices, expected_indices, &mut ctx);
476
477        let patch_values = patches
478            .values()
479            .clone()
480            .execute::<PrimitiveArray>(&mut ctx)
481            .unwrap();
482        let expected_values = PrimitiveArray::from_iter(vec![PI, E]);
483        assert_arrays_eq!(patch_values, expected_values, &mut ctx);
484    }
485
486    #[test]
487    fn test_slice_half_chunk_f32_roundtrip() {
488        // Create 1024 elements, encode, slice to first 512, then decode
489        let values = vec![1.234f32; 1024];
490        let original = PrimitiveArray::new(Buffer::from(values), Validity::NonNullable);
491        let encoded = alp_encode(
492            original.as_view(),
493            None,
494            &mut SESSION.create_execution_ctx(),
495        )
496        .unwrap();
497
498        let sliced_alp = encoded.slice(512..1024).unwrap();
499
500        let expected_slice = original.slice(512..1024).unwrap();
501        assert_arrays_eq!(
502            sliced_alp,
503            expected_slice,
504            &mut SESSION.create_execution_ctx()
505        );
506    }
507
508    #[test]
509    fn test_slice_half_chunk_f64_roundtrip() {
510        let values = vec![5.678f64; 1024];
511        let original = PrimitiveArray::new(Buffer::from(values), Validity::NonNullable);
512        let encoded = alp_encode(
513            original.as_view(),
514            None,
515            &mut SESSION.create_execution_ctx(),
516        )
517        .unwrap();
518
519        let sliced_alp = encoded.slice(512..1024).unwrap();
520
521        let expected_slice = original.slice(512..1024).unwrap();
522        assert_arrays_eq!(
523            sliced_alp,
524            expected_slice,
525            &mut SESSION.create_execution_ctx()
526        );
527    }
528
529    #[test]
530    fn test_slice_half_chunk_with_patches_roundtrip() {
531        let mut values = vec![1.0f64; 1024];
532        values[100] = PI;
533        values[200] = E;
534        values[600] = 42.42;
535
536        let original = PrimitiveArray::new(Buffer::from(values), Validity::NonNullable);
537        let encoded = alp_encode(
538            original.as_view(),
539            None,
540            &mut SESSION.create_execution_ctx(),
541        )
542        .unwrap();
543
544        let sliced_alp = encoded.slice(512..1024).unwrap();
545
546        let expected_slice = original.slice(512..1024).unwrap();
547        assert_arrays_eq!(
548            sliced_alp,
549            expected_slice,
550            &mut SESSION.create_execution_ctx()
551        );
552        assert!(encoded.patches().is_some());
553    }
554
555    #[test]
556    fn test_slice_across_chunks_with_patches_roundtrip() {
557        let mut values = vec![1.0f64; 2048];
558        values[100] = PI;
559        values[200] = E;
560        values[600] = 42.42;
561        values[800] = 42.42;
562        values[1000] = 42.42;
563        values[1023] = 42.42;
564
565        let original = PrimitiveArray::new(Buffer::from(values), Validity::NonNullable);
566        let encoded = alp_encode(
567            original.as_view(),
568            None,
569            &mut SESSION.create_execution_ctx(),
570        )
571        .unwrap();
572
573        let sliced_alp = encoded.slice(1023..1025).unwrap();
574
575        let expected_slice = original.slice(1023..1025).unwrap();
576        assert_arrays_eq!(
577            sliced_alp,
578            expected_slice,
579            &mut SESSION.create_execution_ctx()
580        );
581        assert!(encoded.patches().is_some());
582    }
583
584    #[test]
585    fn test_slice_half_chunk_nullable_roundtrip() {
586        let mut ctx = SESSION.create_execution_ctx();
587        let values = (0..1024)
588            .map(|i| if i % 3 == 0 { None } else { Some(2.5f32) })
589            .collect::<Vec<_>>();
590
591        let original = PrimitiveArray::from_option_iter(values);
592        let encoded = alp_encode(original.as_view(), None, &mut ctx).unwrap();
593
594        let sliced_alp = encoded.slice(512..1024).unwrap();
595        let decoded = sliced_alp.execute::<PrimitiveArray>(&mut ctx).unwrap();
596
597        let expected_slice = original.slice(512..1024).unwrap();
598        assert_arrays_eq!(decoded, expected_slice, &mut ctx);
599    }
600
601    #[test]
602    fn test_large_f32_array_uniform_values() {
603        let size = 10_000;
604        let array = PrimitiveArray::new(buffer![42.125f32; size], Validity::NonNullable);
605        let encoded =
606            alp_encode(array.as_view(), None, &mut SESSION.create_execution_ctx()).unwrap();
607
608        assert!(encoded.patches().is_none());
609        let decoded = decompress_into_array(encoded, &mut SESSION.create_execution_ctx()).unwrap();
610        assert_arrays_eq!(decoded, array, &mut SESSION.create_execution_ctx());
611    }
612
613    #[test]
614    fn test_large_f64_array_uniform_values() {
615        let size = 50_000;
616        let array = PrimitiveArray::new(buffer![123.456789f64; size], Validity::NonNullable);
617        let encoded =
618            alp_encode(array.as_view(), None, &mut SESSION.create_execution_ctx()).unwrap();
619
620        assert!(encoded.patches().is_none());
621        let decoded = decompress_into_array(encoded, &mut SESSION.create_execution_ctx()).unwrap();
622        assert_arrays_eq!(decoded, array, &mut SESSION.create_execution_ctx());
623    }
624
625    #[test]
626    fn test_large_f32_array_with_patches() {
627        let size = 5_000;
628        let mut values = vec![1.5f32; size];
629        values[100] = f32::consts::PI;
630        values[1500] = f32::consts::E;
631        values[3000] = f32::NEG_INFINITY;
632        values[4500] = f32::INFINITY;
633
634        let array = PrimitiveArray::new(Buffer::from(values), Validity::NonNullable);
635        let encoded =
636            alp_encode(array.as_view(), None, &mut SESSION.create_execution_ctx()).unwrap();
637
638        assert!(encoded.patches().is_some());
639        let decoded = decompress_into_array(encoded, &mut SESSION.create_execution_ctx()).unwrap();
640        assert_arrays_eq!(decoded, array, &mut SESSION.create_execution_ctx());
641    }
642
643    #[test]
644    fn test_large_f64_array_with_patches() {
645        let size = 8_000;
646        let mut values = vec![2.2184f64; size];
647        values[0] = PI;
648        values[1000] = E;
649        values[2000] = f64::NAN;
650        values[3000] = f64::INFINITY;
651        values[4000] = f64::NEG_INFINITY;
652        values[5000] = 0.0;
653        values[6000] = -0.0;
654        values[7000] = 999.999999999;
655
656        let array = PrimitiveArray::new(Buffer::from(values.clone()), Validity::NonNullable);
657        let encoded =
658            alp_encode(array.as_view(), None, &mut SESSION.create_execution_ctx()).unwrap();
659
660        assert!(encoded.patches().is_some());
661        let decoded = decompress_into_array(encoded, &mut SESSION.create_execution_ctx()).unwrap();
662
663        for idx in 0..size {
664            let decoded_val = decoded.as_slice::<f64>()[idx];
665            let original_val = values[idx];
666            assert!(
667                NativePType::is_eq(decoded_val, original_val),
668                "At index {idx}: Expected {original_val} but got {decoded_val}"
669            );
670        }
671    }
672
673    #[test]
674    fn test_large_nullable_array() {
675        let size = 12_000;
676        let values: Vec<Option<f32>> = (0..size)
677            .map(|i| {
678                if i % 7 == 0 {
679                    None
680                } else {
681                    Some((i as f32) * 0.1)
682                }
683            })
684            .collect();
685
686        let array = PrimitiveArray::from_option_iter(values);
687        let encoded =
688            alp_encode(array.as_view(), None, &mut SESSION.create_execution_ctx()).unwrap();
689        let decoded = decompress_into_array(encoded, &mut SESSION.create_execution_ctx()).unwrap();
690
691        assert_arrays_eq!(decoded, array, &mut SESSION.create_execution_ctx());
692    }
693
694    #[test]
695    fn test_large_mixed_validity_with_patches() {
696        let size = 6_000;
697        let mut values = vec![10.125f64; size];
698
699        values[500] = PI;
700        values[1500] = E;
701        values[2500] = f64::INFINITY;
702        values[3500] = f64::NEG_INFINITY;
703        values[4500] = f64::NAN;
704
705        let validity = Validity::from_iter((0..size).map(|i| !matches!(i, 500 | 2500)));
706
707        let array = PrimitiveArray::new(Buffer::from(values), validity);
708        let encoded =
709            alp_encode(array.as_view(), None, &mut SESSION.create_execution_ctx()).unwrap();
710        let decoded = decompress_into_array(encoded, &mut SESSION.create_execution_ctx()).unwrap();
711
712        assert_arrays_eq!(decoded, array, &mut SESSION.create_execution_ctx());
713    }
714
715    /// Regression test for patch_chunk index-out-of-bounds when slicing a multi-chunk
716    /// ALP array mid-chunk with patches in the trailing chunk.
717    ///
718    /// The bug: chunk_offsets are sliced at chunk granularity (1024-row boundaries)
719    /// but patches indices/values are sliced at element granularity. When a slice ends
720    /// mid-chunk, patches_end_idx could exceed patches_indices.len(), causing OOB panic
721    /// during decompression.
722    #[test]
723    fn test_slice_mid_chunk_with_patches_in_trailing_chunk() {
724        // 3 chunks (3072 elements), patches scattered across all chunks.
725        let mut values = vec![1.0f64; 3072];
726        // Chunk 0 patches (indices 0..1024)
727        values[100] = PI;
728        values[500] = E;
729        // Chunk 1 patches (indices 1024..2048)
730        values[1100] = PI;
731        values[1500] = E;
732        values[1900] = PI;
733        // Chunk 2 patches (indices 2048..3072)
734        values[2100] = PI;
735        values[2500] = E;
736        values[2900] = PI;
737
738        let original = PrimitiveArray::new(Buffer::from(values), Validity::NonNullable);
739        let encoded = alp_encode(
740            original.as_view(),
741            None,
742            &mut SESSION.create_execution_ctx(),
743        )
744        .unwrap();
745        assert!(encoded.patches().is_some());
746
747        // Slice ending mid-chunk-2 (element 2500 is inside chunk 2 = 2048..3072).
748        // This creates a mismatch: chunk_offsets includes the full chunk 2 offset,
749        // but patches_indices only includes patches up to element 2500.
750        let sliced_alp = encoded.slice(0..2500).unwrap();
751        let expected = original.slice(0..2500).unwrap();
752        assert_arrays_eq!(sliced_alp, expected, &mut SESSION.create_execution_ctx());
753
754        // Also test slicing that starts mid-chunk (both start and end mid-chunk).
755        let sliced_alp = encoded.slice(500..2500).unwrap();
756        let expected = original.slice(500..2500).unwrap();
757        assert_arrays_eq!(sliced_alp, expected, &mut SESSION.create_execution_ctx());
758    }
759}