Skip to main content

vortex_pco/
array.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::cmp;
5use std::fmt::Debug;
6use std::fmt::Display;
7use std::fmt::Formatter;
8use std::hash::Hash;
9use std::hash::Hasher;
10
11use pco::ChunkConfig;
12use pco::PagingSpec;
13use pco::data_types::Number;
14use pco::data_types::NumberType;
15use pco::errors::PcoError;
16use pco::match_number_enum;
17use pco::wrapped::ChunkDecompressor;
18use pco::wrapped::FileCompressor;
19use pco::wrapped::FileDecompressor;
20use prost::Message;
21use vortex_array::Array;
22use vortex_array::ArrayEq;
23use vortex_array::ArrayHash;
24use vortex_array::ArrayId;
25use vortex_array::ArrayParts;
26use vortex_array::ArrayRef;
27use vortex_array::ArrayView;
28use vortex_array::EqMode;
29use vortex_array::ExecutionCtx;
30use vortex_array::ExecutionResult;
31use vortex_array::IntoArray;
32use vortex_array::TypedArrayRef;
33use vortex_array::array_slots;
34use vortex_array::arrays::Primitive;
35use vortex_array::arrays::PrimitiveArray;
36use vortex_array::buffer::BufferHandle;
37use vortex_array::dtype::DType;
38use vortex_array::dtype::PType;
39use vortex_array::dtype::half;
40use vortex_array::scalar::Scalar;
41use vortex_array::serde::ArrayChildren;
42use vortex_array::validity::Validity;
43use vortex_array::vtable::OperationsVTable;
44use vortex_array::vtable::VTable;
45use vortex_array::vtable::ValidityVTable;
46use vortex_array::vtable::child_to_validity;
47use vortex_array::vtable::validity_to_child;
48use vortex_buffer::BufferMut;
49use vortex_buffer::ByteBuffer;
50use vortex_buffer::ByteBufferMut;
51use vortex_error::VortexError;
52use vortex_error::VortexResult;
53use vortex_error::vortex_bail;
54use vortex_error::vortex_ensure;
55use vortex_error::vortex_err;
56use vortex_session::VortexSession;
57use vortex_session::registry::CachedId;
58
59use crate::PcoChunkInfo;
60use crate::PcoMetadata;
61use crate::PcoPageInfo;
62
63// Overall approach here:
64// Chunk the array into Pco chunks (currently using the default recommended size
65// for good compression), and into finer-grained Pco pages. As we go, write each
66// ChunkMeta as a buffer, followed by each of that chunk's pages as a buffer. We
67// store metadata for each of these "components" (chunk or page). At
68// decompression time, we figure out which components we need to read and only
69// process those. We only compress and decompress valid values.
70
71// Visually, during decompression, we have an interval of pages we're
72// decompressing and a tighter interval of the slice we actually care about.
73// |=============values (all valid elements)==============|
74// |<-n_skipped_values->|----decompressed_values------|
75//                          |----slice_values----|
76//                          ^                    ^
77// |<---slice_value_start-->|<--slice_n_values-->|
78// We then insert these values to the correct position using a primitive array
79// constructor.
80
81const VALUES_PER_CHUNK: usize = pco::DEFAULT_MAX_PAGE_N;
82
83/// A [`Pco`]-encoded Vortex array.
84pub type PcoArray = Array<Pco>;
85
86impl ArrayHash for PcoData {
87    fn array_hash<H: Hasher>(&self, state: &mut H, accuracy: EqMode) {
88        self.unsliced_n_rows.hash(state);
89        self.slice_start.hash(state);
90        self.slice_stop.hash(state);
91        // Hash chunk_metas and pages using pointer-based hashing
92        for chunk_meta in &self.chunk_metas {
93            chunk_meta.array_hash(state, accuracy);
94        }
95        for page in &self.pages {
96            page.array_hash(state, accuracy);
97        }
98    }
99}
100
101impl ArrayEq for PcoData {
102    fn array_eq(&self, other: &Self, accuracy: EqMode) -> bool {
103        if self.unsliced_n_rows != other.unsliced_n_rows
104            || self.slice_start != other.slice_start
105            || self.slice_stop != other.slice_stop
106            || self.chunk_metas.len() != other.chunk_metas.len()
107            || self.pages.len() != other.pages.len()
108        {
109            return false;
110        }
111        for (a, b) in self.chunk_metas.iter().zip(&other.chunk_metas) {
112            if !a.array_eq(b, accuracy) {
113                return false;
114            }
115        }
116        for (a, b) in self.pages.iter().zip(&other.pages) {
117            if !a.array_eq(b, accuracy) {
118                return false;
119            }
120        }
121        true
122    }
123}
124
125impl VTable for Pco {
126    type TypedArrayData = PcoData;
127
128    type OperationsVTable = Self;
129    type ValidityVTable = Self;
130
131    fn id(&self) -> ArrayId {
132        static ID: CachedId = CachedId::new("vortex.pco");
133        *ID
134    }
135
136    fn validate(
137        &self,
138        data: &PcoData,
139        dtype: &DType,
140        len: usize,
141        slots: &[Option<ArrayRef>],
142    ) -> VortexResult<()> {
143        let validity = child_to_validity(
144            PcoSlotsView::from_slots(slots).validity,
145            dtype.nullability(),
146        );
147        data.validate(dtype, len, &validity)
148    }
149
150    fn nbuffers(array: ArrayView<'_, Self>) -> usize {
151        array.chunk_metas.len() + array.pages.len()
152    }
153
154    fn buffer(array: ArrayView<'_, Self>, idx: usize) -> BufferHandle {
155        if idx < array.chunk_metas.len() {
156            BufferHandle::new_host(array.chunk_metas[idx].clone())
157        } else {
158            let page_idx = idx - array.chunk_metas.len();
159            BufferHandle::new_host(array.pages[page_idx].clone())
160        }
161    }
162
163    fn buffer_name(array: ArrayView<'_, Self>, idx: usize) -> Option<String> {
164        if idx < array.chunk_metas.len() {
165            Some(format!("chunk_meta_{idx}"))
166        } else {
167            Some(format!("page_{}", idx - array.chunk_metas.len()))
168        }
169    }
170
171    fn with_buffers(
172        &self,
173        array: ArrayView<'_, Self>,
174        buffers: &[BufferHandle],
175    ) -> VortexResult<ArrayParts<Self>> {
176        let mut data = array.data().clone();
177        let chunk_metas_len = data.metadata.chunks.len();
178        vortex_ensure!(buffers.len() >= chunk_metas_len);
179        data.chunk_metas = buffers[..chunk_metas_len]
180            .iter()
181            .map(|buffer| buffer.clone().try_to_host_sync())
182            .collect::<VortexResult<Vec<_>>>()?;
183        data.pages = buffers[chunk_metas_len..]
184            .iter()
185            .map(|buffer| buffer.clone().try_to_host_sync())
186            .collect::<VortexResult<Vec<_>>>()?;
187        Ok(
188            ArrayParts::new(self.clone(), array.dtype().clone(), array.len(), data)
189                .with_slots(array.slots().iter().cloned().collect()),
190        )
191    }
192
193    fn serialize(
194        array: ArrayView<'_, Self>,
195        _session: &VortexSession,
196    ) -> VortexResult<Option<Vec<u8>>> {
197        Ok(Some(array.metadata.clone().encode_to_vec()))
198    }
199
200    fn deserialize(
201        &self,
202        dtype: &DType,
203        len: usize,
204        metadata: &[u8],
205        buffers: &[BufferHandle],
206        children: &dyn ArrayChildren,
207        _session: &VortexSession,
208    ) -> VortexResult<ArrayParts<Self>> {
209        let metadata = PcoMetadata::decode(metadata)?;
210        let validity = if children.is_empty() {
211            Validity::from(dtype.nullability())
212        } else if children.len() == 1 {
213            let validity = children.get(0, &Validity::DTYPE, len)?;
214            Validity::Array(validity)
215        } else {
216            vortex_bail!("PcoArray expected 0 or 1 child, got {}", children.len());
217        };
218
219        vortex_ensure!(buffers.len() >= metadata.chunks.len());
220        let chunk_metas = buffers[..metadata.chunks.len()]
221            .iter()
222            .map(|b| b.clone().try_to_host_sync())
223            .collect::<VortexResult<Vec<_>>>()?;
224        let pages = buffers[metadata.chunks.len()..]
225            .iter()
226            .map(|b| b.clone().try_to_host_sync())
227            .collect::<VortexResult<Vec<_>>>()?;
228
229        let expected_n_pages = metadata
230            .chunks
231            .iter()
232            .map(|info| info.pages.len())
233            .sum::<usize>();
234        vortex_ensure!(pages.len() == expected_n_pages);
235
236        let slots = PcoSlots {
237            validity: validity_to_child(&validity, len),
238        }
239        .into_slots();
240        // SAFETY: `Array::try_from_parts`, which consumes these parts, validates the data before
241        // publishing the array.
242        let data =
243            unsafe { PcoData::new_unchecked(chunk_metas, pages, dtype.as_ptype(), metadata, len) };
244        Ok(ArrayParts::new(self.clone(), dtype.clone(), len, data).with_slots(slots))
245    }
246
247    fn slot_name(_array: ArrayView<'_, Self>, idx: usize) -> String {
248        PcoSlots::NAMES[idx].to_string()
249    }
250
251    fn execute(array: Array<Self>, ctx: &mut ExecutionCtx) -> VortexResult<ExecutionResult> {
252        let unsliced_validity = array.unsliced_validity();
253        Ok(ExecutionResult::done(
254            array
255                .data()
256                .decompress(&unsliced_validity, ctx)?
257                .into_array(),
258        ))
259    }
260
261    fn reduce_parent(
262        array: ArrayView<'_, Self>,
263        parent: &ArrayRef,
264        child_idx: usize,
265    ) -> VortexResult<Option<ArrayRef>> {
266        crate::rules::RULES.evaluate(array, parent, child_idx)
267    }
268}
269
270pub(crate) fn number_type_from_dtype(dtype: &DType) -> NumberType {
271    number_type_from_ptype(dtype.as_ptype())
272}
273
274pub(crate) fn number_type_from_ptype(ptype: PType) -> NumberType {
275    match ptype {
276        PType::F16 => NumberType::F16,
277        PType::F32 => NumberType::F32,
278        PType::F64 => NumberType::F64,
279        PType::I16 => NumberType::I16,
280        PType::I32 => NumberType::I32,
281        PType::I64 => NumberType::I64,
282        PType::U16 => NumberType::U16,
283        PType::U32 => NumberType::U32,
284        PType::U64 => NumberType::U64,
285        _ => unreachable!("PType not supported by Pco: {:?}", ptype),
286    }
287}
288
289fn collect_valid(
290    parray: ArrayView<'_, Primitive>,
291    ctx: &mut ExecutionCtx,
292) -> VortexResult<PrimitiveArray> {
293    let mask = parray
294        .array()
295        .validity()?
296        .execute_mask(parray.array().len(), ctx)?;
297    let result = parray
298        .array()
299        .filter(mask)?
300        .execute::<PrimitiveArray>(ctx)?;
301    Ok(result)
302}
303
304pub(crate) fn vortex_err_from_pco(err: PcoError) -> VortexError {
305    use pco::errors::ErrorKind::*;
306    match err.kind {
307        Io(io_kind) => VortexError::from(std::io::Error::new(io_kind, err.message)),
308        InvalidArgument => vortex_err!(InvalidArgument: "{}", err.message),
309        other => vortex_err!("Pco {:?} error: {}", other, err.message),
310    }
311}
312
313#[derive(Clone, Debug)]
314/// Pco array encoding marker.
315pub struct Pco;
316
317impl Pco {
318    /// Constructs a Pco array after validating its data and validity invariants.
319    pub fn try_new(dtype: DType, data: PcoData, validity: Validity) -> VortexResult<PcoArray> {
320        let len = data.len();
321        data.validate(&dtype, len, &validity)?;
322        // SAFETY: `validate` checked the dtype, length, validity, metadata, and buffer invariants.
323        Ok(unsafe { Self::new_unchecked(dtype, data, validity) })
324    }
325
326    /// Constructs a Pco array without validating its data and validity invariants.
327    ///
328    /// # Safety
329    ///
330    /// The caller must ensure that [`PcoData::validate`] would succeed for `dtype`, `data.len()`,
331    /// and `validity`.
332    pub unsafe fn new_unchecked(dtype: DType, data: PcoData, validity: Validity) -> PcoArray {
333        let len = data.len();
334        let slots = PcoSlots {
335            validity: validity_to_child(&validity, data.unsliced_n_rows()),
336        }
337        .into_slots();
338        unsafe {
339            Array::from_parts_unchecked(ArrayParts::new(Pco, dtype, len, data).with_slots(slots))
340        }
341    }
342
343    /// Compress a primitive array using pcodec.
344    pub fn from_primitive(
345        parray: ArrayView<'_, Primitive>,
346        level: usize,
347        values_per_page: usize,
348        ctx: &mut ExecutionCtx,
349    ) -> VortexResult<PcoArray> {
350        let dtype = parray.dtype().clone();
351        let validity = parray.validity()?;
352        let data = PcoData::from_primitive(parray, level, values_per_page, ctx)?;
353        Self::try_new(dtype, data, validity)
354    }
355}
356
357#[array_slots(Pco)]
358pub struct PcoSlots {
359    /// The validity bitmap indicating which elements are non-null.
360    #[slot(0)]
361    pub validity: Option<ArrayRef>,
362}
363
364/// Additional typed accessors for Pco arrays.
365pub trait PcoArrayExt: PcoArraySlotsExt {
366    /// Reconstruct the unsliced [`Validity`] from the validity slot.
367    fn unsliced_validity(&self) -> Validity {
368        child_to_validity(
369            self.as_ref().slots()[PcoSlots::VALIDITY].as_ref(),
370            self.as_ref().dtype().nullability(),
371        )
372    }
373}
374impl<T: TypedArrayRef<Pco>> PcoArrayExt for T {}
375
376#[derive(Clone, Debug)]
377/// Encoding-specific data for a [`PcoArray`].
378pub struct PcoData {
379    pub(crate) chunk_metas: Vec<ByteBuffer>,
380    pub(crate) pages: Vec<ByteBuffer>,
381    pub(crate) metadata: PcoMetadata,
382    ptype: PType,
383    unsliced_n_rows: usize,
384    slice_start: usize,
385    slice_stop: usize,
386}
387
388impl Display for PcoData {
389    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
390        write!(
391            f,
392            "ptype: {}, nrows: {}, slice: {}..{}",
393            self.ptype, self.unsliced_n_rows, self.slice_start, self.slice_stop
394        )
395    }
396}
397
398impl PcoData {
399    /// Validate dtype, validity, slice, and Pco component invariants.
400    pub fn validate(&self, dtype: &DType, len: usize, validity: &Validity) -> VortexResult<()> {
401        let _ = number_type_from_ptype(self.ptype);
402        vortex_ensure!(
403            dtype.as_ptype() == self.ptype,
404            "expected ptype {}, got {}",
405            self.ptype,
406            dtype.as_ptype()
407        );
408        vortex_ensure!(
409            dtype.nullability() == validity.nullability(),
410            "expected nullability {}, got {}",
411            validity.nullability(),
412            dtype.nullability()
413        );
414        vortex_ensure!(
415            self.slice_start <= self.slice_stop && self.slice_stop <= self.unsliced_n_rows,
416            "invalid slice range {}..{} for {} rows",
417            self.slice_start,
418            self.slice_stop,
419            self.unsliced_n_rows
420        );
421        vortex_ensure!(
422            self.slice_stop - self.slice_start == len,
423            "expected len {len}, got {}",
424            self.slice_stop - self.slice_start
425        );
426        if let Some(validity_len) = validity.maybe_len() {
427            vortex_ensure!(
428                validity_len == self.unsliced_n_rows,
429                "expected validity len {}, got {}",
430                self.unsliced_n_rows,
431                validity_len
432            );
433        }
434        vortex_ensure!(
435            self.chunk_metas.len() == self.metadata.chunks.len(),
436            "expected {} chunk metas, got {}",
437            self.metadata.chunks.len(),
438            self.chunk_metas.len()
439        );
440        vortex_ensure!(
441            self.pages.len()
442                == self
443                    .metadata
444                    .chunks
445                    .iter()
446                    .map(|chunk| chunk.pages.len())
447                    .sum::<usize>(),
448            "page count does not match metadata"
449        );
450
451        let mut n_values = 0usize;
452        for (chunk_idx, chunk) in self.metadata.chunks.iter().enumerate() {
453            let mut chunk_n_values = 0usize;
454            for page in &chunk.pages {
455                let page_n_values = page.n_values as usize;
456                vortex_ensure!(
457                    page_n_values != 0,
458                    "Pco chunk {chunk_idx} contains an empty page"
459                );
460                chunk_n_values = chunk_n_values.checked_add(page_n_values).ok_or_else(|| {
461                    vortex_err!("Pco chunk {chunk_idx} value count overflows usize")
462                })?;
463            }
464            vortex_ensure!(
465                chunk_n_values <= VALUES_PER_CHUNK,
466                "Pco chunk {chunk_idx} contains {chunk_n_values} values, exceeding the maximum of {VALUES_PER_CHUNK}"
467            );
468            n_values = n_values
469                .checked_add(chunk_n_values)
470                .ok_or_else(|| vortex_err!("Pco value count overflows usize"))?;
471        }
472        vortex_ensure!(
473            n_values <= self.unsliced_n_rows,
474            "Pco contains {n_values} values for only {} rows",
475            self.unsliced_n_rows
476        );
477        if validity.definitely_no_nulls() {
478            vortex_ensure!(
479                n_values == self.unsliced_n_rows,
480                "Pco contains {n_values} values for {} non-null rows",
481                self.unsliced_n_rows
482            );
483        } else if validity.definitely_all_null() {
484            vortex_ensure!(n_values == 0, "Pco contains values for an all-null array");
485        }
486        Ok(())
487    }
488
489    /// Constructs unsliced Pco data without validating its metadata and buffers.
490    ///
491    /// # Safety
492    ///
493    /// The returned data must pass [`Self::validate`] before it is decompressed or published as
494    /// part of an array.
495    pub unsafe fn new_unchecked(
496        chunk_metas: Vec<ByteBuffer>,
497        pages: Vec<ByteBuffer>,
498        ptype: PType,
499        metadata: PcoMetadata,
500        len: usize,
501    ) -> Self {
502        Self {
503            chunk_metas,
504            pages,
505            metadata,
506            ptype,
507            unsliced_n_rows: len,
508            slice_start: 0,
509            slice_stop: len,
510        }
511    }
512
513    /// Compress a primitive array into Pco data.
514    pub fn from_primitive(
515        parray: ArrayView<'_, Primitive>,
516        level: usize,
517        values_per_page: usize,
518        ctx: &mut ExecutionCtx,
519    ) -> VortexResult<Self> {
520        Self::from_primitive_with_values_per_chunk(
521            parray,
522            level,
523            VALUES_PER_CHUNK,
524            values_per_page,
525            ctx,
526        )
527    }
528
529    pub(crate) fn from_primitive_with_values_per_chunk(
530        parray: ArrayView<'_, Primitive>,
531        level: usize,
532        values_per_chunk: usize,
533        values_per_page: usize,
534        ctx: &mut ExecutionCtx,
535    ) -> VortexResult<Self> {
536        let number_type = number_type_from_dtype(parray.dtype());
537        let values_per_page = if values_per_page == 0 {
538            values_per_chunk
539        } else {
540            values_per_page
541        };
542
543        // perhaps one day we can make this more configurable
544        let chunk_config = ChunkConfig::default()
545            .with_compression_level(level)
546            .with_paging_spec(PagingSpec::EqualPagesUpTo(values_per_page));
547
548        let values = collect_valid(parray, ctx)?;
549        let n_values = values.len();
550
551        let fc = FileCompressor::default();
552        let mut header = vec![];
553        fc.write_header(&mut header).map_err(vortex_err_from_pco)?;
554
555        let mut chunk_meta_buffers = vec![]; // the Pco component
556        let mut chunk_infos = vec![]; // the Vortex metadata
557        let mut page_buffers = vec![];
558        for chunk_start in (0..n_values).step_by(values_per_chunk) {
559            let chunk_end = cmp::min(n_values, chunk_start + values_per_chunk);
560            let mut cc = match_number_enum!(
561                number_type,
562                NumberType<T> => {
563                    let values = values.to_buffer::<T>();
564                    let chunk = &values.as_slice()[chunk_start..chunk_end];
565                    fc
566                        .chunk_compressor(chunk, &chunk_config)
567                        .map_err(vortex_err_from_pco)?
568                }
569            );
570
571            let mut chunk_meta_buffer = ByteBufferMut::with_capacity(cc.meta_size_hint());
572            cc.write_meta(&mut chunk_meta_buffer)
573                .map_err(vortex_err_from_pco)?;
574            chunk_meta_buffers.push(chunk_meta_buffer.freeze());
575
576            let mut page_infos = vec![];
577            for (page_idx, page_n_values) in cc.n_per_page().into_iter().enumerate() {
578                let mut page = ByteBufferMut::with_capacity(cc.page_size_hint(page_idx));
579                cc.write_page(page_idx, &mut page)
580                    .map_err(vortex_err_from_pco)?;
581                page_buffers.push(page.freeze());
582                page_infos.push(PcoPageInfo {
583                    n_values: u32::try_from(page_n_values)?,
584                });
585            }
586            chunk_infos.push(PcoChunkInfo { pages: page_infos })
587        }
588
589        let metadata = PcoMetadata {
590            header,
591            chunks: chunk_infos,
592        };
593        // SAFETY: The compressor produced matching chunk metadata and pages from `parray`, with
594        // the same ptype, logical length, and valid-value count.
595        Ok(unsafe {
596            PcoData::new_unchecked(
597                chunk_meta_buffers,
598                page_buffers,
599                parray.dtype().as_ptype(),
600                metadata,
601                parray.len(),
602            )
603        })
604    }
605
606    /// Downcast and compress an array into Pco data.
607    ///
608    /// # Errors
609    ///
610    /// Returns an error if the input is not a primitive array or compression fails.
611    pub fn from_array(
612        array: ArrayRef,
613        level: usize,
614        nums_per_page: usize,
615        ctx: &mut ExecutionCtx,
616    ) -> VortexResult<Self> {
617        let parray = array.try_downcast::<Primitive>().map_err(|a| {
618            vortex_err!(
619                "Pco can only encode primitive arrays, got {}",
620                a.encoding_id()
621            )
622        })?;
623        Self::from_primitive(parray.as_view(), level, nums_per_page, ctx)
624    }
625
626    /// Decompress this Pco data into a primitive array.
627    pub(crate) fn decompress(
628        &self,
629        unsliced_validity: &Validity,
630        ctx: &mut ExecutionCtx,
631    ) -> VortexResult<PrimitiveArray> {
632        // To start, we figure out which chunks and pages we need to decompress, and with
633        // what value offset into the first such page.
634        let number_type = number_type_from_ptype(self.ptype);
635        let values_byte_buffer = match_number_enum!(
636            number_type,
637            NumberType<T> => {
638              self.decompress_values_typed::<T>(unsliced_validity, ctx)?
639            }
640        );
641
642        Ok(PrimitiveArray::from_values_byte_buffer(
643            values_byte_buffer,
644            self.ptype,
645            unsliced_validity.slice(self.slice_start..self.slice_stop)?,
646            self.slice_stop - self.slice_start,
647            ctx,
648        ))
649    }
650
651    fn decompress_values_typed<T: Number>(
652        &self,
653        unsliced_validity: &Validity,
654        ctx: &mut ExecutionCtx,
655    ) -> VortexResult<ByteBuffer> {
656        // To start, we figure out what range of values we need to decompress.
657        let slice_value_indices = unsliced_validity
658            .execute_mask(self.unsliced_n_rows, ctx)?
659            .valid_counts_for_indices(&[self.slice_start, self.slice_stop]);
660        let slice_value_start = slice_value_indices[0];
661        let slice_value_stop = slice_value_indices[1];
662        let slice_n_values = slice_value_stop - slice_value_start;
663
664        // Then we decompress those pages into a buffer. Note that these values
665        // may exceed the bounds of the slice, so we need to slice later.
666        let (fd, _) =
667            FileDecompressor::new(self.metadata.header.as_slice()).map_err(vortex_err_from_pco)?;
668        let mut decompressed_values =
669            BufferMut::<T>::with_capacity(slice_n_values.min(VALUES_PER_CHUNK));
670        // Validation bounds every page-count prefix and selected-page subtotal by
671        // `unsliced_n_rows`, so the arithmetic below cannot overflow.
672        let mut page_idx = 0;
673        let mut page_value_start = 0usize;
674        let mut n_skipped_values = 0;
675        for (chunk_info, chunk_meta) in self.metadata.chunks.iter().zip(&self.chunk_metas) {
676            // lazily initialize chunk decompressor
677            let mut chunk_decompressor: Option<ChunkDecompressor<T>> = None;
678            for page_info in &chunk_info.pages {
679                let page_n_values = page_info.n_values as usize;
680                let page_value_stop = page_value_start + page_n_values;
681
682                if page_value_start >= slice_value_stop {
683                    break;
684                }
685
686                if page_value_stop > slice_value_start {
687                    // we need this page
688                    let old_len = decompressed_values.len();
689                    let new_len = old_len + page_n_values;
690                    decompressed_values.reserve(page_n_values);
691                    unsafe {
692                        decompressed_values.set_len(new_len);
693                    }
694                    let page: &[u8] = self
695                        .pages
696                        .get(page_idx)
697                        .ok_or_else(|| vortex_err!("Missing Pco page {page_idx}"))?
698                        .as_ref();
699
700                    let mut cd = match chunk_decompressor.take() {
701                        Some(d) => d,
702                        None => {
703                            let (new_cd, _) = fd
704                                .chunk_decompressor(chunk_meta.as_ref())
705                                .map_err(vortex_err_from_pco)?;
706                            new_cd
707                        }
708                    };
709
710                    let mut pd = cd
711                        .page_decompressor(page, page_n_values)
712                        .map_err(vortex_err_from_pco)?;
713                    pd.read(&mut decompressed_values[old_len..new_len])
714                        .map_err(vortex_err_from_pco)?;
715
716                    chunk_decompressor = Some(cd);
717                } else {
718                    n_skipped_values += page_n_values;
719                }
720
721                page_value_start = page_value_stop;
722                page_idx += 1;
723            }
724        }
725
726        // Slice only the values requested.
727        // Skipped pages end before `slice_value_start`, and the resulting stop is at most
728        // `slice_value_stop`, which is bounded by `unsliced_n_rows`.
729        let value_offset = slice_value_start - n_skipped_values;
730        let value_stop = value_offset + slice_n_values;
731        vortex_ensure!(
732            value_stop <= decompressed_values.len(),
733            "Pco contains {} decompressed values, but the requested range ends at {value_stop}",
734            decompressed_values.len()
735        );
736        Ok(decompressed_values
737            .freeze()
738            .slice(value_offset..value_stop)
739            .into_byte_buffer())
740    }
741
742    pub(crate) fn _slice(&self, start: usize, stop: usize) -> Self {
743        PcoData {
744            slice_start: self.slice_start + start,
745            slice_stop: self.slice_start + stop,
746            ..self.clone()
747        }
748    }
749
750    /// Returns the number of elements in the array.
751    pub fn len(&self) -> usize {
752        self.slice_stop - self.slice_start
753    }
754
755    /// Returns `true` if the array contains no elements.
756    pub fn is_empty(&self) -> bool {
757        self.slice_stop == self.slice_start
758    }
759
760    pub(crate) fn slice_start(&self) -> usize {
761        self.slice_start
762    }
763
764    pub(crate) fn slice_stop(&self) -> usize {
765        self.slice_stop
766    }
767
768    pub(crate) fn unsliced_n_rows(&self) -> usize {
769        self.unsliced_n_rows
770    }
771}
772
773impl ValidityVTable<Pco> for Pco {
774    fn validity(array: ArrayView<'_, Pco>) -> VortexResult<Validity> {
775        array
776            .unsliced_validity()
777            .slice(array.slice_start()..array.slice_stop())
778    }
779}
780
781impl OperationsVTable<Pco> for Pco {
782    fn scalar_at(
783        array: ArrayView<'_, Pco>,
784        index: usize,
785        ctx: &mut ExecutionCtx,
786    ) -> VortexResult<Scalar> {
787        let unsliced_validity = array.unsliced_validity();
788        array
789            ._slice(index, index + 1)
790            .decompress(&unsliced_validity, ctx)?
791            .into_array()
792            .execute_scalar(0, ctx)
793    }
794}
795
796#[cfg(test)]
797mod tests {
798    use vortex_array::IntoArray;
799    use vortex_array::VortexSessionExecute;
800    use vortex_array::array_session;
801    use vortex_array::arrays::PrimitiveArray;
802    use vortex_array::assert_arrays_eq;
803    use vortex_array::validity::Validity;
804    use vortex_buffer::buffer;
805    use vortex_error::VortexResult;
806
807    use super::VALUES_PER_CHUNK;
808    use crate::Pco;
809
810    #[test]
811    fn test_slice_nullable() {
812        let mut ctx = array_session().create_execution_ctx();
813        // Create a nullable array with some nulls
814        let values = PrimitiveArray::new(
815            buffer![10u32, 20, 30, 40, 50, 60],
816            Validity::from_iter([false, true, true, true, true, false]),
817        );
818        let pco = Pco::from_primitive(values.as_view(), 0, 128, &mut ctx).unwrap();
819        assert_arrays_eq!(
820            pco,
821            PrimitiveArray::from_option_iter([
822                None,
823                Some(20u32),
824                Some(30),
825                Some(40),
826                Some(50),
827                None
828            ]),
829            &mut ctx
830        );
831
832        // Slice to get only the non-null values in the middle
833        let sliced = pco.slice(1..5).unwrap();
834        let expected =
835            PrimitiveArray::from_option_iter([Some(20u32), Some(30), Some(40), Some(50)])
836                .into_array();
837        assert_arrays_eq!(sliced, expected, &mut ctx);
838    }
839
840    #[test]
841    fn test_decompress_bounds_initial_allocation() -> VortexResult<()> {
842        let mut ctx = array_session().create_execution_ctx();
843        let values = PrimitiveArray::from_iter([42u32]);
844        let pco = Pco::from_primitive(values.as_view(), 0, 128, &mut ctx)?;
845        let mut data = pco.data().clone();
846
847        // Simulate a corrupt logical length whose validity claims far more values than the
848        // encoded pages contain. This must not be used directly as an allocation size.
849        data.unsliced_n_rows = usize::MAX;
850        data.slice_stop = usize::MAX;
851
852        let result = data.decompress_values_typed::<u32>(&Validity::NonNullable, &mut ctx);
853        assert!(result.is_err());
854        Ok(())
855    }
856
857    #[test]
858    fn test_validate_rejects_oversized_chunk() -> VortexResult<()> {
859        let mut ctx = array_session().create_execution_ctx();
860        let values = PrimitiveArray::from_iter([42u32]);
861        let pco = Pco::from_primitive(values.as_view(), 0, 128, &mut ctx)?;
862        let mut data = pco.data().clone();
863        data.metadata.chunks[0].pages[0].n_values = u32::try_from(VALUES_PER_CHUNK + 1)?;
864
865        assert!(
866            data.validate(pco.dtype(), pco.len(), &Validity::NonNullable)
867                .is_err()
868        );
869        Ok(())
870    }
871}