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