Skip to main content

vortex_zstd/
array.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Debug;
5use std::fmt::Display;
6use std::fmt::Formatter;
7use std::hash::Hash;
8use std::hash::Hasher;
9use std::mem::MaybeUninit;
10use std::ops::Range;
11use std::sync::Arc;
12
13use itertools::Itertools as _;
14use num_traits::AsPrimitive;
15use prost::Message as _;
16use vortex_array::Array;
17use vortex_array::ArrayEq;
18use vortex_array::ArrayHash;
19use vortex_array::ArrayId;
20use vortex_array::ArrayParts;
21use vortex_array::ArrayRef;
22use vortex_array::ArrayView;
23use vortex_array::Canonical;
24use vortex_array::EqMode;
25use vortex_array::ExecutionCtx;
26use vortex_array::ExecutionResult;
27use vortex_array::IntoArray;
28use vortex_array::array_slots;
29use vortex_array::arrays::ConstantArray;
30use vortex_array::arrays::PrimitiveArray;
31use vortex_array::arrays::VarBinViewArray;
32use vortex_array::arrays::varbinview::build_views::BinaryView;
33use vortex_array::arrays::varbinview::build_views::MAX_BUFFER_LEN;
34use vortex_array::buffer::BufferHandle;
35use vortex_array::builders::ArrayBuilder;
36use vortex_array::builders::VarBinBuilder;
37use vortex_array::builders::VarBinViewBuilder;
38use vortex_array::dtype::DType;
39use vortex_array::dtype::OffsetBuilderPType;
40use vortex_array::match_each_varbin_builder;
41use vortex_array::scalar::Scalar;
42use vortex_array::serde::ArrayChildren;
43use vortex_array::smallvec::smallvec;
44use vortex_array::validity::Validity;
45use vortex_array::vtable::OperationsVTable;
46use vortex_array::vtable::VTable;
47use vortex_array::vtable::ValidityVTable;
48use vortex_array::vtable::child_to_validity;
49use vortex_array::vtable::validity_to_child;
50use vortex_buffer::Alignment;
51use vortex_buffer::Buffer;
52use vortex_buffer::BufferMut;
53use vortex_buffer::ByteBuffer;
54use vortex_buffer::ByteBufferMut;
55use vortex_error::VortexError;
56use vortex_error::VortexExpect;
57use vortex_error::VortexResult;
58use vortex_error::vortex_bail;
59use vortex_error::vortex_ensure;
60use vortex_error::vortex_err;
61use vortex_mask::AllOr;
62use vortex_mask::Mask;
63use vortex_session::VortexSession;
64use vortex_session::registry::CachedId;
65use zstd::zstd_safe::WriteBuf;
66
67use crate::ZstdFrameMetadata;
68use crate::ZstdMetadata;
69use crate::validate_frame_content_size;
70
71// Zstd doesn't support training dictionaries on very few samples.
72const MIN_SAMPLES_FOR_DICTIONARY: usize = 8;
73type ViewLen = u32;
74
75// Overall approach here:
76// Zstd can be used on the whole array (values_per_frame = 0), resulting in a single Zstd
77// frame, or it can be used with a dictionary (values_per_frame < # values), resulting in
78// multiple Zstd frames sharing a common dictionary. This latter case is helpful if you
79// want somewhat faster access to slices or individual rows, allowing us to only
80// decompress the necessary frames.
81
82// Visually, during decompression, we have an interval of frames we're
83// decompressing and a tighter interval of the slice we actually care about.
84// |=============values (all valid elements)==============|
85// |<-skipped_uncompressed->|----decompressed-------------|
86//                              |------slice-------|
87//                              ^                  ^
88// |<-slice_uncompressed_start->|                  |
89// |<------------slice_uncompressed_stop---------->|
90// We then insert these values to the correct position using a primitive array
91// constructor.
92
93/// A [`Zstd`]-encoded Vortex array.
94pub type ZstdArray = Array<Zstd>;
95
96impl ArrayHash for ZstdData {
97    fn array_hash<H: Hasher>(&self, state: &mut H, accuracy: EqMode) {
98        match &self.dictionary {
99            Some(dict) => {
100                true.hash(state);
101                dict.array_hash(state, accuracy);
102            }
103            None => {
104                false.hash(state);
105            }
106        }
107        for frame in &self.frames {
108            frame.array_hash(state, accuracy);
109        }
110        self.unsliced_n_rows.hash(state);
111        self.slice_start.hash(state);
112        self.slice_stop.hash(state);
113    }
114}
115
116impl ArrayEq for ZstdData {
117    fn array_eq(&self, other: &Self, accuracy: EqMode) -> bool {
118        if !match (&self.dictionary, &other.dictionary) {
119            (Some(d1), Some(d2)) => d1.array_eq(d2, accuracy),
120            (None, None) => true,
121            _ => false,
122        } {
123            return false;
124        }
125        if self.frames.len() != other.frames.len() {
126            return false;
127        }
128        for (a, b) in self.frames.iter().zip(&other.frames) {
129            if !a.array_eq(b, accuracy) {
130                return false;
131            }
132        }
133        self.unsliced_n_rows == other.unsliced_n_rows
134            && self.slice_start == other.slice_start
135            && self.slice_stop == other.slice_stop
136    }
137}
138
139impl VTable for Zstd {
140    type TypedArrayData = ZstdData;
141
142    type OperationsVTable = Self;
143    type ValidityVTable = Self;
144
145    fn id(&self) -> ArrayId {
146        static ID: CachedId = CachedId::new("vortex.zstd");
147        *ID
148    }
149
150    fn validate(
151        &self,
152        data: &Self::TypedArrayData,
153        dtype: &DType,
154        len: usize,
155        slots: &[Option<ArrayRef>],
156    ) -> VortexResult<()> {
157        let validity = child_to_validity(slots[ZstdSlots::VALIDITY].as_ref(), dtype.nullability());
158        data.validate(dtype, len, &validity)
159    }
160
161    fn nbuffers(array: ArrayView<'_, Self>) -> usize {
162        array.dictionary.is_some() as usize + array.frames.len()
163    }
164
165    fn buffer(array: ArrayView<'_, Self>, idx: usize) -> BufferHandle {
166        if let Some(dict) = &array.dictionary {
167            if idx == 0 {
168                return BufferHandle::new_host(dict.clone());
169            }
170            BufferHandle::new_host(array.frames[idx - 1].clone())
171        } else {
172            BufferHandle::new_host(array.frames[idx].clone())
173        }
174    }
175
176    fn buffer_name(array: ArrayView<'_, Self>, idx: usize) -> Option<String> {
177        if array.dictionary.is_some() {
178            if idx == 0 {
179                Some("dictionary".to_string())
180            } else {
181                Some(format!("frame_{}", idx - 1))
182            }
183        } else {
184            Some(format!("frame_{idx}"))
185        }
186    }
187
188    fn with_buffers(
189        &self,
190        array: ArrayView<'_, Self>,
191        buffers: &[BufferHandle],
192    ) -> VortexResult<ArrayParts<Self>> {
193        let mut data = array.data().clone();
194        if data.dictionary.is_some() {
195            let Some((dictionary, frames)) = buffers.split_first() else {
196                vortex_bail!("Expected dictionary buffer");
197            };
198            data.dictionary = Some(dictionary.clone().try_to_host_sync()?);
199            data.frames = frames
200                .iter()
201                .map(|buffer| buffer.clone().try_to_host_sync())
202                .collect::<VortexResult<Vec<_>>>()?;
203        } else {
204            data.frames = buffers
205                .iter()
206                .map(|buffer| buffer.clone().try_to_host_sync())
207                .collect::<VortexResult<Vec<_>>>()?;
208        }
209        Ok(
210            ArrayParts::new(self.clone(), array.dtype().clone(), array.len(), data)
211                .with_slots(array.slots().iter().cloned().collect()),
212        )
213    }
214
215    fn serialize(
216        array: ArrayView<'_, Self>,
217        _session: &VortexSession,
218    ) -> VortexResult<Option<Vec<u8>>> {
219        Ok(Some(array.metadata.clone().encode_to_vec()))
220    }
221
222    fn deserialize(
223        &self,
224        dtype: &DType,
225        len: usize,
226        metadata: &[u8],
227        buffers: &[BufferHandle],
228        children: &dyn ArrayChildren,
229        _session: &VortexSession,
230    ) -> VortexResult<ArrayParts<Self>> {
231        let metadata = ZstdMetadata::decode(metadata)?;
232        let validity = if children.is_empty() {
233            Validity::from(dtype.nullability())
234        } else if children.len() == 1 {
235            let validity = children.get(0, &Validity::DTYPE, len)?;
236            Validity::Array(validity)
237        } else {
238            vortex_bail!("ZstdArray expected 0 or 1 child, got {}", children.len());
239        };
240
241        let (dictionary_buffer, compressed_buffers) = if metadata.dictionary_size == 0 {
242            // no dictionary
243            (
244                None,
245                buffers
246                    .iter()
247                    .map(|b| b.clone().try_to_host_sync())
248                    .collect::<VortexResult<Vec<_>>>()?,
249            )
250        } else {
251            // with dictionary
252            (
253                Some(buffers[0].clone().try_to_host_sync()?),
254                buffers[1..]
255                    .iter()
256                    .map(|b| b.clone().try_to_host_sync())
257                    .collect::<VortexResult<Vec<_>>>()?,
258            )
259        };
260
261        let slots = smallvec![validity_to_child(&validity, len)];
262        let data = ZstdData::new(dictionary_buffer, compressed_buffers, metadata, len);
263        Ok(ArrayParts::new(self.clone(), dtype.clone(), len, data).with_slots(slots))
264    }
265
266    fn slot_name(_array: ArrayView<'_, Self>, idx: usize) -> String {
267        ZstdSlots::NAMES[idx].to_string()
268    }
269
270    fn execute(array: Array<Self>, ctx: &mut ExecutionCtx) -> VortexResult<ExecutionResult> {
271        let unsliced_validity = child_to_validity(
272            array.as_ref().slots()[ZstdSlots::VALIDITY].as_ref(),
273            array.dtype().nullability(),
274        );
275        array
276            .data()
277            .decompress(array.dtype(), &unsliced_validity, ctx)?
278            .execute::<ArrayRef>(ctx)
279            .map(ExecutionResult::done)
280    }
281
282    fn append_to_builder(
283        array: ArrayView<'_, Self>,
284        builder: &mut dyn ArrayBuilder,
285        ctx: &mut ExecutionCtx,
286    ) -> VortexResult<()> {
287        if let Some(result) =
288            match_each_varbin_builder!(builder, |builder| append_to_varbin(array, builder, ctx))
289        {
290            return result;
291        }
292        // The two arms here are every builder a `Utf8`/`Binary` dtype has: all four
293        // `VarBinBuilder` widths above, and `VarBinViewBuilder` below. There is deliberately no
294        // canonicalize-then-append fallback — it would decompress to a `VarBinView` only for
295        // `VarBinView::append_to_builder` to reject the same remainder.
296        let Some(builder) = builder.as_any_mut().downcast_mut::<VarBinViewBuilder>() else {
297            vortex_bail!("append_to_builder for Zstd requires a variable-binary builder")
298        };
299        append_to_varbinview(array, builder, ctx)
300    }
301
302    fn reduce_parent(
303        array: ArrayView<'_, Self>,
304        parent: &ArrayRef,
305        child_idx: usize,
306    ) -> VortexResult<Option<ArrayRef>> {
307        crate::rules::RULES.evaluate(array, parent, child_idx)
308    }
309}
310
311fn unsliced_validity(array: ArrayView<'_, Zstd>) -> Validity {
312    child_to_validity(
313        array.slots()[ZstdSlots::VALIDITY].as_ref(),
314        array.dtype().nullability(),
315    )
316}
317
318/// Copies the decompressed values straight into `builder`'s byte storage.
319///
320/// The decompressed frames interleave a length prefix with each value, so the bytes have to be
321/// compacted; sizing the offsets, byte storage and validity from the slice's own metadata keeps
322/// that down to one offset store plus one `memcpy` per value.
323fn append_to_varbin<O: OffsetBuilderPType>(
324    array: ArrayView<'_, Zstd>,
325    builder: &mut VarBinBuilder<O>,
326    ctx: &mut ExecutionCtx,
327) -> VortexResult<()>
328where
329    usize: AsPrimitive<O>,
330{
331    let slice = array
332        .data()
333        .decompress_slice(array.dtype(), &unsliced_validity(array), ctx)?;
334    let mask = slice.validity.execute_mask(slice.n_rows, ctx)?;
335    append_slice_to_varbin(&slice, &mask, builder)
336}
337
338/// Copies the values `mask` marks as valid out of `slice` and into `builder`.
339fn append_slice_to_varbin<O: OffsetBuilderPType>(
340    slice: &DecompressedSlice,
341    mask: &Mask,
342    builder: &mut VarBinBuilder<O>,
343) -> VortexResult<()>
344where
345    usize: AsPrimitive<O>,
346{
347    let (values, num_bytes) = slice.value_bytes()?;
348    // Each value is length-prefixed, so the frames can only be walked in order — which is the
349    // order `append_valid_slices` visits the valid rows in. A walk that runs out early hands back
350    // its remainder, which the builder rejects as a byte-count mismatch.
351    let mut values = ZstdValues::new(values);
352    builder.append_valid_slices(num_bytes, mask, |_| values.next_value())
353}
354
355/// Hands the decompressed frames to `builder` as data buffers with views built over them.
356///
357/// The frames already hold the values contiguously, so the views can reference them in place and
358/// the only per-row work is building one view; going through the canonical array instead would
359/// rewrite every view a second time to rebase its buffer index.
360fn append_to_varbinview(
361    array: ArrayView<'_, Zstd>,
362    builder: &mut VarBinViewBuilder,
363    ctx: &mut ExecutionCtx,
364) -> VortexResult<()> {
365    let slice = array
366        .data()
367        .decompress_slice(array.dtype(), &unsliced_validity(array), ctx)?;
368    let mask = slice.validity.execute_mask(slice.n_rows, ctx)?;
369
370    // No values were stored, so there is nothing to reference and the frames can be dropped.
371    if mask.all_false() {
372        builder.append_nulls(slice.n_rows);
373        return Ok(());
374    }
375
376    // The decompressed frames cover whole frames and so can extend past the requested values on
377    // either side. Reconstructing over just the requested region keeps the pushed buffers fully
378    // utilized, which is what `append_views_built_at` requires: it hands them to the finished
379    // array as they are, without compacting them.
380    let value_bytes = slice.bytes.slice(slice.value_byte_range()?);
381    // The values only reveal themselves while walking the length-prefixed frames, so the views
382    // are built inside the builder's numbering callback rather than from a lengths slice.
383    builder.append_views_built_at(&mask, |next_buffer_index| {
384        let (buffers, valid_views) =
385            try_reconstruct_views(&value_bytes, next_buffer_index, MAX_BUFFER_LEN)?;
386        vortex_ensure!(
387            valid_views.len() == mask.true_count(),
388            "Corrupt zstd metadata: the decompressed frames hold {} values for the {} valid rows \
389             of the slice",
390            valid_views.len(),
391            mask.true_count()
392        );
393
394        let views = match mask.bit_buffer() {
395            AllOr::All => valid_views,
396            AllOr::None => unreachable!("handled above"),
397            AllOr::Some(bits) => {
398                // Null rows carry an empty view, so scatter the stored values into their rows.
399                // Walking the set bits a word at a time avoids materializing the mask's indices,
400                // which the views are the only consumer of.
401                let mut views = BufferMut::<BinaryView>::zeroed(slice.n_rows);
402                let mut valid_row = 0;
403                bits.for_each_set_index(|index| {
404                    // In bounds: `valid_views.len() == mask.true_count()` was checked above, and
405                    // `index < slice.n_rows` because `bits` is the mask over those rows.
406                    views[index] = valid_views[valid_row];
407                    valid_row += 1;
408                });
409                views.freeze()
410            }
411        };
412        Ok((buffers, views))
413    })
414}
415
416#[derive(Clone, Debug)]
417/// Zstd array encoding marker.
418pub struct Zstd;
419
420impl Zstd {
421    /// Construct a [`ZstdArray`] from validated compressed data and validity.
422    pub fn try_new(dtype: DType, data: ZstdData, validity: Validity) -> VortexResult<ZstdArray> {
423        let len = data.len();
424        data.validate(&dtype, len, &validity)?;
425        let slots = smallvec![validity_to_child(&validity, data.unsliced_n_rows())];
426        Ok(unsafe {
427            Array::from_parts_unchecked(ArrayParts::new(Zstd, dtype, len, data).with_slots(slots))
428        })
429    }
430
431    /// Compress a [`VarBinViewArray`] using Zstd without a dictionary.
432    pub fn from_var_bin_view_without_dict(
433        vbv: &VarBinViewArray,
434        level: i32,
435        values_per_frame: usize,
436        ctx: &mut ExecutionCtx,
437    ) -> VortexResult<ZstdArray> {
438        let validity = vbv.validity()?;
439        Self::try_new(
440            vbv.dtype().clone(),
441            ZstdData::from_var_bin_view_without_dict(vbv, level, values_per_frame, ctx)?,
442            validity,
443        )
444    }
445
446    /// Compress a [`PrimitiveArray`] using Zstd.
447    pub fn from_primitive(
448        parray: &PrimitiveArray,
449        level: i32,
450        values_per_frame: usize,
451        ctx: &mut ExecutionCtx,
452    ) -> VortexResult<ZstdArray> {
453        let validity = parray.validity()?;
454        Self::try_new(
455            parray.dtype().clone(),
456            ZstdData::from_primitive(parray, level, values_per_frame, ctx)?,
457            validity,
458        )
459    }
460
461    /// Compress a [`VarBinViewArray`] using Zstd.
462    pub fn from_var_bin_view(
463        vbv: &VarBinViewArray,
464        level: i32,
465        values_per_frame: usize,
466        ctx: &mut ExecutionCtx,
467    ) -> VortexResult<ZstdArray> {
468        let validity = vbv.validity()?;
469        Self::try_new(
470            vbv.dtype().clone(),
471            ZstdData::from_var_bin_view(vbv, level, values_per_frame, ctx)?,
472            validity,
473        )
474    }
475
476    /// Decompress a [`ZstdArray`] into its canonical Vortex representation.
477    pub fn decompress(array: &ZstdArray, ctx: &mut ExecutionCtx) -> VortexResult<ArrayRef> {
478        let unsliced_validity = child_to_validity(
479            array.as_ref().slots()[ZstdSlots::VALIDITY].as_ref(),
480            array.dtype().nullability(),
481        );
482        array
483            .data()
484            .decompress(array.dtype(), &unsliced_validity, ctx)
485    }
486}
487
488#[array_slots(Zstd)]
489pub struct ZstdSlots {
490    /// The validity bitmap indicating which elements are non-null.
491    #[slot(0)]
492    pub validity: Option<ArrayRef>,
493}
494
495#[derive(Clone, Debug)]
496/// Encoding-specific data for a [`ZstdArray`].
497pub struct ZstdData {
498    pub(crate) dictionary: Option<ByteBuffer>,
499    pub(crate) frames: Vec<ByteBuffer>,
500    pub(crate) metadata: ZstdMetadata,
501    unsliced_n_rows: usize,
502    slice_start: usize,
503    slice_stop: usize,
504}
505
506impl Display for ZstdData {
507    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
508        write!(
509            f,
510            "nrows: {}, slice: {}..{}",
511            self.unsliced_n_rows, self.slice_start, self.slice_stop
512        )
513    }
514}
515
516/// Movable parts of a [`ZstdData`] value plus its validity.
517pub struct ZstdDataParts {
518    /// Optional zstd dictionary shared by all frames.
519    pub dictionary: Option<ByteBuffer>,
520    /// Compressed zstd frames.
521    pub frames: Vec<ByteBuffer>,
522    /// Serialized frame and dictionary metadata.
523    pub metadata: ZstdMetadata,
524    /// Unsliced validity for the array.
525    pub validity: Validity,
526    /// Unsliced row count.
527    pub n_rows: usize,
528    /// Start of this logical slice in unsliced row coordinates.
529    pub slice_start: usize,
530    /// End of this logical slice in unsliced row coordinates.
531    pub slice_stop: usize,
532}
533
534/// Compressed ZStd frames and their metadata
535#[derive(Debug)]
536struct Frames {
537    dictionary: Option<ByteBuffer>,
538    frames: Vec<ByteBuffer>,
539    frame_metas: Vec<ZstdFrameMetadata>,
540}
541
542fn choose_max_dict_size(uncompressed_size: usize) -> usize {
543    // following recommendations from
544    // https://github.com/facebook/zstd/blob/v1.5.5/lib/zdict.h#L190
545    // that is, 1/100 the data size, up to 100kB.
546    // It appears that zstd can't train dictionaries with <256 bytes.
547    (uncompressed_size / 100).clamp(256, 100 * 1024)
548}
549
550fn collect_valid_primitive(
551    parray: &PrimitiveArray,
552    ctx: &mut ExecutionCtx,
553) -> VortexResult<PrimitiveArray> {
554    let mask = parray
555        .as_ref()
556        .validity()?
557        .execute_mask(parray.as_ref().len(), ctx)?;
558    let result = parray.filter(mask)?.execute::<PrimitiveArray>(ctx)?;
559    Ok(result)
560}
561
562fn collect_valid_vbv(
563    vbv: &VarBinViewArray,
564    ctx: &mut ExecutionCtx,
565) -> VortexResult<(ByteBuffer, Vec<usize>)> {
566    let mask = vbv
567        .as_ref()
568        .validity()?
569        .execute_mask(vbv.as_ref().len(), ctx)?;
570    let buffer_and_value_byte_indices = match mask.bit_buffer() {
571        AllOr::None => (Buffer::empty(), Vec::new()),
572        _ => {
573            let mut buffer = BufferMut::with_capacity(
574                usize::try_from(vbv.nbytes()).vortex_expect("must fit into buffer")
575                    + mask.true_count() * size_of::<ViewLen>(),
576            );
577            let mut value_byte_indices = Vec::new();
578            let views = vbv.views();
579            let buffers = vbv
580                .data_buffers()
581                .iter()
582                .map(|b| b.as_host())
583                .collect::<Vec<_>>();
584            // skip nulls, writing only valid values
585            for (i, view) in views.iter().enumerate() {
586                if !mask.value(i) {
587                    continue;
588                }
589                let value = if view.is_inlined() {
590                    view.as_inlined().value()
591                } else {
592                    let view_ref = view.as_view();
593                    &buffers[view_ref.buffer_index as usize][view_ref.as_range()]
594                };
595                value_byte_indices.push(buffer.len());
596                // here's where we write the string lengths
597                buffer.extend_trusted(ViewLen::try_from(value.len())?.to_le_bytes().into_iter());
598                buffer.extend_from_slice(value);
599            }
600            (buffer.freeze(), value_byte_indices)
601        }
602    };
603    Ok(buffer_and_value_byte_indices)
604}
605
606/// Reconstruct BinaryView structs from length-prefixed byte data.
607///
608/// The buffer contains interleaved u32 lengths (little-endian) and string data.
609/// When the cumulative data exceeds `max_buffer_len`, the buffer is split (zero-copy) into
610/// multiple segments so that BinaryView's u32 offsets can address all data.
611///
612/// Pass [`MAX_BUFFER_LEN`] for `max_buffer_len` in production; a smaller value can be used in
613/// tests to exercise the splitting path without allocating >2 GiB.
614pub fn reconstruct_views(
615    buffer: &ByteBuffer,
616    start_buf_index: u32,
617    max_buffer_len: usize,
618) -> (Vec<ByteBuffer>, Buffer<BinaryView>) {
619    let (buffers, views, _) = walk_views(buffer, start_buf_index, max_buffer_len);
620    (buffers, views)
621}
622
623/// [`reconstruct_views`], but rejecting a buffer that the length prefixes do not tile exactly.
624fn try_reconstruct_views(
625    buffer: &ByteBuffer,
626    start_buf_index: u32,
627    max_buffer_len: usize,
628) -> VortexResult<(Vec<ByteBuffer>, Buffer<BinaryView>)> {
629    match walk_views(buffer, start_buf_index, max_buffer_len) {
630        (buffers, views, None) => Ok((buffers, views)),
631        (_, _, Some(error)) => Err(error),
632    }
633}
634
635/// Walks `buffer` until it is exhausted or a length prefix leaves it, returning what was decoded
636/// along with the error that stopped the walk.
637fn walk_views(
638    buffer: &ByteBuffer,
639    start_buf_index: u32,
640    max_buffer_len: usize,
641) -> (Vec<ByteBuffer>, Buffer<BinaryView>, Option<VortexError>) {
642    let mut views = BufferMut::<BinaryView>::empty();
643    let mut buffers = Vec::new();
644    let mut segment_start: usize = 0;
645    let mut offset = 0;
646    // Only a new segment changes the buffer index, so it is tracked instead of recomputed per view.
647    let mut buf_index = start_buf_index;
648    let mut error = None;
649
650    while offset < buffer.len() {
651        let str_len = match zstd_value_len(buffer.as_slice(), offset) {
652            Ok(str_len) => str_len,
653            Err(err) => {
654                error = Some(err);
655                break;
656            }
657        };
658
659        let value_data_offset = offset + size_of::<ViewLen>();
660        let local_offset = value_data_offset - segment_start;
661
662        if local_offset + str_len > max_buffer_len && offset > segment_start {
663            buffers.push(buffer.slice(segment_start..offset));
664            segment_start = offset;
665            let Some(next_index) = buf_index.checked_add(1) else {
666                error = Some(vortex_err!("Zstd values need more than u32::MAX buffers"));
667                break;
668            };
669            buf_index = next_index;
670        }
671
672        let Ok(local_offset) = u32::try_from(value_data_offset - segment_start) else {
673            error = Some(vortex_err!(
674                "Zstd value offset {} does not fit in u32; max_buffer_len {max_buffer_len} is too large",
675                value_data_offset - segment_start
676            ));
677            break;
678        };
679        let Some(value) = buffer.get(value_data_offset..value_data_offset + str_len) else {
680            error = Some(vortex_err!(
681                "Corrupt zstd value: {str_len} bytes at offset {value_data_offset} run past the \
682                 end of the {} byte frame buffer",
683                buffer.len()
684            ));
685            break;
686        };
687        views.push(BinaryView::make_view(value, buf_index, local_offset));
688        offset = value_data_offset + str_len;
689    }
690
691    if segment_start < buffer.len() {
692        buffers.push(buffer.slice(segment_start..buffer.len()));
693    }
694
695    (buffers, views.freeze(), error)
696}
697
698/// Narrows the views decoded from the frames down to the values a slice requests.
699fn slice_views(
700    views: &Buffer<BinaryView>,
701    range: Range<usize>,
702) -> VortexResult<Buffer<BinaryView>> {
703    vortex_ensure!(
704        range.end <= views.len(),
705        "Corrupt zstd metadata: values {}..{} are out of bounds of the {} values held by the \
706         decompressed frames",
707        range.start,
708        range.end,
709        views.len()
710    );
711    Ok(views.slice(range))
712}
713
714/// A zstd output buffer over uninitialized spare capacity.
715///
716/// `decompress_to_buffer` writes through a raw pointer and reports how many bytes it produced, so
717/// it never reads its destination — but handing it a `&mut [u8]` covering uninitialized memory
718/// would be undefined behaviour regardless of what it does with it. [`WriteBuf`] is the interface
719/// zstd provides for exactly this case, and it keeps the alternative (zeroing the whole buffer
720/// before every decompression) off the hot path.
721struct UninitDestination<'a> {
722    spare: &'a mut [MaybeUninit<u8>],
723    filled: usize,
724}
725
726impl<'a> UninitDestination<'a> {
727    fn new(spare: &'a mut [MaybeUninit<u8>]) -> Self {
728        Self { spare, filled: 0 }
729    }
730}
731
732// SAFETY: `as_mut_ptr` and `capacity` describe the whole spare region, so zstd only ever writes
733// within it, and `filled_until` merely records the count it reports. `as_slice` is bounded by that
734// count, so it never exposes a byte zstd did not write.
735unsafe impl WriteBuf for UninitDestination<'_> {
736    fn as_slice(&self) -> &[u8] {
737        // SAFETY: zstd reported writing `filled` bytes from the start of `spare`.
738        unsafe { std::slice::from_raw_parts(self.spare.as_ptr().cast::<u8>(), self.filled) }
739    }
740
741    fn capacity(&self) -> usize {
742        self.spare.len()
743    }
744
745    fn as_mut_ptr(&mut self) -> *mut u8 {
746        self.spare.as_mut_ptr().cast::<u8>()
747    }
748
749    unsafe fn filled_until(&mut self, n: usize) {
750        self.filled = n;
751    }
752}
753
754struct DecompressedSlice {
755    bytes: ByteBuffer,
756    validity: Validity,
757    byte_width: usize,
758    n_rows: usize,
759    value_idx_start: usize,
760    value_idx_stop: usize,
761    n_skipped_values: usize,
762    /// Number of stored values held by `bytes`, which covers whole frames and so may extend past
763    /// the requested slice on either side.
764    n_buffered_values: usize,
765}
766
767impl DecompressedSlice {
768    /// The range of stored values this slice requests, as an index into `bytes`.
769    ///
770    /// The frame metadata that drives `n_skipped_values` is untrusted, so a frame claiming to hold
771    /// values that precede the ones we decompressed is rejected instead of wrapping.
772    fn value_range(&self) -> VortexResult<Range<usize>> {
773        let start = self
774            .value_idx_start
775            .checked_sub(self.n_skipped_values)
776            .ok_or_else(|| {
777                vortex_err!(
778                    "Corrupt zstd metadata: skipped frames hold {} values, past the first \
779                     requested value {}",
780                    self.n_skipped_values,
781                    self.value_idx_start
782                )
783            })?;
784        let end = self
785            .value_idx_stop
786            .checked_sub(self.n_skipped_values)
787            .ok_or_else(|| {
788                vortex_err!(
789                    "Corrupt zstd metadata: skipped frames hold {} values, past the last \
790                     requested value {}",
791                    self.n_skipped_values,
792                    self.value_idx_stop
793                )
794            })?;
795        vortex_ensure!(
796            start <= end,
797            "Corrupt zstd metadata: value range {start}..{end} is not ascending"
798        );
799        Ok(start..end)
800    }
801
802    /// The bounds within `bytes` of the length-prefixed region that should hold exactly this
803    /// slice's values.
804    ///
805    /// Walking the length prefixes is a dependent load chain, so both ends are derived from the
806    /// slice metadata where possible: an unsliced array skips both walks. The far end is then a
807    /// claim rather than a checked fact, so a caller must hold the values it reads to the count
808    /// from [`Self::value_range`] — with [`ZstdValues`], whose shortfall shows up in the byte total
809    /// from [`Self::value_bytes`], or by counting decoded values as [`try_reconstruct_views`] does.
810    /// Otherwise a region ending part way through a value passes its trailing bytes off as values.
811    fn value_byte_range(&self) -> VortexResult<Range<usize>> {
812        let Range { start, end } = self.value_range()?;
813        let buffer = self.bytes.as_slice();
814        let from = zstd_value_offset(buffer, 0, start)?;
815        let to = if end == self.n_buffered_values {
816            buffer.len()
817        } else {
818            zstd_value_offset(buffer, from, end - start)?
819        };
820        vortex_ensure!(
821            from <= to && to <= buffer.len(),
822            "Corrupt zstd metadata: values {from}..{to} are out of bounds of the {} byte frame \
823             buffer",
824            buffer.len()
825        );
826        Ok(from..to)
827    }
828
829    /// The length-prefixed region of `bytes` holding exactly this slice's values, along with the
830    /// total size of those values once the length prefixes are dropped.
831    fn value_bytes(&self) -> VortexResult<(&[u8], usize)> {
832        let range = self.value_byte_range()?;
833        let n_values = self.value_range()?.len();
834        let buffer = self.bytes.as_slice();
835        let bytes = buffer.get(range.clone()).ok_or_else(|| {
836            vortex_err!(
837                "Corrupt zstd metadata: values {}..{} are out of bounds of the {} byte frame \
838                 buffer",
839                range.start,
840                range.end,
841                buffer.len()
842            )
843        })?;
844        // Every value carries a length prefix, so the region must be at least that large.
845        let prefix_bytes = n_values.checked_mul(size_of::<ViewLen>()).ok_or_else(|| {
846            vortex_err!("Corrupt zstd metadata: value count {n_values} overflows a byte count")
847        })?;
848        let num_bytes = bytes.len().checked_sub(prefix_bytes).ok_or_else(|| {
849            vortex_err!(
850                "Corrupt zstd metadata: {n_values} values do not fit in the {} bytes holding them",
851                bytes.len()
852            )
853        })?;
854        Ok((bytes, num_bytes))
855    }
856}
857
858/// Returns the byte offset `count` length-prefixed values past `offset`.
859///
860/// Each step is bounds-checked by the next prefix read, so only the offset the walk lands on needs
861/// a check of its own.
862fn zstd_value_offset(buffer: &[u8], mut offset: usize, count: usize) -> VortexResult<usize> {
863    for _ in 0..count {
864        offset += size_of::<ViewLen>() + zstd_value_len(buffer, offset)?;
865    }
866    vortex_ensure!(
867        offset <= buffer.len(),
868        "Corrupt zstd values: walking {count} values ended at offset {offset}, past the end of \
869         the {} byte frame buffer",
870        buffer.len()
871    );
872    Ok(offset)
873}
874
875/// Reads the length prefix of the value starting at `offset`.
876#[inline]
877fn zstd_value_len(buffer: &[u8], offset: usize) -> VortexResult<usize> {
878    let prefix = buffer
879        .get(offset..)
880        .and_then(|rest| rest.first_chunk::<{ size_of::<ViewLen>() }>())
881        .ok_or_else(|| {
882            vortex_err!(
883                "Corrupt zstd values: length prefix at offset {offset} runs past the end of the \
884                 {} byte frame buffer",
885                buffer.len()
886            )
887        })?;
888    Ok(ViewLen::from_le_bytes(*prefix) as usize)
889}
890
891/// A forward walk over the values of a length-prefixed region.
892struct ZstdValues<'a> {
893    buffer: &'a [u8],
894    offset: usize,
895}
896
897impl<'a> ZstdValues<'a> {
898    fn new(buffer: &'a [u8]) -> Self {
899        Self { buffer, offset: 0 }
900    }
901
902    /// The next value, or every byte the walk could not decode once a prefix leaves the region.
903    ///
904    /// The remainder is what makes a shortfall visible in the byte total alone, without a second
905    /// walk to validate the region up front. A caller sizes that total as the region minus one
906    /// prefix per value, so `k` of `n` values decoded leaves it still expecting the `n - k`
907    /// prefixes the walk abandoned; the remainder covers those and more, or is empty only because
908    /// the region ended exactly and the decoded bytes already fall short. Neither can add up.
909    fn next_value(&mut self) -> &'a [u8] {
910        let value_start = self.offset + size_of::<ViewLen>();
911        let value = zstd_value_len(self.buffer, self.offset)
912            .ok()
913            .and_then(|len| self.buffer.get(value_start..value_start.checked_add(len)?));
914        match value {
915            Some(value) => {
916                self.offset = value_start + value.len();
917                value
918            }
919            None => &self.buffer[self.offset..],
920        }
921    }
922}
923
924impl ZstdData {
925    /// Construct unsliced zstd data from raw frames and metadata.
926    pub fn new(
927        dictionary: Option<ByteBuffer>,
928        frames: Vec<ByteBuffer>,
929        metadata: ZstdMetadata,
930        n_rows: usize,
931    ) -> Self {
932        Self {
933            dictionary,
934            frames,
935            metadata,
936            unsliced_n_rows: n_rows,
937            slice_start: 0,
938            slice_stop: n_rows,
939        }
940    }
941
942    /// Validate dtype, slice, validity, frame, and dictionary invariants.
943    pub fn validate(&self, dtype: &DType, len: usize, validity: &Validity) -> VortexResult<()> {
944        vortex_ensure!(
945            matches!(
946                dtype,
947                DType::Primitive(..) | DType::Binary(_) | DType::Utf8(_)
948            ),
949            "Unsupported dtype for Zstd array: {dtype}"
950        );
951        vortex_ensure!(
952            self.slice_start <= self.slice_stop,
953            "Invalid slice range {}..{}",
954            self.slice_start,
955            self.slice_stop
956        );
957        vortex_ensure!(
958            self.slice_stop <= self.unsliced_n_rows,
959            "Slice stop {} exceeds unsliced row count {}",
960            self.slice_stop,
961            self.unsliced_n_rows
962        );
963        vortex_ensure!(
964            self.slice_stop - self.slice_start == len,
965            "Slice length {} does not match array length {}",
966            self.slice_stop - self.slice_start,
967            len
968        );
969        if let Some(validity_len) = validity.maybe_len() {
970            vortex_ensure!(
971                validity_len == self.unsliced_n_rows,
972                "Validity length {} does not match unsliced row count {}",
973                validity_len,
974                self.unsliced_n_rows
975            );
976        }
977
978        match &self.dictionary {
979            Some(dictionary) => vortex_ensure!(
980                usize::try_from(self.metadata.dictionary_size)? == dictionary.len(),
981                "Dictionary size metadata {} does not match buffer size {}",
982                self.metadata.dictionary_size,
983                dictionary.len()
984            ),
985            None => vortex_ensure!(
986                self.metadata.dictionary_size == 0,
987                "Dictionary metadata present without dictionary buffer"
988            ),
989        }
990        vortex_ensure!(
991            self.frames.len() == self.metadata.frames.len(),
992            "Frame count {} does not match metadata frame count {}",
993            self.frames.len(),
994            self.metadata.frames.len()
995        );
996        for (index, (frame, metadata)) in self.frames.iter().zip(&self.metadata.frames).enumerate()
997        {
998            validate_frame_content_size(frame.as_slice(), metadata.uncompressed_size, index)?;
999        }
1000
1001        Ok(())
1002    }
1003
1004    pub(crate) fn with_slice(&self, start: usize, stop: usize) -> Self {
1005        let new_start = self.slice_start + start;
1006        let new_stop = self.slice_start + stop;
1007
1008        assert!(
1009            new_start <= self.slice_stop,
1010            "new slice start {new_start} exceeds end {}",
1011            self.slice_stop
1012        );
1013
1014        assert!(
1015            new_stop <= self.slice_stop,
1016            "new slice stop {new_stop} exceeds end {}",
1017            self.slice_stop
1018        );
1019
1020        Self {
1021            slice_start: new_start,
1022            slice_stop: new_stop,
1023            ..self.clone()
1024        }
1025    }
1026
1027    fn compress_values(
1028        value_bytes: &ByteBuffer,
1029        frame_byte_starts: &[usize],
1030        level: i32,
1031        values_per_frame: usize,
1032        n_values: usize,
1033        use_dictionary: bool,
1034    ) -> VortexResult<Frames> {
1035        let n_frames = frame_byte_starts.len();
1036
1037        // Would-be sample sizes if we end up applying zstd dictionary
1038        let mut sample_sizes = Vec::with_capacity(n_frames);
1039        for i in 0..n_frames {
1040            let frame_byte_end = frame_byte_starts
1041                .get(i + 1)
1042                .copied()
1043                .unwrap_or(value_bytes.len());
1044            sample_sizes.push(frame_byte_end - frame_byte_starts[i]);
1045        }
1046        debug_assert_eq!(sample_sizes.iter().sum::<usize>(), value_bytes.len());
1047
1048        let (dictionary, mut compressor) = if !use_dictionary
1049            || sample_sizes.len() < MIN_SAMPLES_FOR_DICTIONARY
1050        {
1051            // no dictionary
1052            (None, zstd::bulk::Compressor::new(level)?)
1053        } else {
1054            // with dictionary
1055            let max_dict_size = choose_max_dict_size(value_bytes.len());
1056            let dict = zstd::dict::from_continuous(value_bytes, &sample_sizes, max_dict_size)
1057                .map_err(|err| VortexError::from(err).with_context("while training dictionary"))?;
1058
1059            let compressor = zstd::bulk::Compressor::with_dictionary(level, &dict)?;
1060            (Some(ByteBuffer::from(dict)), compressor)
1061        };
1062
1063        let mut frame_metas = vec![];
1064        let mut frames = vec![];
1065        for i in 0..n_frames {
1066            let frame_byte_end = frame_byte_starts
1067                .get(i + 1)
1068                .copied()
1069                .unwrap_or(value_bytes.len());
1070
1071            let uncompressed = &value_bytes.slice(frame_byte_starts[i]..frame_byte_end);
1072            let mut compressed = compressor
1073                .compress(uncompressed)
1074                .map_err(|err| VortexError::from(err).with_context("while compressing"))?;
1075            compressed.shrink_to_fit();
1076            frame_metas.push(ZstdFrameMetadata {
1077                uncompressed_size: uncompressed.len() as u64,
1078                n_values: values_per_frame.min(n_values - i * values_per_frame) as u64,
1079            });
1080            frames.push(ByteBuffer::from(compressed));
1081        }
1082
1083        Ok(Frames {
1084            dictionary,
1085            frames,
1086            frame_metas,
1087        })
1088    }
1089
1090    /// Creates a ZstdArray from a primitive array.
1091    ///
1092    /// # Arguments
1093    /// * `parray` - The primitive array to compress
1094    /// * `level` - Zstd compression level (0 = default, negative = fast, positive = better compression)
1095    /// * `values_per_frame` - Number of values per frame (0 = single frame)
1096    pub fn from_primitive(
1097        parray: &PrimitiveArray,
1098        level: i32,
1099        values_per_frame: usize,
1100        ctx: &mut ExecutionCtx,
1101    ) -> VortexResult<Self> {
1102        Self::from_primitive_impl(parray, level, values_per_frame, true, ctx)
1103    }
1104
1105    /// Creates a ZstdArray from a primitive array without using a dictionary.
1106    ///
1107    /// This is useful when the compressed data will be decompressed by systems
1108    /// that don't support ZSTD dictionaries (e.g., nvCOMP on GPU).
1109    ///
1110    /// Note: Without a dictionary, each frame is compressed independently.
1111    /// Dictionaries are trained from sample data from previously seen frames,
1112    /// to improve compression ratio.
1113    ///
1114    /// # Arguments
1115    /// * `parray` - The primitive array to compress
1116    /// * `level` - Zstd compression level (0 = default, negative = fast, positive = better compression)
1117    /// * `values_per_frame` - Number of values per frame (0 = single frame)
1118    pub fn from_primitive_without_dict(
1119        parray: &PrimitiveArray,
1120        level: i32,
1121        values_per_frame: usize,
1122        ctx: &mut ExecutionCtx,
1123    ) -> VortexResult<Self> {
1124        Self::from_primitive_impl(parray, level, values_per_frame, false, ctx)
1125    }
1126
1127    fn from_primitive_impl(
1128        parray: &PrimitiveArray,
1129        level: i32,
1130        values_per_frame: usize,
1131        use_dictionary: bool,
1132        ctx: &mut ExecutionCtx,
1133    ) -> VortexResult<Self> {
1134        let byte_width = parray.ptype().byte_width();
1135
1136        // We compress only the valid elements.
1137        let values = collect_valid_primitive(parray, ctx)?;
1138        let n_values = values.len();
1139        let values_per_frame = if values_per_frame > 0 {
1140            values_per_frame
1141        } else {
1142            n_values
1143        };
1144
1145        let value_bytes = values.buffer_handle().try_to_host_sync()?;
1146        // Align frames to buffer alignment. This is necessary for overaligned buffers.
1147        let alignment = value_bytes.alignment().as_usize();
1148        let step_width = (values_per_frame * byte_width).div_ceil(alignment) * alignment;
1149
1150        let frame_byte_starts = (0..n_values * byte_width)
1151            .step_by(step_width)
1152            .collect::<Vec<_>>();
1153        let Frames {
1154            dictionary,
1155            frames,
1156            frame_metas,
1157        } = Self::compress_values(
1158            &value_bytes,
1159            &frame_byte_starts,
1160            level,
1161            values_per_frame,
1162            n_values,
1163            use_dictionary,
1164        )?;
1165
1166        let metadata = ZstdMetadata {
1167            dictionary_size: dictionary
1168                .as_ref()
1169                .map_or(0, |dict| dict.len())
1170                .try_into()?,
1171            frames: frame_metas,
1172        };
1173
1174        Ok(ZstdData::new(dictionary, frames, metadata, parray.len()))
1175    }
1176
1177    /// Creates a ZstdArray from a VarBinView array.
1178    ///
1179    /// # Arguments
1180    /// * `vbv` - The VarBinView array to compress
1181    /// * `level` - Zstd compression level (0 = default, negative = fast, positive = better compression)
1182    /// * `values_per_frame` - Number of values per frame (0 = single frame)
1183    pub fn from_var_bin_view(
1184        vbv: &VarBinViewArray,
1185        level: i32,
1186        values_per_frame: usize,
1187        ctx: &mut ExecutionCtx,
1188    ) -> VortexResult<Self> {
1189        Self::from_var_bin_view_impl(vbv, level, values_per_frame, true, ctx)
1190    }
1191
1192    /// Creates a ZstdArray from a VarBinView array without using a dictionary.
1193    ///
1194    /// This is useful when the compressed data will be decompressed by systems
1195    /// that don't support ZSTD dictionaries (e.g., nvCOMP on GPU).
1196    ///
1197    /// Note: Without a dictionary, each frame is compressed independently.
1198    /// Dictionaries are trained from sample data from previously seen frames,
1199    /// to improve compression ratio.
1200    ///
1201    /// # Arguments
1202    /// * `vbv` - The VarBinView array to compress
1203    /// * `level` - Zstd compression level (0 = default, negative = fast, positive = better compression)
1204    /// * `values_per_frame` - Number of values per frame (0 = single frame)
1205    pub fn from_var_bin_view_without_dict(
1206        vbv: &VarBinViewArray,
1207        level: i32,
1208        values_per_frame: usize,
1209        ctx: &mut ExecutionCtx,
1210    ) -> VortexResult<Self> {
1211        Self::from_var_bin_view_impl(vbv, level, values_per_frame, false, ctx)
1212    }
1213
1214    fn from_var_bin_view_impl(
1215        vbv: &VarBinViewArray,
1216        level: i32,
1217        values_per_frame: usize,
1218        use_dictionary: bool,
1219        ctx: &mut ExecutionCtx,
1220    ) -> VortexResult<Self> {
1221        // Approach for strings: we prefix each string with its length as a u32.
1222        // This is the same as what Parquet does. In some cases it may be better
1223        // to separate the binary data and lengths as two separate streams, but
1224        // this approach is simpler and can be best in cases when there is
1225        // mutual information between strings and their lengths.
1226        // We compress only the valid elements.
1227        let (value_bytes, value_byte_indices) = collect_valid_vbv(vbv, ctx)?;
1228        let n_values = value_byte_indices.len();
1229        let values_per_frame = if values_per_frame > 0 {
1230            values_per_frame
1231        } else {
1232            n_values
1233        };
1234
1235        let frame_byte_starts = (0..n_values)
1236            .step_by(values_per_frame)
1237            .map(|i| value_byte_indices[i])
1238            .collect::<Vec<_>>();
1239        let Frames {
1240            dictionary,
1241            frames,
1242            frame_metas,
1243        } = Self::compress_values(
1244            &value_bytes,
1245            &frame_byte_starts,
1246            level,
1247            values_per_frame,
1248            n_values,
1249            use_dictionary,
1250        )?;
1251
1252        let metadata = ZstdMetadata {
1253            dictionary_size: dictionary
1254                .as_ref()
1255                .map_or(0, |dict| dict.len())
1256                .try_into()?,
1257            frames: frame_metas,
1258        };
1259        Ok(ZstdData::new(dictionary, frames, metadata, vbv.len()))
1260    }
1261
1262    /// Compress a supported canonical array into zstd data.
1263    ///
1264    /// Returns `Ok(None)` for canonical variants that this encoding does not support.
1265    pub fn from_canonical(
1266        canonical: &Canonical,
1267        level: i32,
1268        values_per_frame: usize,
1269        ctx: &mut ExecutionCtx,
1270    ) -> VortexResult<Option<Self>> {
1271        match canonical {
1272            Canonical::Primitive(parray) => Ok(Some(ZstdData::from_primitive(
1273                parray,
1274                level,
1275                values_per_frame,
1276                ctx,
1277            )?)),
1278            Canonical::VarBinView(vbv) => Ok(Some(ZstdData::from_var_bin_view(
1279                vbv,
1280                level,
1281                values_per_frame,
1282                ctx,
1283            )?)),
1284            _ => Ok(None),
1285        }
1286    }
1287
1288    /// Canonicalize and compress an array into zstd data.
1289    ///
1290    /// # Errors
1291    ///
1292    /// Returns an error if the array's canonical form is unsupported or compression fails.
1293    pub fn from_array(
1294        array: ArrayRef,
1295        level: i32,
1296        values_per_frame: usize,
1297        ctx: &mut ExecutionCtx,
1298    ) -> VortexResult<Self> {
1299        let canonical = array.execute::<Canonical>(ctx)?;
1300        Self::from_canonical(&canonical, level, values_per_frame, ctx)?
1301            .ok_or_else(|| vortex_err!("Zstd can only encode Primitive and VarBinView arrays"))
1302    }
1303
1304    fn byte_width(dtype: &DType) -> usize {
1305        if dtype.is_primitive() {
1306            dtype.as_ptype().byte_width()
1307        } else {
1308            1
1309        }
1310    }
1311
1312    fn decompress_slice(
1313        &self,
1314        dtype: &DType,
1315        unsliced_validity: &Validity,
1316        ctx: &mut ExecutionCtx,
1317    ) -> VortexResult<DecompressedSlice> {
1318        // To start, we figure out which frames we need to decompress, and with
1319        // what row offset into the first such frame.
1320        let byte_width = Self::byte_width(dtype);
1321        let slice_n_rows = self.slice_stop - self.slice_start;
1322        let unsliced_mask = unsliced_validity.execute_mask(self.unsliced_n_rows, ctx)?;
1323        let slice_value_indices =
1324            unsliced_mask.valid_counts_for_indices(&[self.slice_start, self.slice_stop]);
1325
1326        let slice_value_idx_start = slice_value_indices[0];
1327        let slice_value_idx_stop = slice_value_indices[1];
1328
1329        let mut frames_to_decompress = vec![];
1330        let mut value_idx_start = 0;
1331        let mut uncompressed_size_to_decompress = 0usize;
1332        let mut n_skipped_values = 0;
1333        let mut n_buffered_values = 0;
1334        for (frame, frame_meta) in self.frames.iter().zip(&self.metadata.frames) {
1335            if value_idx_start >= slice_value_idx_stop {
1336                break;
1337            }
1338
1339            let frame_uncompressed_size =
1340                usize::try_from(frame_meta.uncompressed_size).map_err(|_| {
1341                    vortex_err!(
1342                        "Zstd frame uncompressed size {} does not fit in a usize",
1343                        frame_meta.uncompressed_size
1344                    )
1345                })?;
1346            let frame_n_values = if frame_meta.n_values != 0 {
1347                usize::try_from(frame_meta.n_values).map_err(|_| {
1348                    vortex_err!(
1349                        "Zstd frame value count {} does not fit in a usize",
1350                        frame_meta.n_values
1351                    )
1352                })?
1353            } else if dtype.is_primitive() {
1354                // Possibly older primitive-only metadata that just didn't store this. Fixed-width
1355                // values make the byte count an exact value count.
1356                frame_uncompressed_size / byte_width
1357            } else {
1358                // The same fallback would read a byte count as a value count for variable-width
1359                // values, which misattributes values to frames. A single frame holds every stored
1360                // value, so that case is still recoverable; anything else is not.
1361                vortex_ensure!(
1362                    self.frames.len() == 1,
1363                    "Zstd frame metadata for a variable-width array is missing its value count"
1364                );
1365                unsliced_mask.true_count()
1366            };
1367
1368            // Bounding the running total also bounds the two accumulators below, which partition
1369            // it between the frames we keep and the ones we skip.
1370            let value_idx_stop = value_idx_start.checked_add(frame_n_values).ok_or_else(|| {
1371                vortex_err!("Corrupt zstd metadata: frame value counts overflow a usize")
1372            })?;
1373            if value_idx_stop > slice_value_idx_start {
1374                // we need this frame
1375                frames_to_decompress.push(frame);
1376                uncompressed_size_to_decompress = uncompressed_size_to_decompress
1377                    .checked_add(frame_uncompressed_size)
1378                    .ok_or_else(|| {
1379                        vortex_err!("Corrupt zstd metadata: frame sizes overflow a usize")
1380                    })?;
1381                n_buffered_values += frame_n_values;
1382            } else {
1383                n_skipped_values += frame_n_values;
1384            }
1385            value_idx_start = value_idx_stop;
1386        }
1387
1388        // then we actually decompress those frames
1389        let mut decompressor = if let Some(dictionary) = &self.dictionary {
1390            zstd::bulk::Decompressor::with_dictionary(dictionary)?
1391        } else {
1392            zstd::bulk::Decompressor::new()?
1393        };
1394        let mut decompressed = ByteBufferMut::with_capacity_aligned(
1395            uncompressed_size_to_decompress,
1396            Alignment::new(byte_width),
1397        );
1398        let mut uncompressed_start = 0;
1399        for frame in frames_to_decompress {
1400            // Decompress straight into the spare capacity. Each frame gets only the region after
1401            // the ones before it, bounded by the size the metadata declared, so a frame that
1402            // expands further than advertised is refused by zstd rather than overrunning.
1403            let mut destination = UninitDestination::new(
1404                &mut decompressed.spare_capacity_mut()
1405                    [uncompressed_start..uncompressed_size_to_decompress],
1406            );
1407            uncompressed_start +=
1408                decompressor.decompress_to_buffer(frame.as_slice(), &mut destination)?;
1409        }
1410        if uncompressed_start != uncompressed_size_to_decompress {
1411            vortex_bail!(
1412                "Zstd metadata or frames were corrupt; expected {} bytes but decompressed {}",
1413                uncompressed_size_to_decompress,
1414                uncompressed_start
1415            );
1416        }
1417        // SAFETY: the loop above decompressed exactly `uncompressed_start` bytes into the front of
1418        // the spare capacity, and the check above pins that to the requested length.
1419        unsafe { decompressed.set_len(uncompressed_start) };
1420
1421        let decompressed = decompressed.freeze();
1422        // Last, we slice the exact values requested out of the decompressed data.
1423        let mut slice_validity = unsliced_validity.slice(self.slice_start..self.slice_stop)?;
1424
1425        // NOTE: this block handles setting the output type when the validity and DType disagree.
1426        //
1427        // ZSTD is a compact block compressor, meaning that null values are not stored inline in
1428        // the data frames. A ZSTD Array that was initialized must always hold onto its full
1429        // validity bitmap, even if sliced to only include non-null values.
1430        //
1431        // We ensure that the validity of the decompressed array ALWAYS matches the validity
1432        // implied by the DType.
1433        if !dtype.is_nullable() && !matches!(slice_validity, Validity::NonNullable) {
1434            vortex_ensure!(
1435                matches!(slice_validity, Validity::AllValid),
1436                "ZSTD array expects to be non-nullable but there are nulls after decompression"
1437            );
1438
1439            slice_validity = Validity::NonNullable;
1440        } else if dtype.is_nullable() && matches!(slice_validity, Validity::NonNullable) {
1441            slice_validity = Validity::AllValid;
1442        }
1443        // END OF IMPORTANT BLOCK
1444        //
1445
1446        Ok(DecompressedSlice {
1447            bytes: decompressed,
1448            validity: slice_validity,
1449            byte_width,
1450            n_rows: slice_n_rows,
1451            value_idx_start: slice_value_idx_start,
1452            value_idx_stop: slice_value_idx_stop,
1453            n_skipped_values,
1454            n_buffered_values,
1455        })
1456    }
1457
1458    fn decompress(
1459        &self,
1460        dtype: &DType,
1461        unsliced_validity: &Validity,
1462        ctx: &mut ExecutionCtx,
1463    ) -> VortexResult<ArrayRef> {
1464        let slice = self.decompress_slice(dtype, unsliced_validity, ctx)?;
1465        match dtype {
1466            DType::Primitive(..) => {
1467                let Range { start, end } = slice.value_range()?;
1468                let byte_range = start
1469                    .checked_mul(slice.byte_width)
1470                    .zip(end.checked_mul(slice.byte_width))
1471                    .filter(|(_, byte_stop)| *byte_stop <= slice.bytes.len())
1472                    .map(|(byte_start, byte_stop)| byte_start..byte_stop)
1473                    .ok_or_else(|| {
1474                        vortex_err!(
1475                            "Corrupt zstd metadata: values {start}..{end} of {} bytes each are \
1476                             out of bounds of the {} byte frame buffer",
1477                            slice.byte_width,
1478                            slice.bytes.len()
1479                        )
1480                    })?;
1481                let slice_values_buffer = slice.bytes.slice(byte_range);
1482                let primitive = PrimitiveArray::from_values_byte_buffer(
1483                    slice_values_buffer,
1484                    dtype.as_ptype(),
1485                    slice.validity,
1486                    slice.n_rows,
1487                    ctx,
1488                );
1489
1490                Ok(primitive.into_array())
1491            }
1492            DType::Binary(_) | DType::Utf8(_) => {
1493                match slice.validity.execute_mask(slice.n_rows, ctx)?.indices() {
1494                    AllOr::All => {
1495                        let (buffers, all_views) =
1496                            try_reconstruct_views(&slice.bytes, 0, MAX_BUFFER_LEN)?;
1497                        let valid_views = slice_views(&all_views, slice.value_range()?)?;
1498
1499                        // SAFETY: we properly construct the views inside `reconstruct_views`
1500                        Ok(unsafe {
1501                            VarBinViewArray::new_unchecked(
1502                                valid_views,
1503                                Arc::from(buffers),
1504                                dtype.clone(),
1505                                slice.validity,
1506                            )
1507                        }
1508                        .into_array())
1509                    }
1510                    AllOr::None => Ok(ConstantArray::new(
1511                        Scalar::null(dtype.clone()),
1512                        slice.n_rows,
1513                    )
1514                    .into_array()),
1515                    AllOr::Some(valid_indices) => {
1516                        let (buffers, all_views) =
1517                            try_reconstruct_views(&slice.bytes, 0, MAX_BUFFER_LEN)?;
1518                        let valid_views = slice_views(&all_views, slice.value_range()?)?;
1519
1520                        let mut views = BufferMut::<BinaryView>::zeroed(slice.n_rows);
1521                        for (view, index) in valid_views.into_iter().zip_eq(valid_indices) {
1522                            views[*index] = view
1523                        }
1524
1525                        // SAFETY: we properly construct the views inside `reconstruct_views`
1526                        Ok(unsafe {
1527                            VarBinViewArray::new_unchecked(
1528                                views.freeze(),
1529                                Arc::from(buffers),
1530                                dtype.clone(),
1531                                slice.validity,
1532                            )
1533                        }
1534                        .into_array())
1535                    }
1536                }
1537            }
1538            _ => vortex_bail!("Unsupported dtype for Zstd array: {}", dtype),
1539        }
1540    }
1541
1542    /// Returns the length of the array.
1543    #[inline]
1544    pub fn len(&self) -> usize {
1545        self.slice_stop - self.slice_start
1546    }
1547
1548    /// Returns whether the array is empty.
1549    #[inline]
1550    pub fn is_empty(&self) -> bool {
1551        self.slice_stop == self.slice_start
1552    }
1553
1554    /// Split this data into movable parts, attaching the supplied validity.
1555    pub fn into_parts(self, validity: Validity) -> ZstdDataParts {
1556        ZstdDataParts {
1557            dictionary: self.dictionary,
1558            frames: self.frames,
1559            metadata: self.metadata,
1560            validity,
1561            n_rows: self.unsliced_n_rows,
1562            slice_start: self.slice_start,
1563            slice_stop: self.slice_stop,
1564        }
1565    }
1566
1567    pub(crate) fn slice_start(&self) -> usize {
1568        self.slice_start
1569    }
1570
1571    pub(crate) fn slice_stop(&self) -> usize {
1572        self.slice_stop
1573    }
1574
1575    pub(crate) fn unsliced_n_rows(&self) -> usize {
1576        self.unsliced_n_rows
1577    }
1578}
1579
1580impl ValidityVTable<Zstd> for Zstd {
1581    fn validity(array: ArrayView<'_, Zstd>) -> VortexResult<Validity> {
1582        let unsliced_validity = child_to_validity(
1583            array.slots()[ZstdSlots::VALIDITY].as_ref(),
1584            array.dtype().nullability(),
1585        );
1586        unsliced_validity.slice(array.slice_start()..array.slice_stop())
1587    }
1588}
1589
1590impl OperationsVTable<Zstd> for Zstd {
1591    fn scalar_at(
1592        array: ArrayView<'_, Zstd>,
1593        index: usize,
1594        ctx: &mut ExecutionCtx,
1595    ) -> VortexResult<Scalar> {
1596        let unsliced_validity = child_to_validity(
1597            array.slots()[ZstdSlots::VALIDITY].as_ref(),
1598            array.dtype().nullability(),
1599        );
1600        let sliced = array.data().with_slice(index, index + 1);
1601        sliced
1602            .decompress(array.dtype(), &unsliced_validity, ctx)?
1603            .execute_scalar(0, ctx)
1604    }
1605}
1606
1607#[cfg(test)]
1608#[expect(clippy::cast_possible_truncation)]
1609mod tests {
1610    use rstest::rstest;
1611    use vortex_array::arrays::varbin::VarBinArrayExt as _;
1612    use vortex_array::builders::VarBinBuilder;
1613    use vortex_array::dtype::DType;
1614    use vortex_array::dtype::Nullability::NonNullable;
1615    use vortex_array::validity::Validity;
1616    use vortex_buffer::ByteBuffer;
1617    use vortex_error::VortexResult;
1618    use vortex_mask::Mask;
1619
1620    use super::DecompressedSlice;
1621    use super::ViewLen;
1622    use super::append_slice_to_varbin;
1623    use super::reconstruct_views;
1624    use super::try_reconstruct_views;
1625    use super::zstd_value_len;
1626    use super::zstd_value_offset;
1627    use crate::array::BinaryView;
1628
1629    /// Build a Zstd-style interleaved buffer: [u32-LE length][string bytes] repeated.
1630    fn make_interleaved(strings: &[&[u8]]) -> ByteBuffer {
1631        let mut buf = Vec::new();
1632        for s in strings {
1633            let len = s.len() as u32;
1634            buf.extend_from_slice(&len.to_le_bytes());
1635            buf.extend_from_slice(s);
1636        }
1637        ByteBuffer::copy_from(buf.as_slice())
1638    }
1639
1640    /// A slice over `bytes` that requests `value_idx_start..value_idx_stop`.
1641    fn decompressed_slice(
1642        bytes: ByteBuffer,
1643        value_idx_start: usize,
1644        value_idx_stop: usize,
1645        n_skipped_values: usize,
1646        n_buffered_values: usize,
1647    ) -> DecompressedSlice {
1648        DecompressedSlice {
1649            bytes,
1650            validity: Validity::NonNullable,
1651            byte_width: 1,
1652            n_rows: value_idx_stop - value_idx_start,
1653            value_idx_start,
1654            value_idx_stop,
1655            n_skipped_values,
1656            n_buffered_values,
1657        }
1658    }
1659
1660    #[test]
1661    fn test_reconstruct_views_no_split() {
1662        let strings: &[&[u8]] = &[b"hello", b"world"];
1663        let buf = make_interleaved(strings);
1664        let (buffers, views) = reconstruct_views(&buf, 0, 1024);
1665
1666        assert_eq!(buffers.len(), 1);
1667        assert_eq!(views.len(), 2);
1668        // Each entry: [u32 len (4 bytes)][data], so offsets are 4 and 4+5+4=13
1669        assert_eq!(views[0], BinaryView::make_view(b"hello", 0, 4));
1670        assert_eq!(views[1], BinaryView::make_view(b"world", 0, 13));
1671    }
1672
1673    #[test]
1674    fn test_reconstruct_views_split_across_segments() {
1675        // "aaaaaaaaaaaaa" (13 bytes) and "bbbbbbbbbbbbb" (13 bytes).
1676        // Each entry occupies 4 (length prefix) + 13 (data) = 17 bytes.
1677        // With max_buffer_len=20, the second entry's data (offset 4+13+4=21) exceeds the limit,
1678        // so it rolls into a second segment.
1679        let strings: &[&[u8]] = &[b"aaaaaaaaaaaaa", b"bbbbbbbbbbbbb"];
1680        let buf = make_interleaved(strings);
1681        let (buffers, views) = reconstruct_views(&buf, 0, 20);
1682
1683        assert_eq!(buffers.len(), 2);
1684        assert_eq!(views.len(), 2);
1685        assert_eq!(views[0], BinaryView::make_view(b"aaaaaaaaaaaaa", 0, 4));
1686        // Second entry starts a new segment at byte 17 (the length prefix), so local offset = 4.
1687        assert_eq!(views[1], BinaryView::make_view(b"bbbbbbbbbbbbb", 1, 4));
1688    }
1689
1690    /// A buffer whose last entry claims more bytes than remain, as corrupt frame data would.
1691    fn make_overrunning() -> ByteBuffer {
1692        let mut buf = Vec::new();
1693        buf.extend_from_slice(&5u32.to_le_bytes());
1694        buf.extend_from_slice(b"hello");
1695        buf.extend_from_slice(&9u32.to_le_bytes());
1696        buf.extend_from_slice(b"ab");
1697        ByteBuffer::copy_from(buf.as_slice())
1698    }
1699
1700    #[test]
1701    fn test_reconstruct_views_rejects_overrunning_value() {
1702        let buf = make_overrunning();
1703        assert!(try_reconstruct_views(&buf, 0, 1024).is_err());
1704
1705        // The lenient walk keeps the decodable prefix instead of panicking.
1706        let (buffers, views) = reconstruct_views(&buf, 0, 1024);
1707        assert_eq!(buffers.len(), 1);
1708        assert_eq!(views.len(), 1);
1709        assert_eq!(views[0], BinaryView::make_view(b"hello", 0, 4));
1710    }
1711
1712    #[test]
1713    fn test_reconstruct_views_rejects_truncated_length_prefix() {
1714        // A trailing partial length prefix cannot start a value.
1715        let buf =
1716            ByteBuffer::copy_from([5u8, 0, 0, 0, b'h', b'e', b'l', b'l', b'o', 1, 0].as_ref());
1717        assert!(try_reconstruct_views(&buf, 0, 1024).is_err());
1718        assert_eq!(reconstruct_views(&buf, 0, 1024).1.len(), 1);
1719    }
1720
1721    #[rstest]
1722    #[case::truncated_buffer(&[0u8, 0, 0], 0)]
1723    #[case::truncated_tail(&[4u8, 0, 0, 0], 2)]
1724    #[case::offset_at_end(&[4u8, 0, 0, 0], 4)]
1725    #[case::offset_past_end(&[4u8, 0, 0, 0], 64)]
1726    fn test_zstd_value_len_rejects_out_of_bounds(#[case] buffer: &[u8], #[case] offset: usize) {
1727        assert!(zstd_value_len(buffer, offset).is_err());
1728    }
1729
1730    #[test]
1731    fn test_zstd_value_offset_rejects_walking_past_the_end() -> VortexResult<()> {
1732        let buf = make_interleaved(&[b"hello", b"world"]);
1733        assert_eq!(zstd_value_offset(buf.as_slice(), 0, 2)?, buf.len());
1734        // Only two values are stored, so the third step leaves the buffer.
1735        assert!(zstd_value_offset(buf.as_slice(), 0, 3).is_err());
1736        Ok(())
1737    }
1738
1739    #[test]
1740    fn test_value_range_rejects_skipping_past_the_requested_values() {
1741        // Frame metadata claiming more skipped values than the slice starts at would wrap.
1742        let slice = decompressed_slice(make_interleaved(&[b"hello"]), 2, 3, 4, 1);
1743        assert!(slice.value_range().is_err());
1744        assert!(slice.value_bytes().is_err());
1745    }
1746
1747    #[rstest]
1748    // The buffered value count matches, so both ends come from the metadata.
1749    #[case::exact_metadata(2)]
1750    // It does not, so the far end is walked instead.
1751    #[case::walked_end(9)]
1752    fn test_value_bytes_totals_the_stored_values(
1753        #[case] n_buffered_values: usize,
1754    ) -> VortexResult<()> {
1755        let buf = make_interleaved(&[b"hello", b"world"]);
1756        let slice = decompressed_slice(buf.clone(), 0, 2, 0, n_buffered_values);
1757        let (bytes, num_bytes) = slice.value_bytes()?;
1758        assert_eq!(bytes, buf.as_slice());
1759        assert_eq!(num_bytes, buf.len() - 2 * size_of::<ViewLen>());
1760        Ok(())
1761    }
1762
1763    #[test]
1764    fn test_value_bytes_rejects_more_values_than_the_buffer_holds() {
1765        // Frame metadata claims nine values but only two are stored.
1766        let slice = decompressed_slice(make_interleaved(&[b"hello", b"world"]), 0, 5, 0, 9);
1767        assert!(slice.value_bytes().is_err());
1768    }
1769
1770    #[test]
1771    fn test_append_to_varbin_copies_the_stored_values() -> VortexResult<()> {
1772        let slice = decompressed_slice(make_interleaved(&[b"hello", b"world"]), 0, 2, 0, 2);
1773        let mut builder = VarBinBuilder::<i32>::new_in(
1774            DType::Utf8(NonNullable),
1775            vortex_buffer::BufferAllocatorRef::static_ref(),
1776        );
1777        append_slice_to_varbin(&slice, &Mask::new_true(2), &mut builder)?;
1778
1779        let appended = builder.finish_into_varbin();
1780        assert_eq!(appended.bytes_at(0).as_slice(), b"hello");
1781        assert_eq!(appended.bytes_at(1).as_slice(), b"world");
1782        Ok(())
1783    }
1784
1785    #[test]
1786    fn test_append_to_varbin_rejects_a_dangling_length_prefix() {
1787        let mut buffer = Vec::new();
1788        buffer.extend_from_slice(&3u32.to_le_bytes());
1789        buffer.extend_from_slice(b"cat");
1790        // A prefix with no value after it. It takes up exactly the four bytes the missing value's
1791        // own prefix would have, so treating that value as empty still totals the byte count the
1792        // metadata implies and appends ["cat", ""].
1793        buffer.extend_from_slice(&1u32.to_le_bytes());
1794
1795        let slice = decompressed_slice(ByteBuffer::copy_from(buffer.as_slice()), 0, 2, 0, 2);
1796        let mut builder = VarBinBuilder::<i32>::new_in(
1797            DType::Utf8(NonNullable),
1798            vortex_buffer::BufferAllocatorRef::static_ref(),
1799        );
1800        assert!(append_slice_to_varbin(&slice, &Mask::new_true(2), &mut builder).is_err());
1801        // The builder rejected the values before committing any of them.
1802        assert_eq!(builder.finish_into_varbin().len(), 0);
1803    }
1804}