Skip to main content

vortex_array/arrays/varbin/
array.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Display;
5use std::fmt::Formatter;
6
7use num_traits::AsPrimitive;
8use vortex_array::arrays::PrimitiveArray;
9use vortex_buffer::BufferAllocatorRef;
10use vortex_buffer::ByteBuffer;
11use vortex_error::VortexExpect;
12use vortex_error::VortexResult;
13use vortex_error::vortex_ensure;
14use vortex_error::vortex_err;
15
16use crate::ArrayRef;
17use crate::ArraySlots;
18use crate::VortexSessionExecute;
19use crate::array::Array;
20use crate::array::ArrayParts;
21use crate::array::TypedArrayRef;
22use crate::array::child_to_validity;
23use crate::array::validity_to_child;
24use crate::array_slots;
25use crate::arrays::VarBin;
26use crate::arrays::varbin::builder::VarBinBuilder;
27use crate::buffer::BufferHandle;
28use crate::dtype::DType;
29use crate::dtype::Nullability;
30use crate::dtype::OffsetBuilderPType;
31use crate::legacy_session;
32use crate::match_each_integer_ptype;
33use crate::validity::Validity;
34
35#[array_slots(VarBin)]
36pub struct VarBinSlots {
37    /// The offsets array defining the start/end of each variable-length binary element.
38    #[slot(0)]
39    pub offsets: ArrayRef,
40    /// The validity bitmap indicating which elements are non-null.
41    #[slot(1)]
42    pub validity: Option<ArrayRef>,
43}
44
45#[derive(Clone, Debug)]
46pub struct VarBinData {
47    pub(super) bytes: BufferHandle,
48}
49
50impl Display for VarBinData {
51    fn fmt(&self, _f: &mut Formatter<'_>) -> std::fmt::Result {
52        Ok(())
53    }
54}
55
56pub struct VarBinDataParts {
57    pub dtype: DType,
58    pub bytes: BufferHandle,
59    pub offsets: ArrayRef,
60    pub validity: Validity,
61}
62
63impl VarBinData {
64    /// Creates a new `VarBinArray`.
65    ///
66    /// # Panics
67    ///
68    /// Panics if the provided components do not satisfy the invariants documented
69    /// in `VarBinArray::new_unchecked`.
70    pub fn build(offsets: ArrayRef, bytes: ByteBuffer, dtype: DType, validity: Validity) -> Self {
71        Self::try_build(offsets, bytes, dtype, validity).vortex_expect("VarBinArray new")
72    }
73
74    /// Creates a new `VarBinArray`.
75    ///
76    /// # Panics
77    ///
78    /// Panics if the provided components do not satisfy the invariants documented
79    /// in `VarBinArray::new_unchecked`.
80    pub fn build_from_handle(
81        offset: ArrayRef,
82        bytes: BufferHandle,
83        dtype: DType,
84        validity: Validity,
85    ) -> Self {
86        Self::try_build_from_handle(offset, bytes, dtype, validity).vortex_expect("VarBinArray new")
87    }
88
89    pub(crate) fn make_slots(offsets: ArrayRef, validity: &Validity, len: usize) -> ArraySlots {
90        VarBinSlots {
91            offsets,
92            validity: validity_to_child(validity, len),
93        }
94        .into_slots()
95    }
96
97    /// Constructs a new `VarBinArray`.
98    ///
99    /// See `VarBinArray::new_unchecked` for more information.
100    ///
101    /// # Errors
102    ///
103    /// Returns an error if the provided components do not satisfy the invariants documented in
104    /// `VarBinArray::new_unchecked`.
105    pub fn try_build(
106        offsets: ArrayRef,
107        bytes: ByteBuffer,
108        dtype: DType,
109        validity: Validity,
110    ) -> VortexResult<Self> {
111        let bytes = BufferHandle::new_host(bytes);
112        Self::validate(&offsets, &bytes, &dtype, &validity)?;
113
114        // SAFETY: validate ensures all invariants are met.
115        Ok(unsafe { Self::new_unchecked_from_handle(bytes) })
116    }
117
118    /// Constructs a new `VarBinArray` from a `BufferHandle` of memory that may exist
119    /// on the CPU or GPU.
120    ///
121    /// See `VarBinArray::new_unchecked` for more information.
122    ///
123    /// # Errors
124    ///
125    /// Returns an error if the provided components do not satisfy the invariants documented in
126    /// `VarBinArray::new_unchecked`.
127    pub fn try_build_from_handle(
128        offsets: ArrayRef,
129        bytes: BufferHandle,
130        dtype: DType,
131        validity: Validity,
132    ) -> VortexResult<Self> {
133        Self::validate(&offsets, &bytes, &dtype, &validity)?;
134
135        // SAFETY: validate ensures all invariants are met.
136        Ok(unsafe { Self::new_unchecked_from_handle(bytes) })
137    }
138
139    /// Creates a new `VarBinArray` without validation from these components:
140    ///
141    /// * `offsets` is an array of byte offsets into the `bytes` buffer.
142    /// * `bytes` is a buffer containing all the variable-length data concatenated.
143    /// * `dtype` specifies whether this contains UTF-8 strings or binary data.
144    /// * `validity` holds the null values.
145    ///
146    /// # Safety
147    ///
148    /// The caller must ensure all of the following invariants are satisfied:
149    ///
150    /// ## Offsets Requirements
151    ///
152    /// - `offsets` must be a non-nullable integer array.
153    /// - `offsets` must contain at least 1 element (for empty array, it contains \[0\]).
154    /// - All values in `offsets` must be monotonically non-decreasing.
155    /// - The first value in `offsets` must be 0.
156    /// - No offset value may exceed `bytes.len()`.
157    ///
158    /// ## Type Requirements
159    ///
160    /// - `dtype` must be exactly [`DType::Binary`] or [`DType::Utf8`].
161    /// - If `dtype` is [`DType::Utf8`], every byte slice `bytes[offsets[i]..offsets[i+1]]` must be valid UTF-8.
162    /// - `dtype.is_nullable()` must match the nullability of `validity`.
163    ///
164    /// ## Validity Requirements
165    ///
166    /// - If `validity` is [`Validity::Array`], its length must exactly equal `offsets.len() - 1`.
167    pub unsafe fn new_unchecked(bytes: ByteBuffer) -> Self {
168        // SAFETY: `new_unchecked_from_handle` has same invariants which should be checked
169        //  by caller.
170        unsafe { Self::new_unchecked_from_handle(BufferHandle::new_host(bytes)) }
171    }
172
173    /// Creates a new `VarBinArray` without validation from its components, with string data
174    /// stored in a `BufferHandle` (CPU or GPU).
175    ///
176    /// # Safety
177    ///
178    /// The caller must ensure all the invariants documented in `new_unchecked` are satisfied.
179    pub unsafe fn new_unchecked_from_handle(bytes: BufferHandle) -> Self {
180        Self { bytes }
181    }
182
183    /// Validates the components that would be used to create a `VarBinArray`.
184    ///
185    /// This function checks all the invariants required by `VarBinArray::new_unchecked`.
186    pub fn validate(
187        offsets: &ArrayRef,
188        bytes: &BufferHandle,
189        dtype: &DType,
190        validity: &Validity,
191    ) -> VortexResult<()> {
192        // Check offsets are non-nullable integer
193        vortex_ensure!(
194            offsets.dtype().is_int() && !offsets.dtype().is_nullable(),
195            MismatchedTypes: "non nullable int", offsets.dtype()
196        );
197
198        // Check dtype is Binary or Utf8
199        vortex_ensure!(
200            matches!(dtype, DType::Binary(_) | DType::Utf8(_)),
201            MismatchedTypes: "utf8 or binary", dtype
202        );
203
204        // Check nullability matches
205        vortex_ensure!(
206            dtype.is_nullable() != matches!(validity, Validity::NonNullable),
207            InvalidArgument: "incorrect validity {:?} for dtype {}",
208            validity,
209            dtype
210        );
211
212        // Check offsets has at least one element
213        vortex_ensure!(
214            !offsets.is_empty(),
215            InvalidArgument: "Offsets must have at least one element"
216        );
217
218        // Check validity length
219        if let Some(validity_len) = validity.maybe_len() {
220            vortex_ensure!(
221                validity_len == offsets.len() - 1,
222                "Validity length {} doesn't match array length {}",
223                validity_len,
224                offsets.len() - 1
225            );
226        }
227
228        // Validate UTF-8 for Utf8 dtype. Skip when offsets/bytes are not host-resident.
229        if offsets.is_host()
230            && bytes.is_on_host()
231            && matches!(dtype, DType::Utf8(_))
232            && let Some(bytes) = bytes.as_host_opt()
233        {
234            Self::validate_utf8(offsets, bytes.as_ref(), validity)?;
235        }
236
237        Ok(())
238    }
239
240    /// Validates that every non-null value is valid UTF-8.
241    #[allow(clippy::disallowed_methods)]
242    fn validate_utf8(offsets: &ArrayRef, bytes: &[u8], validity: &Validity) -> VortexResult<()> {
243        let validate_at = |i: usize, start: usize, end: usize| -> VortexResult<()> {
244            let string_bytes = &bytes[start..end];
245            simdutf8::basic::from_utf8(string_bytes).map_err(|_| {
246                #[expect(clippy::unwrap_used)]
247                // run validation using `compat` package to get more detailed error message
248                let err = simdutf8::compat::from_utf8(string_bytes).unwrap_err();
249                vortex_err!("invalid utf-8: {err} at index {i}")
250            })?;
251            Ok(())
252        };
253
254        let mut ctx = legacy_session().create_execution_ctx();
255        // TODO(joe): update the created VarBin with this decompressed Array.
256        let primitive_offsets = offsets.clone().execute::<PrimitiveArray>(&mut ctx)?;
257
258        // Array-backed validity is the only variant that needs an execution context: execute it into
259        // a mask once. The constant variants resolve null-ness without one. Resolving this before
260        // the per-type dispatch keeps the dtype loop simple.
261        let mask = match validity {
262            Validity::Array(_) => {
263                Some(validity.execute_mask(primitive_offsets.len().saturating_sub(1), &mut ctx)?)
264            }
265            _ => None,
266        };
267        let all_invalid = validity.definitely_all_null();
268
269        match_each_integer_ptype!(primitive_offsets.dtype().as_ptype(), |O| {
270            let offsets_slice = primitive_offsets.as_slice::<O>();
271
272            let last_offset: usize = offsets_slice[offsets_slice.len() - 1].as_();
273            vortex_ensure!(
274                last_offset <= bytes.len(),
275                InvalidArgument: "Last offset {} exceeds bytes length {}",
276                last_offset,
277                bytes.len()
278            );
279
280            for (i, (start, end)) in offsets_slice
281                .windows(2)
282                .map(|o| (o[0].as_(), o[1].as_()))
283                .enumerate()
284            {
285                let valid = mask.as_ref().map_or(!all_invalid, |mask| mask.value(i));
286                if valid {
287                    validate_at(i, start, end)?;
288                }
289            }
290        });
291        Ok(())
292    }
293
294    /// Access the value bytes child buffer
295    ///
296    /// # Note
297    ///
298    /// Bytes child buffer is never sliced when the array is sliced so this can include values
299    /// that are not logically present in the array. Users should prefer `sliced_bytes`
300    /// unless they're resolving values via the offset child array.
301    #[inline]
302    pub fn bytes(&self) -> &ByteBuffer {
303        self.bytes.as_host()
304    }
305
306    /// Access the value bytes buffer handle.
307    #[inline]
308    pub fn bytes_handle(&self) -> &BufferHandle {
309        &self.bytes
310    }
311}
312
313pub trait VarBinArrayExt: VarBinArraySlotsExt {
314    fn dtype_parts(&self) -> (bool, Nullability) {
315        match self.as_ref().dtype() {
316            DType::Utf8(nullability) => (true, *nullability),
317            DType::Binary(nullability) => (false, *nullability),
318            _ => unreachable!("VarBinArrayExt requires a utf8 or binary dtype"),
319        }
320    }
321
322    fn is_utf8(&self) -> bool {
323        self.dtype_parts().0
324    }
325
326    fn nullability(&self) -> Nullability {
327        self.dtype_parts().1
328    }
329
330    fn varbin_validity(&self) -> Validity {
331        child_to_validity(
332            self.as_ref().slots()[VarBinSlots::VALIDITY].as_ref(),
333            self.nullability(),
334        )
335    }
336
337    #[allow(clippy::disallowed_methods)]
338    fn offset_at(&self, index: usize) -> usize {
339        assert!(
340            index <= self.as_ref().len(),
341            "Index {index} out of bounds 0..={}",
342            self.as_ref().len()
343        );
344
345        (&self
346            .offsets()
347            .execute_scalar(index, &mut legacy_session().create_execution_ctx())
348            .vortex_expect("offsets must support execute_scalar"))
349            .try_into()
350            .vortex_expect("Failed to convert offset to usize")
351    }
352
353    fn bytes_at(&self, index: usize) -> ByteBuffer {
354        let start = self.offset_at(index);
355        let end = self.offset_at(index + 1);
356        self.bytes().slice(start..end)
357    }
358
359    fn sliced_bytes(&self) -> ByteBuffer {
360        let first_offset: usize = self.offset_at(0);
361        let last_offset = self.offset_at(self.as_ref().len());
362        self.bytes().slice(first_offset..last_offset)
363    }
364}
365impl<T: TypedArrayRef<VarBin>> VarBinArrayExt for T {}
366
367/// Forwarding constructors for `VarBinArray` (= `Array<VarBin>`).
368impl Array<VarBin> {
369    pub fn from_vec<T: AsRef<[u8]>>(vec: Vec<T>, dtype: DType) -> Self {
370        let size: usize = vec.iter().map(|v| v.as_ref().len()).sum();
371        if size < u32::MAX as usize {
372            Self::from_vec_sized::<u32, T>(vec, dtype)
373        } else {
374            Self::from_vec_sized::<u64, T>(vec, dtype)
375        }
376    }
377
378    #[expect(
379        clippy::same_name_method,
380        reason = "intentionally named from_iter like Iterator::from_iter"
381    )]
382    pub fn from_iter<T: AsRef<[u8]>, I: IntoIterator<Item = Option<T>>>(
383        iter: I,
384        dtype: DType,
385    ) -> Self {
386        let iter = iter.into_iter();
387        let mut builder = VarBinBuilder::<u32>::with_capacity_in(
388            dtype,
389            iter.size_hint().0,
390            BufferAllocatorRef::static_ref(),
391        );
392        for v in iter {
393            builder.append(v.as_ref().map(|o| o.as_ref()));
394        }
395        builder.finish_into_varbin()
396    }
397
398    pub fn from_iter_nonnull<T: AsRef<[u8]>, I: IntoIterator<Item = T>>(
399        iter: I,
400        dtype: DType,
401    ) -> Self {
402        let iter = iter.into_iter();
403        let mut builder = VarBinBuilder::<u32>::with_capacity_in(
404            dtype,
405            iter.size_hint().0,
406            BufferAllocatorRef::static_ref(),
407        );
408        for v in iter {
409            builder.append_value(v);
410        }
411        builder.finish_into_varbin()
412    }
413
414    fn from_vec_sized<O, T>(vec: Vec<T>, dtype: DType) -> Self
415    where
416        O: OffsetBuilderPType,
417        T: AsRef<[u8]>,
418    {
419        let mut builder = VarBinBuilder::<O>::with_capacity_in(
420            dtype,
421            vec.len(),
422            BufferAllocatorRef::static_ref(),
423        );
424        for v in vec {
425            builder.append_value(v.as_ref());
426        }
427        builder.finish_into_varbin()
428    }
429
430    /// Create from a vector of string slices.
431    pub fn from_strs(value: Vec<&str>) -> Self {
432        Self::from_vec(value, DType::Utf8(Nullability::NonNullable))
433    }
434
435    /// Create from a vector of optional string slices.
436    pub fn from_nullable_strs(value: Vec<Option<&str>>) -> Self {
437        Self::from_iter(value, DType::Utf8(Nullability::Nullable))
438    }
439
440    /// Create from a vector of byte slices.
441    pub fn from_bytes(value: Vec<&[u8]>) -> Self {
442        Self::from_vec(value, DType::Binary(Nullability::NonNullable))
443    }
444
445    /// Create from a vector of optional byte slices.
446    pub fn from_nullable_bytes(value: Vec<Option<&[u8]>>) -> Self {
447        Self::from_iter(value, DType::Binary(Nullability::Nullable))
448    }
449
450    pub fn into_data_parts(self) -> VarBinDataParts {
451        let dtype = self.dtype().clone();
452        let validity = self.varbin_validity();
453        let offsets = self.offsets().clone();
454        let data = self.into_data();
455        VarBinDataParts {
456            dtype,
457            bytes: data.bytes,
458            offsets,
459            validity,
460        }
461    }
462}
463
464impl Array<VarBin> {
465    /// Creates a new `VarBinArray`.
466    pub fn new(offsets: ArrayRef, bytes: ByteBuffer, dtype: DType, validity: Validity) -> Self {
467        let len = offsets.len().saturating_sub(1);
468        let slots = VarBinData::make_slots(offsets, &validity, len);
469        let data = VarBinData::build(
470            slots[VarBinSlots::OFFSETS]
471                .as_ref()
472                .vortex_expect("VarBinArray offsets slot")
473                .clone(),
474            bytes,
475            dtype.clone(),
476            validity,
477        );
478        unsafe {
479            Array::from_parts_unchecked(ArrayParts::new(VarBin, dtype, len, data).with_slots(slots))
480        }
481    }
482
483    /// Creates a new `VarBinArray` without validation.
484    ///
485    /// # Safety
486    ///
487    /// See [`VarBinData::new_unchecked`].
488    pub unsafe fn new_unchecked(
489        offsets: ArrayRef,
490        bytes: ByteBuffer,
491        dtype: DType,
492        validity: Validity,
493    ) -> Self {
494        let len = offsets.len().saturating_sub(1);
495        let slots = VarBinData::make_slots(offsets, &validity, len);
496        let data = unsafe { VarBinData::new_unchecked(bytes) };
497        unsafe {
498            Array::from_parts_unchecked(ArrayParts::new(VarBin, dtype, len, data).with_slots(slots))
499        }
500    }
501
502    /// Creates a new `VarBinArray` without validation from a [`BufferHandle`].
503    ///
504    /// # Safety
505    ///
506    /// See [`VarBinData::new_unchecked_from_handle`].
507    pub unsafe fn new_unchecked_from_handle(
508        offsets: ArrayRef,
509        bytes: BufferHandle,
510        dtype: DType,
511        validity: Validity,
512    ) -> Self {
513        let len = offsets.len().saturating_sub(1);
514        let slots = VarBinData::make_slots(offsets, &validity, len);
515        let data = unsafe { VarBinData::new_unchecked_from_handle(bytes) };
516        unsafe {
517            Array::from_parts_unchecked(ArrayParts::new(VarBin, dtype, len, data).with_slots(slots))
518        }
519    }
520
521    /// Constructs a new `VarBinArray`.
522    pub fn try_new(
523        offsets: ArrayRef,
524        bytes: ByteBuffer,
525        dtype: DType,
526        validity: Validity,
527    ) -> VortexResult<Self> {
528        let len = offsets.len() - 1;
529        let bytes = BufferHandle::new_host(bytes);
530        VarBinData::validate(&offsets, &bytes, &dtype, &validity)?;
531        let slots = VarBinData::make_slots(offsets, &validity, len);
532        // SAFETY: validate ensures all invariants are met.
533        let data = unsafe { VarBinData::new_unchecked_from_handle(bytes) };
534        Ok(unsafe {
535            Array::from_parts_unchecked(ArrayParts::new(VarBin, dtype, len, data).with_slots(slots))
536        })
537    }
538}
539
540impl From<Vec<&[u8]>> for Array<VarBin> {
541    fn from(value: Vec<&[u8]>) -> Self {
542        Self::from_vec(value, DType::Binary(Nullability::NonNullable))
543    }
544}
545
546impl From<Vec<Vec<u8>>> for Array<VarBin> {
547    fn from(value: Vec<Vec<u8>>) -> Self {
548        Self::from_vec(value, DType::Binary(Nullability::NonNullable))
549    }
550}
551
552impl From<Vec<String>> for Array<VarBin> {
553    fn from(value: Vec<String>) -> Self {
554        Self::from_vec(value, DType::Utf8(Nullability::NonNullable))
555    }
556}
557
558impl From<Vec<&str>> for Array<VarBin> {
559    fn from(value: Vec<&str>) -> Self {
560        Self::from_vec(value, DType::Utf8(Nullability::NonNullable))
561    }
562}
563
564impl From<Vec<Option<&[u8]>>> for Array<VarBin> {
565    fn from(value: Vec<Option<&[u8]>>) -> Self {
566        Self::from_iter(value, DType::Binary(Nullability::Nullable))
567    }
568}
569
570impl From<Vec<Option<Vec<u8>>>> for Array<VarBin> {
571    fn from(value: Vec<Option<Vec<u8>>>) -> Self {
572        Self::from_iter(value, DType::Binary(Nullability::Nullable))
573    }
574}
575
576impl From<Vec<Option<String>>> for Array<VarBin> {
577    fn from(value: Vec<Option<String>>) -> Self {
578        Self::from_iter(value, DType::Utf8(Nullability::Nullable))
579    }
580}
581
582impl From<Vec<Option<&str>>> for Array<VarBin> {
583    fn from(value: Vec<Option<&str>>) -> Self {
584        Self::from_iter(value, DType::Utf8(Nullability::Nullable))
585    }
586}
587
588impl<'a> FromIterator<Option<&'a [u8]>> for Array<VarBin> {
589    fn from_iter<T: IntoIterator<Item = Option<&'a [u8]>>>(iter: T) -> Self {
590        Self::from_iter(iter, DType::Binary(Nullability::Nullable))
591    }
592}
593
594impl FromIterator<Option<Vec<u8>>> for Array<VarBin> {
595    fn from_iter<T: IntoIterator<Item = Option<Vec<u8>>>>(iter: T) -> Self {
596        Self::from_iter(iter, DType::Binary(Nullability::Nullable))
597    }
598}
599
600impl FromIterator<Option<String>> for Array<VarBin> {
601    fn from_iter<T: IntoIterator<Item = Option<String>>>(iter: T) -> Self {
602        Self::from_iter(iter, DType::Utf8(Nullability::Nullable))
603    }
604}
605
606impl<'a> FromIterator<Option<&'a str>> for Array<VarBin> {
607    fn from_iter<T: IntoIterator<Item = Option<&'a str>>>(iter: T) -> Self {
608        Self::from_iter(iter, DType::Utf8(Nullability::Nullable))
609    }
610}