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::sync::Arc;
10
11use itertools::Itertools as _;
12use prost::Message as _;
13use vortex_array::Array;
14use vortex_array::ArrayEq;
15use vortex_array::ArrayHash;
16use vortex_array::ArrayId;
17use vortex_array::ArrayParts;
18use vortex_array::ArrayRef;
19use vortex_array::ArrayView;
20use vortex_array::Canonical;
21use vortex_array::EqMode;
22use vortex_array::ExecutionCtx;
23use vortex_array::ExecutionResult;
24use vortex_array::IntoArray;
25use vortex_array::arrays::ConstantArray;
26use vortex_array::arrays::PrimitiveArray;
27use vortex_array::arrays::VarBinViewArray;
28use vortex_array::arrays::varbinview::build_views::BinaryView;
29use vortex_array::arrays::varbinview::build_views::MAX_BUFFER_LEN;
30use vortex_array::buffer::BufferHandle;
31use vortex_array::dtype::DType;
32use vortex_array::scalar::Scalar;
33use vortex_array::serde::ArrayChildren;
34use vortex_array::smallvec::smallvec;
35use vortex_array::validity::Validity;
36use vortex_array::vtable::OperationsVTable;
37use vortex_array::vtable::VTable;
38use vortex_array::vtable::ValidityVTable;
39use vortex_array::vtable::child_to_validity;
40use vortex_array::vtable::validity_to_child;
41use vortex_buffer::Alignment;
42use vortex_buffer::Buffer;
43use vortex_buffer::BufferMut;
44use vortex_buffer::ByteBuffer;
45use vortex_buffer::ByteBufferMut;
46use vortex_error::VortexError;
47use vortex_error::VortexExpect;
48use vortex_error::VortexResult;
49use vortex_error::vortex_bail;
50use vortex_error::vortex_ensure;
51use vortex_error::vortex_err;
52use vortex_error::vortex_panic;
53use vortex_mask::AllOr;
54use vortex_session::VortexSession;
55use vortex_session::registry::CachedId;
56
57use crate::ZstdFrameMetadata;
58use crate::ZstdMetadata;
59
60// Zstd doesn't support training dictionaries on very few samples.
61const MIN_SAMPLES_FOR_DICTIONARY: usize = 8;
62type ViewLen = u32;
63
64// Overall approach here:
65// Zstd can be used on the whole array (values_per_frame = 0), resulting in a single Zstd
66// frame, or it can be used with a dictionary (values_per_frame < # values), resulting in
67// multiple Zstd frames sharing a common dictionary. This latter case is helpful if you
68// want somewhat faster access to slices or individual rows, allowing us to only
69// decompress the necessary frames.
70
71// Visually, during decompression, we have an interval of frames we're
72// decompressing and a tighter interval of the slice we actually care about.
73// |=============values (all valid elements)==============|
74// |<-skipped_uncompressed->|----decompressed-------------|
75//                              |------slice-------|
76//                              ^                  ^
77// |<-slice_uncompressed_start->|                  |
78// |<------------slice_uncompressed_stop---------->|
79// We then insert these values to the correct position using a primitive array
80// constructor.
81
82/// A [`Zstd`]-encoded Vortex array.
83pub type ZstdArray = Array<Zstd>;
84
85impl ArrayHash for ZstdData {
86    fn array_hash<H: Hasher>(&self, state: &mut H, accuracy: EqMode) {
87        match &self.dictionary {
88            Some(dict) => {
89                true.hash(state);
90                dict.array_hash(state, accuracy);
91            }
92            None => {
93                false.hash(state);
94            }
95        }
96        for frame in &self.frames {
97            frame.array_hash(state, accuracy);
98        }
99        self.unsliced_n_rows.hash(state);
100        self.slice_start.hash(state);
101        self.slice_stop.hash(state);
102    }
103}
104
105impl ArrayEq for ZstdData {
106    fn array_eq(&self, other: &Self, accuracy: EqMode) -> bool {
107        if !match (&self.dictionary, &other.dictionary) {
108            (Some(d1), Some(d2)) => d1.array_eq(d2, accuracy),
109            (None, None) => true,
110            _ => false,
111        } {
112            return false;
113        }
114        if self.frames.len() != other.frames.len() {
115            return false;
116        }
117        for (a, b) in self.frames.iter().zip(&other.frames) {
118            if !a.array_eq(b, accuracy) {
119                return false;
120            }
121        }
122        self.unsliced_n_rows == other.unsliced_n_rows
123            && self.slice_start == other.slice_start
124            && self.slice_stop == other.slice_stop
125    }
126}
127
128impl VTable for Zstd {
129    type TypedArrayData = ZstdData;
130
131    type OperationsVTable = Self;
132    type ValidityVTable = Self;
133
134    fn id(&self) -> ArrayId {
135        static ID: CachedId = CachedId::new("vortex.zstd");
136        *ID
137    }
138
139    fn validate(
140        &self,
141        data: &Self::TypedArrayData,
142        dtype: &DType,
143        len: usize,
144        slots: &[Option<ArrayRef>],
145    ) -> VortexResult<()> {
146        let validity = child_to_validity(slots[0].as_ref(), dtype.nullability());
147        data.validate(dtype, len, &validity)
148    }
149
150    fn nbuffers(array: ArrayView<'_, Self>) -> usize {
151        array.dictionary.is_some() as usize + array.frames.len()
152    }
153
154    fn buffer(array: ArrayView<'_, Self>, idx: usize) -> BufferHandle {
155        if let Some(dict) = &array.dictionary {
156            if idx == 0 {
157                return BufferHandle::new_host(dict.clone());
158            }
159            BufferHandle::new_host(array.frames[idx - 1].clone())
160        } else {
161            BufferHandle::new_host(array.frames[idx].clone())
162        }
163    }
164
165    fn buffer_name(array: ArrayView<'_, Self>, idx: usize) -> Option<String> {
166        if array.dictionary.is_some() {
167            if idx == 0 {
168                Some("dictionary".to_string())
169            } else {
170                Some(format!("frame_{}", idx - 1))
171            }
172        } else {
173            Some(format!("frame_{idx}"))
174        }
175    }
176
177    fn with_buffers(
178        &self,
179        array: ArrayView<'_, Self>,
180        buffers: &[BufferHandle],
181    ) -> VortexResult<ArrayParts<Self>> {
182        let mut data = array.data().clone();
183        if data.dictionary.is_some() {
184            let Some((dictionary, frames)) = buffers.split_first() else {
185                vortex_bail!("Expected dictionary buffer");
186            };
187            data.dictionary = Some(dictionary.clone().try_to_host_sync()?);
188            data.frames = frames
189                .iter()
190                .map(|buffer| buffer.clone().try_to_host_sync())
191                .collect::<VortexResult<Vec<_>>>()?;
192        } else {
193            data.frames = buffers
194                .iter()
195                .map(|buffer| buffer.clone().try_to_host_sync())
196                .collect::<VortexResult<Vec<_>>>()?;
197        }
198        Ok(
199            ArrayParts::new(self.clone(), array.dtype().clone(), array.len(), data)
200                .with_slots(array.slots().iter().cloned().collect()),
201        )
202    }
203
204    fn serialize(
205        array: ArrayView<'_, Self>,
206        _session: &VortexSession,
207    ) -> VortexResult<Option<Vec<u8>>> {
208        Ok(Some(array.metadata.clone().encode_to_vec()))
209    }
210
211    fn deserialize(
212        &self,
213        dtype: &DType,
214        len: usize,
215        metadata: &[u8],
216        buffers: &[BufferHandle],
217        children: &dyn ArrayChildren,
218        _session: &VortexSession,
219    ) -> VortexResult<ArrayParts<Self>> {
220        let metadata = ZstdMetadata::decode(metadata)?;
221        let validity = if children.is_empty() {
222            Validity::from(dtype.nullability())
223        } else if children.len() == 1 {
224            let validity = children.get(0, &Validity::DTYPE, len)?;
225            Validity::Array(validity)
226        } else {
227            vortex_bail!("ZstdArray expected 0 or 1 child, got {}", children.len());
228        };
229
230        let (dictionary_buffer, compressed_buffers) = if metadata.dictionary_size == 0 {
231            // no dictionary
232            (
233                None,
234                buffers
235                    .iter()
236                    .map(|b| b.clone().try_to_host_sync())
237                    .collect::<VortexResult<Vec<_>>>()?,
238            )
239        } else {
240            // with dictionary
241            (
242                Some(buffers[0].clone().try_to_host_sync()?),
243                buffers[1..]
244                    .iter()
245                    .map(|b| b.clone().try_to_host_sync())
246                    .collect::<VortexResult<Vec<_>>>()?,
247            )
248        };
249
250        let slots = smallvec![validity_to_child(&validity, len)];
251        let data = ZstdData::new(dictionary_buffer, compressed_buffers, metadata, len);
252        Ok(ArrayParts::new(self.clone(), dtype.clone(), len, data).with_slots(slots))
253    }
254
255    fn slot_name(_array: ArrayView<'_, Self>, idx: usize) -> String {
256        SLOT_NAMES[idx].to_string()
257    }
258
259    fn execute(array: Array<Self>, ctx: &mut ExecutionCtx) -> VortexResult<ExecutionResult> {
260        let unsliced_validity = child_to_validity(
261            array.as_ref().slots()[0].as_ref(),
262            array.dtype().nullability(),
263        );
264        array
265            .data()
266            .decompress(array.dtype(), &unsliced_validity, ctx)?
267            .execute::<ArrayRef>(ctx)
268            .map(ExecutionResult::done)
269    }
270
271    fn reduce_parent(
272        array: ArrayView<'_, Self>,
273        parent: &ArrayRef,
274        child_idx: usize,
275    ) -> VortexResult<Option<ArrayRef>> {
276        crate::rules::RULES.evaluate(array, parent, child_idx)
277    }
278}
279
280#[derive(Clone, Debug)]
281/// Zstd array encoding marker.
282pub struct Zstd;
283
284impl Zstd {
285    /// Construct a [`ZstdArray`] from validated compressed data and validity.
286    pub fn try_new(dtype: DType, data: ZstdData, validity: Validity) -> VortexResult<ZstdArray> {
287        let len = data.len();
288        data.validate(&dtype, len, &validity)?;
289        let slots = smallvec![validity_to_child(&validity, data.unsliced_n_rows())];
290        Ok(unsafe {
291            Array::from_parts_unchecked(ArrayParts::new(Zstd, dtype, len, data).with_slots(slots))
292        })
293    }
294
295    /// Compress a [`VarBinViewArray`] using Zstd without a dictionary.
296    pub fn from_var_bin_view_without_dict(
297        vbv: &VarBinViewArray,
298        level: i32,
299        values_per_frame: usize,
300        ctx: &mut ExecutionCtx,
301    ) -> VortexResult<ZstdArray> {
302        let validity = vbv.validity()?;
303        Self::try_new(
304            vbv.dtype().clone(),
305            ZstdData::from_var_bin_view_without_dict(vbv, level, values_per_frame, ctx)?,
306            validity,
307        )
308    }
309
310    /// Compress a [`PrimitiveArray`] using Zstd.
311    pub fn from_primitive(
312        parray: &PrimitiveArray,
313        level: i32,
314        values_per_frame: usize,
315        ctx: &mut ExecutionCtx,
316    ) -> VortexResult<ZstdArray> {
317        let validity = parray.validity()?;
318        Self::try_new(
319            parray.dtype().clone(),
320            ZstdData::from_primitive(parray, level, values_per_frame, ctx)?,
321            validity,
322        )
323    }
324
325    /// Compress a [`VarBinViewArray`] using Zstd.
326    pub fn from_var_bin_view(
327        vbv: &VarBinViewArray,
328        level: i32,
329        values_per_frame: usize,
330        ctx: &mut ExecutionCtx,
331    ) -> VortexResult<ZstdArray> {
332        let validity = vbv.validity()?;
333        Self::try_new(
334            vbv.dtype().clone(),
335            ZstdData::from_var_bin_view(vbv, level, values_per_frame, ctx)?,
336            validity,
337        )
338    }
339
340    /// Decompress a [`ZstdArray`] into its canonical Vortex representation.
341    pub fn decompress(array: &ZstdArray, ctx: &mut ExecutionCtx) -> VortexResult<ArrayRef> {
342        let unsliced_validity = child_to_validity(
343            array.as_ref().slots()[0].as_ref(),
344            array.dtype().nullability(),
345        );
346        array
347            .data()
348            .decompress(array.dtype(), &unsliced_validity, ctx)
349    }
350}
351
352/// The validity bitmap indicating which elements are non-null.
353pub(super) const NUM_SLOTS: usize = 1;
354pub(super) const SLOT_NAMES: [&str; NUM_SLOTS] = ["validity"];
355
356#[derive(Clone, Debug)]
357/// Encoding-specific data for a [`ZstdArray`].
358pub struct ZstdData {
359    pub(crate) dictionary: Option<ByteBuffer>,
360    pub(crate) frames: Vec<ByteBuffer>,
361    pub(crate) metadata: ZstdMetadata,
362    unsliced_n_rows: usize,
363    slice_start: usize,
364    slice_stop: usize,
365}
366
367impl Display for ZstdData {
368    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
369        write!(
370            f,
371            "nrows: {}, slice: {}..{}",
372            self.unsliced_n_rows, self.slice_start, self.slice_stop
373        )
374    }
375}
376
377/// Movable parts of a [`ZstdData`] value plus its validity.
378pub struct ZstdDataParts {
379    /// Optional zstd dictionary shared by all frames.
380    pub dictionary: Option<ByteBuffer>,
381    /// Compressed zstd frames.
382    pub frames: Vec<ByteBuffer>,
383    /// Serialized frame and dictionary metadata.
384    pub metadata: ZstdMetadata,
385    /// Unsliced validity for the array.
386    pub validity: Validity,
387    /// Unsliced row count.
388    pub n_rows: usize,
389    /// Start of this logical slice in unsliced row coordinates.
390    pub slice_start: usize,
391    /// End of this logical slice in unsliced row coordinates.
392    pub slice_stop: usize,
393}
394
395/// Compressed ZStd frames and their metadata
396#[derive(Debug)]
397struct Frames {
398    dictionary: Option<ByteBuffer>,
399    frames: Vec<ByteBuffer>,
400    frame_metas: Vec<ZstdFrameMetadata>,
401}
402
403fn choose_max_dict_size(uncompressed_size: usize) -> usize {
404    // following recommendations from
405    // https://github.com/facebook/zstd/blob/v1.5.5/lib/zdict.h#L190
406    // that is, 1/100 the data size, up to 100kB.
407    // It appears that zstd can't train dictionaries with <256 bytes.
408    (uncompressed_size / 100).clamp(256, 100 * 1024)
409}
410
411fn collect_valid_primitive(
412    parray: &PrimitiveArray,
413    ctx: &mut ExecutionCtx,
414) -> VortexResult<PrimitiveArray> {
415    let mask = parray
416        .as_ref()
417        .validity()?
418        .execute_mask(parray.as_ref().len(), ctx)?;
419    let result = parray.filter(mask)?.execute::<PrimitiveArray>(ctx)?;
420    Ok(result)
421}
422
423fn collect_valid_vbv(
424    vbv: &VarBinViewArray,
425    ctx: &mut ExecutionCtx,
426) -> VortexResult<(ByteBuffer, Vec<usize>)> {
427    let mask = vbv
428        .as_ref()
429        .validity()?
430        .execute_mask(vbv.as_ref().len(), ctx)?;
431    let buffer_and_value_byte_indices = match mask.bit_buffer() {
432        AllOr::None => (Buffer::empty(), Vec::new()),
433        _ => {
434            let mut buffer = BufferMut::with_capacity(
435                usize::try_from(vbv.nbytes()).vortex_expect("must fit into buffer")
436                    + mask.true_count() * size_of::<ViewLen>(),
437            );
438            let mut value_byte_indices = Vec::new();
439            let views = vbv.views();
440            let buffers = vbv
441                .data_buffers()
442                .iter()
443                .map(|b| b.as_host())
444                .collect::<Vec<_>>();
445            // skip nulls, writing only valid values
446            for (i, view) in views.iter().enumerate() {
447                if !mask.value(i) {
448                    continue;
449                }
450                let value = if view.is_inlined() {
451                    view.as_inlined().value()
452                } else {
453                    let view_ref = view.as_view();
454                    &buffers[view_ref.buffer_index as usize][view_ref.as_range()]
455                };
456                value_byte_indices.push(buffer.len());
457                // here's where we write the string lengths
458                buffer.extend_trusted(ViewLen::try_from(value.len())?.to_le_bytes().into_iter());
459                buffer.extend_from_slice(value);
460            }
461            (buffer.freeze(), value_byte_indices)
462        }
463    };
464    Ok(buffer_and_value_byte_indices)
465}
466
467/// Reconstruct BinaryView structs from length-prefixed byte data.
468///
469/// The buffer contains interleaved u32 lengths (little-endian) and string data.
470/// When the cumulative data exceeds `max_buffer_len`, the buffer is split (zero-copy) into
471/// multiple segments so that BinaryView's u32 offsets can address all data.
472///
473/// Pass [`MAX_BUFFER_LEN`] for `max_buffer_len` in production; a smaller value can be used in
474/// tests to exercise the splitting path without allocating >2 GiB.
475pub fn reconstruct_views(
476    buffer: &ByteBuffer,
477    max_buffer_len: usize,
478) -> (Vec<ByteBuffer>, Buffer<BinaryView>) {
479    let mut views = BufferMut::<BinaryView>::empty();
480    let mut buffers = Vec::new();
481    let mut segment_start: usize = 0;
482    let mut offset = 0;
483
484    while offset < buffer.len() {
485        let str_len = ViewLen::from_le_bytes(
486            buffer
487                .get(offset..offset + size_of::<ViewLen>())
488                .vortex_expect("corrupted zstd length")
489                .try_into()
490                .ok()
491                .vortex_expect("must fit ViewLen size"),
492        ) as usize;
493
494        let value_data_offset = offset + size_of::<ViewLen>();
495        let local_offset = value_data_offset - segment_start;
496
497        if local_offset + str_len > max_buffer_len && offset > segment_start {
498            buffers.push(buffer.slice(segment_start..offset));
499            segment_start = offset;
500        }
501
502        let local_offset = u32::try_from(value_data_offset - segment_start)
503            .vortex_expect("local offset within segment must fit in u32");
504        let buf_index = u32::try_from(buffers.len()).vortex_expect("buffer index must fit in u32");
505        let value = &buffer[value_data_offset..value_data_offset + str_len];
506        views.push(BinaryView::make_view(value, buf_index, local_offset));
507        offset = value_data_offset + str_len;
508    }
509
510    if segment_start < buffer.len() {
511        buffers.push(buffer.slice(segment_start..buffer.len()));
512    }
513
514    (buffers, views.freeze())
515}
516
517impl ZstdData {
518    /// Construct unsliced zstd data from raw frames and metadata.
519    pub fn new(
520        dictionary: Option<ByteBuffer>,
521        frames: Vec<ByteBuffer>,
522        metadata: ZstdMetadata,
523        n_rows: usize,
524    ) -> Self {
525        Self {
526            dictionary,
527            frames,
528            metadata,
529            unsliced_n_rows: n_rows,
530            slice_start: 0,
531            slice_stop: n_rows,
532        }
533    }
534
535    /// Validate dtype, slice, validity, frame, and dictionary invariants.
536    pub fn validate(&self, dtype: &DType, len: usize, validity: &Validity) -> VortexResult<()> {
537        vortex_ensure!(
538            matches!(
539                dtype,
540                DType::Primitive(..) | DType::Binary(_) | DType::Utf8(_)
541            ),
542            "Unsupported dtype for Zstd array: {dtype}"
543        );
544        vortex_ensure!(
545            self.slice_start <= self.slice_stop,
546            "Invalid slice range {}..{}",
547            self.slice_start,
548            self.slice_stop
549        );
550        vortex_ensure!(
551            self.slice_stop <= self.unsliced_n_rows,
552            "Slice stop {} exceeds unsliced row count {}",
553            self.slice_stop,
554            self.unsliced_n_rows
555        );
556        vortex_ensure!(
557            self.slice_stop - self.slice_start == len,
558            "Slice length {} does not match array length {}",
559            self.slice_stop - self.slice_start,
560            len
561        );
562        if let Some(validity_len) = validity.maybe_len() {
563            vortex_ensure!(
564                validity_len == self.unsliced_n_rows,
565                "Validity length {} does not match unsliced row count {}",
566                validity_len,
567                self.unsliced_n_rows
568            );
569        }
570
571        match &self.dictionary {
572            Some(dictionary) => vortex_ensure!(
573                usize::try_from(self.metadata.dictionary_size)? == dictionary.len(),
574                "Dictionary size metadata {} does not match buffer size {}",
575                self.metadata.dictionary_size,
576                dictionary.len()
577            ),
578            None => vortex_ensure!(
579                self.metadata.dictionary_size == 0,
580                "Dictionary metadata present without dictionary buffer"
581            ),
582        }
583        vortex_ensure!(
584            self.frames.len() == self.metadata.frames.len(),
585            "Frame count {} does not match metadata frame count {}",
586            self.frames.len(),
587            self.metadata.frames.len()
588        );
589
590        Ok(())
591    }
592
593    pub(crate) fn with_slice(&self, start: usize, stop: usize) -> Self {
594        let new_start = self.slice_start + start;
595        let new_stop = self.slice_start + stop;
596
597        assert!(
598            new_start <= self.slice_stop,
599            "new slice start {new_start} exceeds end {}",
600            self.slice_stop
601        );
602
603        assert!(
604            new_stop <= self.slice_stop,
605            "new slice stop {new_stop} exceeds end {}",
606            self.slice_stop
607        );
608
609        Self {
610            slice_start: new_start,
611            slice_stop: new_stop,
612            ..self.clone()
613        }
614    }
615
616    fn compress_values(
617        value_bytes: &ByteBuffer,
618        frame_byte_starts: &[usize],
619        level: i32,
620        values_per_frame: usize,
621        n_values: usize,
622        use_dictionary: bool,
623    ) -> VortexResult<Frames> {
624        let n_frames = frame_byte_starts.len();
625
626        // Would-be sample sizes if we end up applying zstd dictionary
627        let mut sample_sizes = Vec::with_capacity(n_frames);
628        for i in 0..n_frames {
629            let frame_byte_end = frame_byte_starts
630                .get(i + 1)
631                .copied()
632                .unwrap_or(value_bytes.len());
633            sample_sizes.push(frame_byte_end - frame_byte_starts[i]);
634        }
635        debug_assert_eq!(sample_sizes.iter().sum::<usize>(), value_bytes.len());
636
637        let (dictionary, mut compressor) = if !use_dictionary
638            || sample_sizes.len() < MIN_SAMPLES_FOR_DICTIONARY
639        {
640            // no dictionary
641            (None, zstd::bulk::Compressor::new(level)?)
642        } else {
643            // with dictionary
644            let max_dict_size = choose_max_dict_size(value_bytes.len());
645            let dict = zstd::dict::from_continuous(value_bytes, &sample_sizes, max_dict_size)
646                .map_err(|err| VortexError::from(err).with_context("while training dictionary"))?;
647
648            let compressor = zstd::bulk::Compressor::with_dictionary(level, &dict)?;
649            (Some(ByteBuffer::from(dict)), compressor)
650        };
651
652        let mut frame_metas = vec![];
653        let mut frames = vec![];
654        for i in 0..n_frames {
655            let frame_byte_end = frame_byte_starts
656                .get(i + 1)
657                .copied()
658                .unwrap_or(value_bytes.len());
659
660            let uncompressed = &value_bytes.slice(frame_byte_starts[i]..frame_byte_end);
661            let compressed = compressor
662                .compress(uncompressed)
663                .map_err(|err| VortexError::from(err).with_context("while compressing"))?;
664            frame_metas.push(ZstdFrameMetadata {
665                uncompressed_size: uncompressed.len() as u64,
666                n_values: values_per_frame.min(n_values - i * values_per_frame) as u64,
667            });
668            frames.push(ByteBuffer::from(compressed));
669        }
670
671        Ok(Frames {
672            dictionary,
673            frames,
674            frame_metas,
675        })
676    }
677
678    /// Creates a ZstdArray from a primitive array.
679    ///
680    /// # Arguments
681    /// * `parray` - The primitive array to compress
682    /// * `level` - Zstd compression level (0 = default, negative = fast, positive = better compression)
683    /// * `values_per_frame` - Number of values per frame (0 = single frame)
684    pub fn from_primitive(
685        parray: &PrimitiveArray,
686        level: i32,
687        values_per_frame: usize,
688        ctx: &mut ExecutionCtx,
689    ) -> VortexResult<Self> {
690        Self::from_primitive_impl(parray, level, values_per_frame, true, ctx)
691    }
692
693    /// Creates a ZstdArray from a primitive array without using a dictionary.
694    ///
695    /// This is useful when the compressed data will be decompressed by systems
696    /// that don't support ZSTD dictionaries (e.g., nvCOMP on GPU).
697    ///
698    /// Note: Without a dictionary, each frame is compressed independently.
699    /// Dictionaries are trained from sample data from previously seen frames,
700    /// to improve compression ratio.
701    ///
702    /// # Arguments
703    /// * `parray` - The primitive array to compress
704    /// * `level` - Zstd compression level (0 = default, negative = fast, positive = better compression)
705    /// * `values_per_frame` - Number of values per frame (0 = single frame)
706    pub fn from_primitive_without_dict(
707        parray: &PrimitiveArray,
708        level: i32,
709        values_per_frame: usize,
710        ctx: &mut ExecutionCtx,
711    ) -> VortexResult<Self> {
712        Self::from_primitive_impl(parray, level, values_per_frame, false, ctx)
713    }
714
715    fn from_primitive_impl(
716        parray: &PrimitiveArray,
717        level: i32,
718        values_per_frame: usize,
719        use_dictionary: bool,
720        ctx: &mut ExecutionCtx,
721    ) -> VortexResult<Self> {
722        let byte_width = parray.ptype().byte_width();
723
724        // We compress only the valid elements.
725        let values = collect_valid_primitive(parray, ctx)?;
726        let n_values = values.len();
727        let values_per_frame = if values_per_frame > 0 {
728            values_per_frame
729        } else {
730            n_values
731        };
732
733        let value_bytes = values.buffer_handle().try_to_host_sync()?;
734        // Align frames to buffer alignment. This is necessary for overaligned buffers.
735        let alignment = *value_bytes.alignment();
736        let step_width = (values_per_frame * byte_width).div_ceil(alignment) * alignment;
737
738        let frame_byte_starts = (0..n_values * byte_width)
739            .step_by(step_width)
740            .collect::<Vec<_>>();
741        let Frames {
742            dictionary,
743            frames,
744            frame_metas,
745        } = Self::compress_values(
746            &value_bytes,
747            &frame_byte_starts,
748            level,
749            values_per_frame,
750            n_values,
751            use_dictionary,
752        )?;
753
754        let metadata = ZstdMetadata {
755            dictionary_size: dictionary
756                .as_ref()
757                .map_or(0, |dict| dict.len())
758                .try_into()?,
759            frames: frame_metas,
760        };
761
762        Ok(ZstdData::new(dictionary, frames, metadata, parray.len()))
763    }
764
765    /// Creates a ZstdArray from a VarBinView array.
766    ///
767    /// # Arguments
768    /// * `vbv` - The VarBinView array to compress
769    /// * `level` - Zstd compression level (0 = default, negative = fast, positive = better compression)
770    /// * `values_per_frame` - Number of values per frame (0 = single frame)
771    pub fn from_var_bin_view(
772        vbv: &VarBinViewArray,
773        level: i32,
774        values_per_frame: usize,
775        ctx: &mut ExecutionCtx,
776    ) -> VortexResult<Self> {
777        Self::from_var_bin_view_impl(vbv, level, values_per_frame, true, ctx)
778    }
779
780    /// Creates a ZstdArray from a VarBinView array without using a dictionary.
781    ///
782    /// This is useful when the compressed data will be decompressed by systems
783    /// that don't support ZSTD dictionaries (e.g., nvCOMP on GPU).
784    ///
785    /// Note: Without a dictionary, each frame is compressed independently.
786    /// Dictionaries are trained from sample data from previously seen frames,
787    /// to improve compression ratio.
788    ///
789    /// # Arguments
790    /// * `vbv` - The VarBinView array to compress
791    /// * `level` - Zstd compression level (0 = default, negative = fast, positive = better compression)
792    /// * `values_per_frame` - Number of values per frame (0 = single frame)
793    pub fn from_var_bin_view_without_dict(
794        vbv: &VarBinViewArray,
795        level: i32,
796        values_per_frame: usize,
797        ctx: &mut ExecutionCtx,
798    ) -> VortexResult<Self> {
799        Self::from_var_bin_view_impl(vbv, level, values_per_frame, false, ctx)
800    }
801
802    fn from_var_bin_view_impl(
803        vbv: &VarBinViewArray,
804        level: i32,
805        values_per_frame: usize,
806        use_dictionary: bool,
807        ctx: &mut ExecutionCtx,
808    ) -> VortexResult<Self> {
809        // Approach for strings: we prefix each string with its length as a u32.
810        // This is the same as what Parquet does. In some cases it may be better
811        // to separate the binary data and lengths as two separate streams, but
812        // this approach is simpler and can be best in cases when there is
813        // mutual information between strings and their lengths.
814        // We compress only the valid elements.
815        let (value_bytes, value_byte_indices) = collect_valid_vbv(vbv, ctx)?;
816        let n_values = value_byte_indices.len();
817        let values_per_frame = if values_per_frame > 0 {
818            values_per_frame
819        } else {
820            n_values
821        };
822
823        let frame_byte_starts = (0..n_values)
824            .step_by(values_per_frame)
825            .map(|i| value_byte_indices[i])
826            .collect::<Vec<_>>();
827        let Frames {
828            dictionary,
829            frames,
830            frame_metas,
831        } = Self::compress_values(
832            &value_bytes,
833            &frame_byte_starts,
834            level,
835            values_per_frame,
836            n_values,
837            use_dictionary,
838        )?;
839
840        let metadata = ZstdMetadata {
841            dictionary_size: dictionary
842                .as_ref()
843                .map_or(0, |dict| dict.len())
844                .try_into()?,
845            frames: frame_metas,
846        };
847        Ok(ZstdData::new(dictionary, frames, metadata, vbv.len()))
848    }
849
850    /// Compress a supported canonical array into zstd data.
851    ///
852    /// Returns `Ok(None)` for canonical variants that this encoding does not support.
853    pub fn from_canonical(
854        canonical: &Canonical,
855        level: i32,
856        values_per_frame: usize,
857        ctx: &mut ExecutionCtx,
858    ) -> VortexResult<Option<Self>> {
859        match canonical {
860            Canonical::Primitive(parray) => Ok(Some(ZstdData::from_primitive(
861                parray,
862                level,
863                values_per_frame,
864                ctx,
865            )?)),
866            Canonical::VarBinView(vbv) => Ok(Some(ZstdData::from_var_bin_view(
867                vbv,
868                level,
869                values_per_frame,
870                ctx,
871            )?)),
872            _ => Ok(None),
873        }
874    }
875
876    /// Canonicalize and compress an array into zstd data.
877    ///
878    /// # Errors
879    ///
880    /// Returns an error if the array's canonical form is unsupported or compression fails.
881    pub fn from_array(
882        array: ArrayRef,
883        level: i32,
884        values_per_frame: usize,
885        ctx: &mut ExecutionCtx,
886    ) -> VortexResult<Self> {
887        let canonical = array.execute::<Canonical>(ctx)?;
888        Self::from_canonical(&canonical, level, values_per_frame, ctx)?
889            .ok_or_else(|| vortex_err!("Zstd can only encode Primitive and VarBinView arrays"))
890    }
891
892    fn byte_width(dtype: &DType) -> usize {
893        if dtype.is_primitive() {
894            dtype.as_ptype().byte_width()
895        } else {
896            1
897        }
898    }
899
900    fn decompress(
901        &self,
902        dtype: &DType,
903        unsliced_validity: &Validity,
904        ctx: &mut ExecutionCtx,
905    ) -> VortexResult<ArrayRef> {
906        // To start, we figure out which frames we need to decompress, and with
907        // what row offset into the first such frame.
908        let byte_width = Self::byte_width(dtype);
909        let slice_n_rows = self.slice_stop - self.slice_start;
910        let slice_value_indices = unsliced_validity
911            .execute_mask(self.unsliced_n_rows, ctx)?
912            .valid_counts_for_indices(&[self.slice_start, self.slice_stop]);
913
914        let slice_value_idx_start = slice_value_indices[0];
915        let slice_value_idx_stop = slice_value_indices[1];
916
917        let mut frames_to_decompress = vec![];
918        let mut value_idx_start = 0;
919        let mut uncompressed_size_to_decompress = 0;
920        let mut n_skipped_values = 0;
921        for (frame, frame_meta) in self.frames.iter().zip(&self.metadata.frames) {
922            if value_idx_start >= slice_value_idx_stop {
923                break;
924            }
925
926            let frame_uncompressed_size = usize::try_from(frame_meta.uncompressed_size)
927                .vortex_expect("Uncompressed size must fit in usize");
928            let frame_n_values = if frame_meta.n_values == 0 {
929                // possibly older primitive-only metadata that just didn't store this
930                frame_uncompressed_size / byte_width
931            } else {
932                usize::try_from(frame_meta.n_values).vortex_expect("frame size must fit usize")
933            };
934
935            let value_idx_stop = value_idx_start + frame_n_values;
936            if value_idx_stop > slice_value_idx_start {
937                // we need this frame
938                frames_to_decompress.push(frame);
939                uncompressed_size_to_decompress += frame_uncompressed_size;
940            } else {
941                n_skipped_values += frame_n_values;
942            }
943            value_idx_start = value_idx_stop;
944        }
945
946        // then we actually decompress those frames
947        let mut decompressor = if let Some(dictionary) = &self.dictionary {
948            zstd::bulk::Decompressor::with_dictionary(dictionary)?
949        } else {
950            zstd::bulk::Decompressor::new()?
951        };
952        let mut decompressed = ByteBufferMut::with_capacity_aligned(
953            uncompressed_size_to_decompress,
954            Alignment::new(byte_width),
955        );
956        unsafe {
957            // safety: we immediately fill all bytes in the following loop,
958            // assuming our metadata's uncompressed size is correct
959            decompressed.set_len(uncompressed_size_to_decompress);
960        }
961        let mut uncompressed_start = 0;
962        for frame in frames_to_decompress {
963            let uncompressed_written = decompressor
964                .decompress_to_buffer(frame.as_slice(), &mut decompressed[uncompressed_start..])?;
965            uncompressed_start += uncompressed_written;
966        }
967        if uncompressed_start != uncompressed_size_to_decompress {
968            vortex_panic!(
969                "Zstd metadata or frames were corrupt; expected {} bytes but decompressed {}",
970                uncompressed_size_to_decompress,
971                uncompressed_start
972            );
973        }
974
975        let decompressed = decompressed.freeze();
976        // Last, we slice the exact values requested out of the decompressed data.
977        let mut slice_validity = unsliced_validity.slice(self.slice_start..self.slice_stop)?;
978
979        // NOTE: this block handles setting the output type when the validity and DType disagree.
980        //
981        // ZSTD is a compact block compressor, meaning that null values are not stored inline in
982        // the data frames. A ZSTD Array that was initialized must always hold onto its full
983        // validity bitmap, even if sliced to only include non-null values.
984        //
985        // We ensure that the validity of the decompressed array ALWAYS matches the validity
986        // implied by the DType.
987        if !dtype.is_nullable() && !matches!(slice_validity, Validity::NonNullable) {
988            assert!(
989                matches!(slice_validity, Validity::AllValid),
990                "ZSTD array expects to be non-nullable but there are nulls after decompression"
991            );
992
993            slice_validity = Validity::NonNullable;
994        } else if dtype.is_nullable() && matches!(slice_validity, Validity::NonNullable) {
995            slice_validity = Validity::AllValid;
996        }
997        // END OF IMPORTANT BLOCK
998        //
999
1000        match dtype {
1001            DType::Primitive(..) => {
1002                let slice_values_buffer = decompressed.slice(
1003                    (slice_value_idx_start - n_skipped_values) * byte_width
1004                        ..(slice_value_idx_stop - n_skipped_values) * byte_width,
1005                );
1006                let primitive = PrimitiveArray::from_values_byte_buffer(
1007                    slice_values_buffer,
1008                    dtype.as_ptype(),
1009                    slice_validity,
1010                    slice_n_rows,
1011                    ctx,
1012                );
1013
1014                Ok(primitive.into_array())
1015            }
1016            DType::Binary(_) | DType::Utf8(_) => {
1017                match slice_validity.execute_mask(slice_n_rows, ctx)?.indices() {
1018                    AllOr::All => {
1019                        let (buffers, all_views) = reconstruct_views(&decompressed, MAX_BUFFER_LEN);
1020                        let valid_views = all_views.slice(
1021                            slice_value_idx_start - n_skipped_values
1022                                ..slice_value_idx_stop - n_skipped_values,
1023                        );
1024
1025                        // SAFETY: we properly construct the views inside `reconstruct_views`
1026                        Ok(unsafe {
1027                            VarBinViewArray::new_unchecked(
1028                                valid_views,
1029                                Arc::from(buffers),
1030                                dtype.clone(),
1031                                slice_validity,
1032                            )
1033                        }
1034                        .into_array())
1035                    }
1036                    AllOr::None => Ok(ConstantArray::new(
1037                        Scalar::null(dtype.clone()),
1038                        slice_n_rows,
1039                    )
1040                    .into_array()),
1041                    AllOr::Some(valid_indices) => {
1042                        let (buffers, all_views) = reconstruct_views(&decompressed, MAX_BUFFER_LEN);
1043                        let valid_views = all_views.slice(
1044                            slice_value_idx_start - n_skipped_values
1045                                ..slice_value_idx_stop - n_skipped_values,
1046                        );
1047
1048                        let mut views = BufferMut::<BinaryView>::zeroed(slice_n_rows);
1049                        for (view, index) in valid_views.into_iter().zip_eq(valid_indices) {
1050                            views[*index] = view
1051                        }
1052
1053                        // SAFETY: we properly construct the views inside `reconstruct_views`
1054                        Ok(unsafe {
1055                            VarBinViewArray::new_unchecked(
1056                                views.freeze(),
1057                                Arc::from(buffers),
1058                                dtype.clone(),
1059                                slice_validity,
1060                            )
1061                        }
1062                        .into_array())
1063                    }
1064                }
1065            }
1066            _ => vortex_panic!("Unsupported dtype for Zstd array: {}", dtype),
1067        }
1068    }
1069
1070    /// Returns the length of the array.
1071    #[inline]
1072    pub fn len(&self) -> usize {
1073        self.slice_stop - self.slice_start
1074    }
1075
1076    /// Returns whether the array is empty.
1077    #[inline]
1078    pub fn is_empty(&self) -> bool {
1079        self.slice_stop == self.slice_start
1080    }
1081
1082    /// Split this data into movable parts, attaching the supplied validity.
1083    pub fn into_parts(self, validity: Validity) -> ZstdDataParts {
1084        ZstdDataParts {
1085            dictionary: self.dictionary,
1086            frames: self.frames,
1087            metadata: self.metadata,
1088            validity,
1089            n_rows: self.unsliced_n_rows,
1090            slice_start: self.slice_start,
1091            slice_stop: self.slice_stop,
1092        }
1093    }
1094
1095    pub(crate) fn slice_start(&self) -> usize {
1096        self.slice_start
1097    }
1098
1099    pub(crate) fn slice_stop(&self) -> usize {
1100        self.slice_stop
1101    }
1102
1103    pub(crate) fn unsliced_n_rows(&self) -> usize {
1104        self.unsliced_n_rows
1105    }
1106}
1107
1108impl ValidityVTable<Zstd> for Zstd {
1109    fn validity(array: ArrayView<'_, Zstd>) -> VortexResult<Validity> {
1110        let unsliced_validity =
1111            child_to_validity(array.slots()[0].as_ref(), array.dtype().nullability());
1112        unsliced_validity.slice(array.slice_start()..array.slice_stop())
1113    }
1114}
1115
1116impl OperationsVTable<Zstd> for Zstd {
1117    fn scalar_at(
1118        array: ArrayView<'_, Zstd>,
1119        index: usize,
1120        ctx: &mut ExecutionCtx,
1121    ) -> VortexResult<Scalar> {
1122        let unsliced_validity =
1123            child_to_validity(array.slots()[0].as_ref(), array.dtype().nullability());
1124        let sliced = array.data().with_slice(index, index + 1);
1125        sliced
1126            .decompress(array.dtype(), &unsliced_validity, ctx)?
1127            .execute_scalar(0, ctx)
1128    }
1129}
1130
1131#[cfg(test)]
1132#[expect(clippy::cast_possible_truncation)]
1133mod tests {
1134    use vortex_buffer::ByteBuffer;
1135
1136    use super::reconstruct_views;
1137    use crate::array::BinaryView;
1138
1139    /// Build a Zstd-style interleaved buffer: [u32-LE length][string bytes] repeated.
1140    fn make_interleaved(strings: &[&[u8]]) -> ByteBuffer {
1141        let mut buf = Vec::new();
1142        for s in strings {
1143            let len = s.len() as u32;
1144            buf.extend_from_slice(&len.to_le_bytes());
1145            buf.extend_from_slice(s);
1146        }
1147        ByteBuffer::copy_from(buf.as_slice())
1148    }
1149
1150    #[test]
1151    fn test_reconstruct_views_no_split() {
1152        let strings: &[&[u8]] = &[b"hello", b"world"];
1153        let buf = make_interleaved(strings);
1154        let (buffers, views) = reconstruct_views(&buf, 1024);
1155
1156        assert_eq!(buffers.len(), 1);
1157        assert_eq!(views.len(), 2);
1158        // Each entry: [u32 len (4 bytes)][data], so offsets are 4 and 4+5+4=13
1159        assert_eq!(views[0], BinaryView::make_view(b"hello", 0, 4));
1160        assert_eq!(views[1], BinaryView::make_view(b"world", 0, 13));
1161    }
1162
1163    #[test]
1164    fn test_reconstruct_views_split_across_segments() {
1165        // "aaaaaaaaaaaaa" (13 bytes) and "bbbbbbbbbbbbb" (13 bytes).
1166        // Each entry occupies 4 (length prefix) + 13 (data) = 17 bytes.
1167        // With max_buffer_len=20, the second entry's data (offset 4+13+4=21) exceeds the limit,
1168        // so it rolls into a second segment.
1169        let strings: &[&[u8]] = &[b"aaaaaaaaaaaaa", b"bbbbbbbbbbbbb"];
1170        let buf = make_interleaved(strings);
1171        let (buffers, views) = reconstruct_views(&buf, 20);
1172
1173        assert_eq!(buffers.len(), 2);
1174        assert_eq!(views.len(), 2);
1175        assert_eq!(views[0], BinaryView::make_view(b"aaaaaaaaaaaaa", 0, 4));
1176        // Second entry starts a new segment at byte 17 (the length prefix), so local offset = 4.
1177        assert_eq!(views[1], BinaryView::make_view(b"bbbbbbbbbbbbb", 1, 4));
1178    }
1179}