1use 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 prost::Message as _;
12use vortex_array::Array;
13use vortex_array::ArrayEq;
14use vortex_array::ArrayHash;
15use vortex_array::ArrayId;
16use vortex_array::ArrayParts;
17use vortex_array::ArrayRef;
18use vortex_array::ArraySlots;
19use vortex_array::ArrayView;
20use vortex_array::EqMode;
21use vortex_array::ExecutionCtx;
22use vortex_array::ExecutionResult;
23use vortex_array::buffer::BufferHandle;
24use vortex_array::dtype::DType;
25use vortex_array::scalar::Scalar;
26use vortex_array::serde::ArrayChildren;
27use vortex_array::session::ArraySessionExt;
28use vortex_array::validity::Validity;
29use vortex_array::vtable::OperationsVTable;
30use vortex_array::vtable::VTable;
31use vortex_array::vtable::ValidityVTable;
32use vortex_buffer::Alignment;
33use vortex_buffer::ByteBuffer;
34use vortex_buffer::ByteBufferMut;
35use vortex_error::VortexResult;
36use vortex_error::vortex_ensure_eq;
37use vortex_error::vortex_err;
38use vortex_session::VortexSession;
39use vortex_session::registry::CachedId;
40
41use crate::ZstdBuffersMetadata;
42
43pub type ZstdBuffersArray = Array<ZstdBuffers>;
45
46#[derive(Clone, Debug)]
47pub struct ZstdBuffers;
49
50impl ZstdBuffers {
51 pub fn try_new(
53 dtype: DType,
54 len: usize,
55 data: ZstdBuffersData,
56 ) -> VortexResult<ZstdBuffersArray> {
57 Array::try_from_parts(ArrayParts::new(ZstdBuffers, dtype, len, data))
58 }
59
60 pub fn compress(
65 array: &ArrayRef,
66 level: i32,
67 session: &VortexSession,
68 ) -> VortexResult<ZstdBuffersArray> {
69 let encoding_id = array.encoding_id();
70 let metadata = session
71 .array_serialize(array)?
72 .ok_or_else(|| vortex_err!("[ZstdBuffers]: Array does not support serialization"))?;
73 let buffer_handles = array.buffer_handles();
74 let children = array.children();
75
76 let mut compressed_buffers = Vec::with_capacity(buffer_handles.len());
77 let mut uncompressed_sizes = Vec::with_capacity(buffer_handles.len());
78 let mut buffer_alignments = Vec::with_capacity(buffer_handles.len());
79
80 let mut compressor = zstd::bulk::Compressor::new(level)?;
81 for handle in &buffer_handles {
83 buffer_alignments.push(u32::from(handle.alignment()));
84 let host_buf = handle.clone().try_to_host_sync()?;
85 uncompressed_sizes.push(host_buf.len() as u64);
86 let compressed = compressor.compress(&host_buf)?;
87 compressed_buffers.push(BufferHandle::new_host(ByteBuffer::from(compressed)));
88 }
89
90 let data = ZstdBuffersData {
91 inner_encoding_id: encoding_id,
92 inner_metadata: metadata,
93 compressed_buffers,
94 uncompressed_sizes,
95 buffer_alignments,
96 };
97 let slots: ArraySlots = children.into_iter().map(Some).collect();
98 let compressed = Array::try_from_parts(
99 ArrayParts::new(ZstdBuffers, array.dtype().clone(), array.len(), data)
100 .with_slots(slots),
101 )?;
102 compressed.statistics().inherit_from(array.statistics());
103 Ok(compressed)
104 }
105
106 pub fn build_inner(
108 array: &ZstdBuffersArray,
109 buffer_handles: &[BufferHandle],
110 session: &VortexSession,
111 ) -> VortexResult<ArrayRef> {
112 let registry = session.arrays().registry().clone();
113 let inner_vtable = registry
114 .find(&array.data().inner_encoding_id)
115 .ok_or_else(|| {
116 vortex_err!("Unknown inner encoding: {}", array.data().inner_encoding_id)
117 })?;
118
119 let children: Vec<ArrayRef> = array.slots().iter().flatten().cloned().collect();
120 inner_vtable.deserialize(
121 array.dtype(),
122 array.len(),
123 &array.data().inner_metadata,
124 buffer_handles,
125 &children.as_slice(),
126 session,
127 )
128 }
129
130 fn decompress_and_build_inner(
131 array: &ZstdBuffersArray,
132 session: &VortexSession,
133 ) -> VortexResult<ArrayRef> {
134 let decompressed_buffers = array.data().decompress_buffers()?;
135 Self::build_inner(array, &decompressed_buffers, session)
136 }
137}
138
139#[derive(Clone, Debug)]
145pub struct ZstdBuffersData {
146 inner_encoding_id: ArrayId,
147 inner_metadata: Vec<u8>,
148 compressed_buffers: Vec<BufferHandle>,
149 uncompressed_sizes: Vec<u64>,
150 buffer_alignments: Vec<u32>,
151}
152
153impl Display for ZstdBuffersData {
154 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
155 write!(f, "inner_encoding: {}", self.inner_encoding_id)
156 }
157}
158
159#[derive(Clone, Debug)]
160pub struct ZstdBuffersDecodePlan {
162 compressed_buffers: Vec<BufferHandle>,
163 frame_sizes: Arc<[usize]>,
164 output_sizes: Arc<[usize]>,
165 output_offsets: Vec<usize>,
166 output_alignments: Vec<Alignment>,
167 output_size_total: usize,
168 output_size_max: usize,
169}
170
171impl ZstdBuffersDecodePlan {
172 pub fn compressed_buffers(&self) -> &[BufferHandle] {
174 &self.compressed_buffers
175 }
176
177 pub fn frame_sizes(&self) -> Arc<[usize]> {
179 Arc::clone(&self.frame_sizes)
180 }
181
182 pub fn output_sizes(&self) -> Arc<[usize]> {
184 Arc::clone(&self.output_sizes)
185 }
186
187 pub fn output_offsets(&self) -> &[usize] {
189 &self.output_offsets
190 }
191
192 pub fn output_size_total(&self) -> usize {
194 self.output_size_total
195 }
196
197 pub fn output_size_max(&self) -> usize {
199 self.output_size_max
200 }
201
202 pub fn num_frames(&self) -> usize {
204 self.compressed_buffers.len()
205 }
206
207 pub fn split_output_handle(
210 &self,
211 output_handle: &BufferHandle,
212 ) -> VortexResult<Vec<BufferHandle>> {
213 self.output_offsets
214 .iter()
215 .zip(self.output_sizes.iter())
216 .zip(self.output_alignments.iter())
217 .map(|((&offset, &size), &alignment)| {
218 output_handle
219 .slice(offset..offset + size)
220 .ensure_aligned(alignment)
221 })
222 .collect::<VortexResult<Vec<_>>>()
223 }
224}
225
226impl ZstdBuffersData {
227 fn validate(&self) -> VortexResult<()> {
228 vortex_ensure_eq!(
229 self.compressed_buffers.len(),
230 self.uncompressed_sizes.len(),
231 "zstd_buffers metadata mismatch: {} compressed buffers vs {} sizes",
232 self.compressed_buffers.len(),
233 self.uncompressed_sizes.len()
234 );
235 vortex_ensure_eq!(
236 self.compressed_buffers.len(),
237 self.buffer_alignments.len(),
238 "zstd_buffers metadata mismatch: {} compressed buffers vs {} alignments",
239 self.compressed_buffers.len(),
240 self.buffer_alignments.len()
241 );
242 Ok(())
243 }
244
245 fn decompress_buffers(&self) -> VortexResult<Vec<BufferHandle>> {
246 let mut decompressor = zstd::bulk::Decompressor::new()?;
249 let mut result = Vec::with_capacity(self.compressed_buffers.len());
250 for (i, (buf, &uncompressed_size)) in self
251 .compressed_buffers
252 .iter()
253 .zip(&self.uncompressed_sizes)
254 .enumerate()
255 {
256 let size = usize::try_from(uncompressed_size)?;
257 let alignment = self.buffer_alignments.get(i).copied().unwrap_or(1);
258
259 let aligned = Alignment::try_from(alignment)?;
260 let mut output = ByteBufferMut::with_capacity_aligned(size, aligned);
261 let spare = output.spare_capacity_mut();
262
263 if spare.len() < size {
266 return Err(vortex_err!(
267 "Insufficient output capacity: expected at least {}, got {}",
268 size,
269 spare.len()
270 ));
271 }
272 let dst =
275 unsafe { std::slice::from_raw_parts_mut(spare.as_mut_ptr().cast::<u8>(), size) };
276 let compressed = buf.clone().try_to_host_sync()?;
277 let written = decompressor.decompress_to_buffer(compressed.as_slice(), dst)?;
278 if written != size {
279 return Err(vortex_err!(
280 "Decompressed size mismatch: expected {}, got {}",
281 size,
282 written
283 ));
284 }
285 unsafe { output.set_len(size) };
287 result.push(BufferHandle::new_host(output.freeze()));
288 }
289 Ok(result)
290 }
291
292 pub fn decode_plan(&self) -> VortexResult<ZstdBuffersDecodePlan> {
294 self.validate()?;
297
298 let output_sizes = self
299 .uncompressed_sizes
300 .iter()
301 .map(|&size| usize::try_from(size))
302 .collect::<Result<Vec<_>, _>>()?;
303 let output_size_max = output_sizes.iter().copied().max().unwrap_or(0);
304
305 let output_alignments = self
306 .buffer_alignments
307 .iter()
308 .map(|&alignment| Alignment::try_from(alignment))
309 .collect::<VortexResult<Vec<_>>>()?;
310
311 let (output_offsets, output_size_total) =
312 compute_output_layout(&output_sizes, &output_alignments);
313
314 let compressed_buffers = self.compressed_buffers.clone();
315 let frame_sizes: Arc<[usize]> = compressed_buffers
316 .iter()
317 .map(BufferHandle::len)
318 .collect::<Vec<_>>()
319 .into();
320 let output_sizes: Arc<[usize]> = output_sizes.into();
321
322 Ok(ZstdBuffersDecodePlan {
323 compressed_buffers,
324 frame_sizes,
325 output_sizes,
326 output_offsets,
327 output_alignments,
328 output_size_total,
329 output_size_max,
330 })
331 }
332}
333
334fn compute_output_layout(
335 output_sizes: &[usize],
336 output_alignments: &[Alignment],
337) -> (Vec<usize>, usize) {
338 let mut offsets = Vec::with_capacity(output_sizes.len());
341 let mut total_size = 0usize;
342
343 for (&size, &alignment) in output_sizes.iter().zip(output_alignments.iter()) {
344 total_size = total_size.next_multiple_of(*alignment);
345 offsets.push(total_size);
346 total_size += size;
347 }
348
349 (offsets, total_size)
350}
351
352#[expect(clippy::disallowed_methods, reason = "interning a dynamic id")]
353fn array_id_from_string(s: &str) -> ArrayId {
354 ArrayId::new(s)
355}
356
357impl ArrayHash for ZstdBuffersData {
358 fn array_hash<H: Hasher>(&self, state: &mut H, accuracy: EqMode) {
359 self.inner_encoding_id.hash(state);
360 self.inner_metadata.hash(state);
361 for buf in &self.compressed_buffers {
362 buf.array_hash(state, accuracy);
363 }
364 self.uncompressed_sizes.hash(state);
365 self.buffer_alignments.hash(state);
366 }
367}
368
369impl ArrayEq for ZstdBuffersData {
370 fn array_eq(&self, other: &Self, accuracy: EqMode) -> bool {
371 self.inner_encoding_id == other.inner_encoding_id
372 && self.inner_metadata == other.inner_metadata
373 && self.compressed_buffers.len() == other.compressed_buffers.len()
374 && self
375 .compressed_buffers
376 .iter()
377 .zip(&other.compressed_buffers)
378 .all(|(a, b)| a.array_eq(b, accuracy))
379 && self.uncompressed_sizes == other.uncompressed_sizes
380 && self.buffer_alignments == other.buffer_alignments
381 }
382}
383
384impl VTable for ZstdBuffers {
385 type TypedArrayData = ZstdBuffersData;
386 type OperationsVTable = Self;
387 type ValidityVTable = Self;
388
389 fn id(&self) -> ArrayId {
390 static ID: CachedId = CachedId::new("vortex.zstd_buffers");
391 *ID
392 }
393
394 fn validate(
395 &self,
396 data: &Self::TypedArrayData,
397 _dtype: &DType,
398 _len: usize,
399 _slots: &[Option<ArrayRef>],
400 ) -> VortexResult<()> {
401 data.validate()
402 }
403
404 fn nbuffers(array: ArrayView<'_, Self>) -> usize {
405 array.compressed_buffers.len()
406 }
407
408 fn buffer(array: ArrayView<'_, Self>, idx: usize) -> BufferHandle {
409 array.compressed_buffers[idx].clone()
410 }
411
412 fn buffer_name(_array: ArrayView<'_, Self>, idx: usize) -> Option<String> {
413 Some(format!("compressed_{idx}"))
414 }
415
416 fn with_buffers(
417 &self,
418 array: ArrayView<'_, Self>,
419 buffers: &[BufferHandle],
420 ) -> VortexResult<ArrayParts<Self>> {
421 let mut data = array.data().clone();
422 data.compressed_buffers = buffers.to_vec();
423 Ok(
424 ArrayParts::new(self.clone(), array.dtype().clone(), array.len(), data)
425 .with_slots(array.slots().iter().cloned().collect()),
426 )
427 }
428
429 fn slot_name(_array: ArrayView<'_, Self>, idx: usize) -> String {
430 format!("child_{idx}")
431 }
432
433 fn serialize(
434 array: ArrayView<'_, Self>,
435 _session: &VortexSession,
436 ) -> VortexResult<Option<Vec<u8>>> {
437 let children: Vec<&ArrayRef> = array.slots().iter().flatten().collect();
438 let child_dtypes = children
439 .iter()
440 .map(|child| child.dtype().try_into())
441 .collect::<VortexResult<Vec<_>>>()?;
442 let child_lens = children.iter().map(|child| child.len() as u64).collect();
443
444 Ok(Some(
445 ZstdBuffersMetadata {
446 inner_encoding_id: array.inner_encoding_id.to_string(),
447 inner_metadata: array.inner_metadata.clone(),
448 uncompressed_sizes: array.uncompressed_sizes.clone(),
449 buffer_alignments: array.buffer_alignments.clone(),
450 child_dtypes,
451 child_lens,
452 }
453 .encode_to_vec(),
454 ))
455 }
456
457 fn deserialize(
458 &self,
459 dtype: &DType,
460 len: usize,
461 metadata: &[u8],
462 buffers: &[BufferHandle],
463 children: &dyn ArrayChildren,
464 session: &VortexSession,
465 ) -> VortexResult<ArrayParts<Self>> {
466 let metadata = ZstdBuffersMetadata::decode(metadata)?;
467 let compressed_buffers: Vec<BufferHandle> = buffers.to_vec();
468
469 vortex_ensure_eq!(metadata.child_dtypes.len(), children.len());
473 vortex_ensure_eq!(metadata.child_lens.len(), children.len());
474
475 let slots: ArraySlots = (0..children.len())
476 .map(|i| {
477 let child_dtype = DType::from_proto(&metadata.child_dtypes[i], session)?;
478 let child_len = usize::try_from(metadata.child_lens[i])?;
479 children.get(i, &child_dtype, child_len).map(Some)
480 })
481 .collect::<VortexResult<Vec<_>>>()?
482 .into();
483
484 let data = ZstdBuffersData {
485 inner_encoding_id: array_id_from_string(&metadata.inner_encoding_id),
486 inner_metadata: metadata.inner_metadata.clone(),
487 compressed_buffers,
488 uncompressed_sizes: metadata.uncompressed_sizes.clone(),
489 buffer_alignments: metadata.buffer_alignments.clone(),
490 };
491
492 data.validate()?;
493 Ok(ArrayParts::new(self.clone(), dtype.clone(), len, data).with_slots(slots))
494 }
495
496 fn execute(array: Array<Self>, ctx: &mut ExecutionCtx) -> VortexResult<ExecutionResult> {
499 let session = ctx.session();
500 let inner_array = ZstdBuffers::decompress_and_build_inner(&array, session)?;
501 inner_array
502 .execute::<ArrayRef>(ctx)
503 .map(ExecutionResult::done)
504 }
505}
506
507impl OperationsVTable<ZstdBuffers> for ZstdBuffers {
508 fn scalar_at(
509 array: ArrayView<'_, ZstdBuffers>,
510 index: usize,
511 ctx: &mut ExecutionCtx,
512 ) -> VortexResult<Scalar> {
513 let inner_array =
517 ZstdBuffers::decompress_and_build_inner(&array.into_owned(), ctx.session())?;
518 inner_array.execute_scalar(index, ctx)
519 }
520}
521
522impl ValidityVTable<ZstdBuffers> for ZstdBuffers {
523 #[allow(clippy::disallowed_methods)]
524 fn validity(array: ArrayView<'_, ZstdBuffers>) -> VortexResult<Validity> {
525 if !array.dtype().is_nullable() {
526 return Ok(Validity::NonNullable);
527 }
528
529 let inner_array = ZstdBuffers::decompress_and_build_inner(
530 &array.into_owned(),
531 vortex_array::legacy_session(),
532 )?;
533 inner_array.validity()
534 }
535}
536
537#[cfg(test)]
538mod tests {
539 use rstest::rstest;
540 use vortex_array::ArrayContext;
541 use vortex_array::ArrayRef;
542 use vortex_array::IntoArray;
543 use vortex_array::VortexSessionExecute;
544 use vortex_array::array_session;
545 use vortex_array::arrays::PrimitiveArray;
546 use vortex_array::arrays::VarBinViewArray;
547 use vortex_array::assert_arrays_eq;
548 use vortex_array::expr::stats::Precision;
549 use vortex_array::expr::stats::Stat;
550 use vortex_array::expr::stats::StatsProvider;
551 use vortex_array::serde::SerializeOptions;
552 use vortex_array::serde::SerializedArray;
553 use vortex_array::session::ArraySessionExt;
554 use vortex_buffer::ByteBufferMut;
555 use vortex_error::VortexResult;
556 use vortex_session::registry::ReadContext;
557
558 use super::*;
559
560 fn make_primitive_array() -> ArrayRef {
561 PrimitiveArray::from_iter(0i32..100).into_array()
562 }
563
564 fn make_varbinview_array() -> ArrayRef {
565 VarBinViewArray::from_iter_str(["hello", "world", "foo", "bar", "a longer string here"])
566 .into_array()
567 }
568
569 fn make_nullable_primitive_array() -> ArrayRef {
570 PrimitiveArray::from_option_iter([Some(1i32), None, Some(3), None, Some(5)]).into_array()
571 }
572
573 fn make_nullable_varbinview_array() -> ArrayRef {
574 VarBinViewArray::from_iter_nullable_str([
575 Some("hello"),
576 None,
577 Some("world"),
578 None,
579 Some("a moderately long string for testing"),
580 ])
581 .into_array()
582 }
583
584 fn make_empty_primitive_array() -> ArrayRef {
585 PrimitiveArray::from_iter(Vec::<i32>::new()).into_array()
586 }
587
588 fn make_inlined_varbinview_array() -> ArrayRef {
589 VarBinViewArray::from_iter_str(["hi", "ok", "yes", "no"]).into_array()
590 }
591
592 #[rstest]
593 #[case::primitive(make_primitive_array())]
594 #[case::varbinview(make_varbinview_array())]
595 #[case::nullable_primitive(make_nullable_primitive_array())]
596 #[case::nullable_varbinview(make_nullable_varbinview_array())]
597 #[case::empty_primitive(make_empty_primitive_array())]
598 #[case::inlined_varbinview(make_inlined_varbinview_array())]
599 fn test_roundtrip(#[case] input: ArrayRef) -> VortexResult<()> {
600 let compressed = ZstdBuffers::compress(&input, 3, &array_session())?;
601
602 assert_eq!(compressed.len(), input.len());
603 assert_eq!(compressed.dtype(), input.dtype());
604
605 let mut ctx = array_session().create_execution_ctx();
606 let decompressed = compressed.into_array().execute::<ArrayRef>(&mut ctx)?;
607
608 assert_arrays_eq!(input, decompressed, &mut ctx);
609 Ok(())
610 }
611
612 #[rstest]
613 #[case::primitive(make_primitive_array())]
614 #[case::varbinview(make_varbinview_array())]
615 #[case::nullable_primitive(make_nullable_primitive_array())]
616 #[case::nullable_varbinview(make_nullable_varbinview_array())]
617 #[case::empty_primitive(make_empty_primitive_array())]
618 #[case::inlined_varbinview(make_inlined_varbinview_array())]
619 fn test_serde_roundtrip(#[case] input: ArrayRef) -> VortexResult<()> {
620 let session = array_session();
621 session.arrays().register(ZstdBuffers);
622
623 let compressed = ZstdBuffers::compress(&input, 3, &session)?.into_array();
624 let dtype = compressed.dtype().clone();
625 let len = compressed.len();
626
627 let array_ctx = ArrayContext::empty();
628 let serialized =
629 compressed.serialize(&array_ctx, &session, &SerializeOptions::default())?;
630
631 let mut concat = ByteBufferMut::empty();
632 for buf in serialized {
633 concat.extend_from_slice(buf.as_ref());
634 }
635 let parts = SerializedArray::try_from(concat.freeze())?;
636 let decoded = parts.decode(&dtype, len, &ReadContext::new(array_ctx.to_ids()), &session)?;
637
638 let mut ctx = session.create_execution_ctx();
639 let decoded = decoded.execute::<ArrayRef>(&mut ctx)?;
640 assert_arrays_eq!(input, decoded, &mut ctx);
641 Ok(())
642 }
643
644 #[test]
645 fn test_compress_inherits_stats() -> VortexResult<()> {
646 let input = make_primitive_array();
647 input.statistics().set(Stat::Min, Precision::exact(0i32));
648
649 let compressed = ZstdBuffers::compress(&input, 3, &array_session())?;
650
651 assert!(!compressed.statistics().get(Stat::Min).is_absent());
652 Ok(())
653 }
654
655 #[test]
656 fn test_validity_delegates_for_nullable_input() -> VortexResult<()> {
657 let input = make_nullable_primitive_array();
658 let compressed = ZstdBuffers::compress(&input, 3, &array_session())?.into_array();
659
660 let mut ctx = array_session().create_execution_ctx();
661 assert_eq!(compressed.all_valid(&mut ctx)?, input.all_valid(&mut ctx)?);
662 assert_eq!(
663 compressed.all_invalid(&mut ctx)?,
664 input.all_invalid(&mut ctx)?
665 );
666
667 for i in 0..input.len() {
668 assert_eq!(
669 compressed.is_valid(i, &mut ctx)?,
670 input.is_valid(i, &mut ctx)?
671 );
672 }
673
674 Ok(())
675 }
676}