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