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