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