Skip to main content

vortex_array/arrays/primitive/array/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Display;
5use std::fmt::Formatter;
6use std::iter::repeat;
7
8use smallvec::smallvec;
9use vortex_buffer::Alignment;
10use vortex_buffer::Buffer;
11use vortex_buffer::BufferMut;
12use vortex_buffer::ByteBuffer;
13use vortex_buffer::ByteBufferMut;
14use vortex_error::VortexExpect;
15use vortex_error::VortexResult;
16use vortex_error::vortex_err;
17use vortex_error::vortex_panic;
18
19use crate::ArraySlots;
20use crate::ExecutionCtx;
21use crate::array::Array;
22use crate::array::ArrayParts;
23use crate::array::TypedArrayRef;
24use crate::arrays::BoolArray;
25use crate::arrays::Primitive;
26use crate::arrays::PrimitiveArray;
27use crate::dtype::DType;
28use crate::dtype::NativePType;
29use crate::dtype::Nullability;
30use crate::dtype::PType;
31use crate::match_each_native_ptype;
32use crate::validity::Validity;
33
34mod cast;
35mod conversion;
36mod patch;
37mod top_value;
38
39pub use patch::chunk_range;
40pub use patch::patch_chunk;
41
42use crate::ArrayRef;
43use crate::aggregate_fn::NumericalAggregateOpts;
44use crate::aggregate_fn::fns::min_max::min_max;
45use crate::array::child_to_validity;
46use crate::array::validity_to_child;
47use crate::array_slots;
48use crate::arrays::bool::BoolArrayExt;
49use crate::buffer::BufferHandle;
50use crate::builtins::ArrayBuiltins;
51
52#[array_slots(Primitive)]
53pub struct PrimitiveSlots {
54    /// The validity bitmap indicating which elements are non-null.
55    pub validity: Option<ArrayRef>,
56}
57
58/// A primitive array that stores [native types][crate::dtype::NativePType] in a contiguous buffer
59/// of memory, along with an optional validity child.
60///
61/// This mirrors the Apache Arrow Primitive layout and can be converted into and out of one
62/// without allocations or copies.
63///
64/// The underlying buffer must be natively aligned to the primitive type they are representing.
65///
66/// Values are stored in their native representation with proper alignment.
67/// Null values still occupy space in the buffer but are marked invalid in the validity mask.
68///
69/// # Examples
70///
71/// ```
72/// # fn main() -> vortex_error::VortexResult<()> {
73/// use vortex_array::arrays::PrimitiveArray;
74/// use vortex_array::{VortexSessionExecute, array_session};
75///
76/// // Create from iterator using FromIterator impl
77/// let array: PrimitiveArray = [1i32, 2, 3, 4, 5].into_iter().collect();
78///
79/// // Slice the array
80/// let sliced = array.slice(1..3)?;
81///
82/// // Access individual values
83/// let mut ctx = array_session().create_execution_ctx();
84/// let value = sliced.execute_scalar(0, &mut ctx).unwrap();
85/// assert_eq!(value, 2i32.into());
86///
87/// # Ok(())
88/// # }
89/// ```
90#[derive(Clone, Debug)]
91pub struct PrimitiveData {
92    pub(super) ptype: PType,
93    pub(super) buffer: BufferHandle,
94}
95
96impl Display for PrimitiveData {
97    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
98        write!(f, "ptype: {}", self.ptype)
99    }
100}
101
102pub struct PrimitiveDataParts {
103    pub ptype: PType,
104    pub buffer: BufferHandle,
105    pub validity: Validity,
106}
107
108pub trait PrimitiveArrayExt: TypedArrayRef<Primitive> {
109    fn ptype(&self) -> PType {
110        match self.as_ref().dtype() {
111            DType::Primitive(ptype, _) => *ptype,
112            _ => unreachable!("PrimitiveArrayExt requires a primitive dtype"),
113        }
114    }
115
116    fn nullability(&self) -> Nullability {
117        match self.as_ref().dtype() {
118            DType::Primitive(_, nullability) => *nullability,
119            _ => unreachable!("PrimitiveArrayExt requires a primitive dtype"),
120        }
121    }
122
123    fn validity_child(&self) -> Option<&ArrayRef> {
124        self.as_ref().slots()[PrimitiveSlots::VALIDITY].as_ref()
125    }
126
127    fn validity(&self) -> Validity {
128        child_to_validity(
129            self.as_ref().slots()[PrimitiveSlots::VALIDITY].as_ref(),
130            self.nullability(),
131        )
132    }
133
134    fn buffer_handle(&self) -> &BufferHandle {
135        &self.buffer
136    }
137
138    fn reinterpret_cast(&self, ptype: PType) -> PrimitiveArray {
139        if self.ptype() == ptype {
140            return self.to_owned();
141        }
142
143        assert_eq!(
144            self.ptype().byte_width(),
145            ptype.byte_width(),
146            "can't reinterpret cast between integers of two different widths"
147        );
148
149        PrimitiveArray::from_buffer_handle(
150            self.buffer_handle().clone(),
151            ptype,
152            PrimitiveArrayExt::validity(self),
153        )
154    }
155
156    /// Narrow the array to the smallest possible integer type that can represent all values.
157    fn narrow(&self, ctx: &mut ExecutionCtx) -> VortexResult<PrimitiveArray> {
158        if !self.ptype().is_int() {
159            return Ok(self.to_owned());
160        }
161
162        let Some(min_max) = min_max(self.as_ref(), ctx, NumericalAggregateOpts::default())? else {
163            return Ok(PrimitiveArray::new(
164                Buffer::<u8>::zeroed(self.len()),
165                PrimitiveArrayExt::validity(self),
166            ));
167        };
168
169        // If we can't cast to i64, then leave the array as its original type.
170        // It's too big to downcast anyway.
171        let Ok(min) = min_max
172            .min
173            .cast(&PType::I64.into())
174            .and_then(|s| i64::try_from(&s))
175        else {
176            return Ok(self.to_owned());
177        };
178        let Ok(max) = min_max
179            .max
180            .cast(&PType::I64.into())
181            .and_then(|s| i64::try_from(&s))
182        else {
183            return Ok(self.to_owned());
184        };
185
186        let nullability = self.as_ref().dtype().nullability();
187
188        if min < 0 || max < 0 {
189            // Signed
190            if min >= i8::MIN as i64 && max <= i8::MAX as i64 {
191                let result = self
192                    .as_ref()
193                    .cast(DType::Primitive(PType::I8, nullability))?
194                    .execute::<PrimitiveArray>(ctx)?;
195                return Ok(result);
196            }
197
198            if min >= i16::MIN as i64 && max <= i16::MAX as i64 {
199                let result = self
200                    .as_ref()
201                    .cast(DType::Primitive(PType::I16, nullability))?
202                    .execute::<PrimitiveArray>(ctx)?;
203                return Ok(result);
204            }
205
206            if min >= i32::MIN as i64 && max <= i32::MAX as i64 {
207                let result = self
208                    .as_ref()
209                    .cast(DType::Primitive(PType::I32, nullability))?
210                    .execute::<PrimitiveArray>(ctx)?;
211                return Ok(result);
212            }
213        } else {
214            // Unsigned
215            if max <= u8::MAX as i64 {
216                let result = self
217                    .as_ref()
218                    .cast(DType::Primitive(PType::U8, nullability))?
219                    .execute::<PrimitiveArray>(ctx)?;
220                return Ok(result);
221            }
222
223            if max <= u16::MAX as i64 {
224                let result = self
225                    .as_ref()
226                    .cast(DType::Primitive(PType::U16, nullability))?
227                    .execute::<PrimitiveArray>(ctx)?;
228                return Ok(result);
229            }
230
231            if max <= u32::MAX as i64 {
232                let result = self
233                    .as_ref()
234                    .cast(DType::Primitive(PType::U32, nullability))?
235                    .execute::<PrimitiveArray>(ctx)?;
236                return Ok(result);
237            }
238        }
239
240        Ok(self.to_owned())
241    }
242}
243impl<T: TypedArrayRef<Primitive>> PrimitiveArrayExt for T {}
244
245// TODO(connor): There are a lot of places where we could be using `new_unchecked` in the codebase.
246impl PrimitiveData {
247    /// Build the slots vector for this array.
248    pub(super) fn make_slots(validity: &Validity, len: usize) -> ArraySlots {
249        smallvec![validity_to_child(validity, len)]
250    }
251
252    /// Create a new array from a buffer handle.
253    ///
254    /// # Safety
255    ///
256    /// Should ensure that the provided BufferHandle points at sufficiently large region of aligned
257    /// memory to hold the `ptype` values.
258    pub unsafe fn new_unchecked_from_handle(
259        handle: BufferHandle,
260        ptype: PType,
261        _validity: Validity,
262    ) -> Self {
263        Self {
264            ptype,
265            buffer: handle,
266        }
267    }
268
269    /// Creates a new `PrimitiveArray`.
270    ///
271    /// # Panics
272    ///
273    /// Panics if the provided components do not satisfy the invariants documented
274    /// in `PrimitiveArray::new_unchecked`.
275    pub fn new<T: NativePType>(buffer: impl Into<Buffer<T>>, validity: Validity) -> Self {
276        let buffer = buffer.into();
277        Self::try_new(buffer, validity).vortex_expect("PrimitiveArray construction failed")
278    }
279
280    /// Constructs a new `PrimitiveArray`.
281    ///
282    /// See `PrimitiveArray::new_unchecked` for more information.
283    ///
284    /// # Errors
285    ///
286    /// Returns an error if the provided components do not satisfy the invariants documented in
287    /// `PrimitiveArray::new_unchecked`.
288    #[inline]
289    pub fn try_new<T: NativePType>(buffer: Buffer<T>, validity: Validity) -> VortexResult<Self> {
290        Self::validate(&buffer, &validity)?;
291
292        // SAFETY: validate ensures all invariants are met.
293        Ok(unsafe { Self::new_unchecked(buffer, validity) })
294    }
295
296    /// Creates a new `PrimitiveArray` without validation from these components:
297    ///
298    /// * `buffer` is a typed buffer containing the primitive values.
299    /// * `validity` holds the null values.
300    ///
301    /// # Safety
302    ///
303    /// The caller must ensure all of the following invariants are satisfied:
304    ///
305    /// ## Validity Requirements
306    ///
307    /// - If `validity` is [`Validity::Array`], its length must exactly equal `buffer.len()`.
308    #[inline]
309    pub unsafe fn new_unchecked<T: NativePType>(buffer: Buffer<T>, _validity: Validity) -> Self {
310        #[cfg(debug_assertions)]
311        Self::validate(&buffer, &_validity)
312            .vortex_expect("[Debug Assertion]: Invalid `PrimitiveArray` parameters");
313
314        Self {
315            ptype: T::PTYPE,
316            buffer: BufferHandle::new_host(buffer.into_byte_buffer()),
317        }
318    }
319
320    /// Validates the components that would be used to create a `PrimitiveArray`.
321    ///
322    /// This function checks all the invariants required by `PrimitiveArray::new_unchecked`.
323    #[inline]
324    pub fn validate<T: NativePType>(buffer: &Buffer<T>, validity: &Validity) -> VortexResult<()> {
325        if let Some(len) = validity.maybe_len()
326            && buffer.len() != len
327        {
328            return Err(vortex_err!(
329                InvalidArgument:
330                "Buffer and validity length mismatch: buffer={}, validity={}",
331                buffer.len(),
332                len
333            ));
334        }
335        Ok(())
336    }
337
338    pub fn empty<T: NativePType>(nullability: Nullability) -> Self {
339        Self::new(Buffer::<T>::empty(), nullability.into())
340    }
341}
342
343impl Array<Primitive> {
344    pub fn empty<T: NativePType>(nullability: Nullability) -> Self {
345        let dtype = DType::Primitive(T::PTYPE, nullability);
346        let len = 0;
347        let data = PrimitiveData::empty::<T>(nullability);
348        let slots = PrimitiveData::make_slots(&Validity::from(nullability), len);
349        unsafe {
350            Array::from_parts_unchecked(
351                ArrayParts::new(Primitive, dtype, len, data).with_slots(slots),
352            )
353        }
354    }
355
356    /// Creates a new `PrimitiveArray`.
357    ///
358    /// # Panics
359    ///
360    /// Panics if the provided components do not satisfy the invariants.
361    pub fn new<T: NativePType>(buffer: impl Into<Buffer<T>>, validity: Validity) -> Self {
362        let buffer = buffer.into();
363        let dtype = DType::Primitive(T::PTYPE, validity.nullability());
364        let len = buffer.len();
365        let slots = PrimitiveData::make_slots(&validity, len);
366        let data = PrimitiveData::new(buffer, validity);
367        unsafe {
368            Array::from_parts_unchecked(
369                ArrayParts::new(Primitive, dtype, len, data).with_slots(slots),
370            )
371        }
372    }
373
374    /// Constructs a new `PrimitiveArray`.
375    pub fn try_new<T: NativePType>(buffer: Buffer<T>, validity: Validity) -> VortexResult<Self> {
376        let dtype = DType::Primitive(T::PTYPE, validity.nullability());
377        let len = buffer.len();
378        let slots = PrimitiveData::make_slots(&validity, len);
379        let data = PrimitiveData::try_new(buffer, validity)?;
380        Ok(unsafe {
381            Array::from_parts_unchecked(
382                ArrayParts::new(Primitive, dtype, len, data).with_slots(slots),
383            )
384        })
385    }
386
387    /// Creates a new `PrimitiveArray` without validation.
388    ///
389    /// # Safety
390    ///
391    /// See [`PrimitiveData::new_unchecked`].
392    pub unsafe fn new_unchecked<T: NativePType>(buffer: Buffer<T>, validity: Validity) -> Self {
393        let dtype = DType::Primitive(T::PTYPE, validity.nullability());
394        let len = buffer.len();
395        let slots = PrimitiveData::make_slots(&validity, len);
396        let data = unsafe { PrimitiveData::new_unchecked(buffer, validity) };
397        unsafe {
398            Array::from_parts_unchecked(
399                ArrayParts::new(Primitive, dtype, len, data).with_slots(slots),
400            )
401        }
402    }
403
404    /// Create a new array from a buffer handle.
405    ///
406    /// # Safety
407    ///
408    /// See [`PrimitiveData::new_unchecked_from_handle`].
409    pub unsafe fn new_unchecked_from_handle(
410        handle: BufferHandle,
411        ptype: PType,
412        validity: Validity,
413    ) -> Self {
414        let dtype = DType::Primitive(ptype, validity.nullability());
415        let len = handle.len() / ptype.byte_width();
416        let slots = PrimitiveData::make_slots(&validity, len);
417        let data = unsafe { PrimitiveData::new_unchecked_from_handle(handle, ptype, validity) };
418        unsafe {
419            Array::from_parts_unchecked(
420                ArrayParts::new(Primitive, dtype, len, data).with_slots(slots),
421            )
422        }
423    }
424
425    /// Creates a new `PrimitiveArray` from a [`BufferHandle`].
426    pub fn from_buffer_handle(handle: BufferHandle, ptype: PType, validity: Validity) -> Self {
427        let dtype = DType::Primitive(ptype, validity.nullability());
428        let len = handle.len() / ptype.byte_width();
429        let slots = PrimitiveData::make_slots(&validity, len);
430        let data = PrimitiveData::from_buffer_handle(handle, ptype, validity);
431        Array::try_from_parts(ArrayParts::new(Primitive, dtype, len, data).with_slots(slots))
432            .vortex_expect("PrimitiveData is always valid")
433    }
434
435    /// Creates a new `PrimitiveArray` from a [`ByteBuffer`].
436    pub fn from_byte_buffer(buffer: ByteBuffer, ptype: PType, validity: Validity) -> Self {
437        let dtype = DType::Primitive(ptype, validity.nullability());
438        let len = buffer.len() / ptype.byte_width();
439        let slots = PrimitiveData::make_slots(&validity, len);
440        let data = PrimitiveData::from_byte_buffer(buffer, ptype, validity);
441        unsafe {
442            Array::from_parts_unchecked(
443                ArrayParts::new(Primitive, dtype, len, data).with_slots(slots),
444            )
445        }
446    }
447
448    /// Create a PrimitiveArray from a byte buffer containing only the valid elements.
449    pub fn from_values_byte_buffer(
450        valid_elems_buffer: ByteBuffer,
451        ptype: PType,
452        validity: Validity,
453        n_rows: usize,
454        ctx: &mut ExecutionCtx,
455    ) -> Self {
456        let dtype = DType::Primitive(ptype, validity.nullability());
457        let len = n_rows;
458        let slots = PrimitiveData::make_slots(&validity, len);
459        let data = PrimitiveData::from_values_byte_buffer(
460            valid_elems_buffer,
461            ptype,
462            validity,
463            n_rows,
464            ctx,
465        );
466        unsafe {
467            Array::from_parts_unchecked(
468                ArrayParts::new(Primitive, dtype, len, data).with_slots(slots),
469            )
470        }
471    }
472
473    /// Validates the components that would be used to create a `PrimitiveArray`.
474    pub fn validate<T: NativePType>(buffer: &Buffer<T>, validity: &Validity) -> VortexResult<()> {
475        PrimitiveData::validate(buffer, validity)
476    }
477
478    pub fn into_data_parts(self) -> PrimitiveDataParts {
479        let validity = PrimitiveArrayExt::validity(&self);
480        let ptype = PrimitiveArrayExt::ptype(&self);
481        let data = self.into_data();
482        PrimitiveDataParts {
483            ptype,
484            buffer: data.buffer,
485            validity,
486        }
487    }
488
489    pub fn map_each_with_validity<T, R, F>(self, ctx: &mut ExecutionCtx, f: F) -> VortexResult<Self>
490    where
491        T: NativePType,
492        R: NativePType,
493        F: FnMut((T, bool)) -> R,
494    {
495        let validity = PrimitiveArrayExt::validity(&self);
496        let data = self.into_data();
497        let buf_iter = data.to_buffer::<T>().into_iter();
498
499        let buffer = match &validity {
500            Validity::NonNullable | Validity::AllValid => {
501                Buffer::<R>::from_trusted_len_iter(buf_iter.zip(repeat(true)).map(f))
502            }
503            Validity::AllInvalid => {
504                Buffer::<R>::from_trusted_len_iter(buf_iter.zip(repeat(false)).map(f))
505            }
506            Validity::Array(val) => {
507                let val = val.clone().execute::<BoolArray>(ctx)?.into_bit_buffer();
508                Buffer::<R>::from_trusted_len_iter(buf_iter.zip(val.iter()).map(f))
509            }
510        };
511        Ok(PrimitiveArray::new(buffer, validity))
512    }
513}
514
515impl PrimitiveData {
516    pub fn len(&self) -> usize {
517        self.buffer.len() / self.ptype.byte_width()
518    }
519
520    /// Returns `true` if the array is empty.
521    pub fn is_empty(&self) -> bool {
522        self.buffer.is_empty()
523    }
524
525    pub fn ptype(&self) -> PType {
526        self.ptype
527    }
528
529    /// Get access to the buffer handle backing the array.
530    pub fn buffer_handle(&self) -> &BufferHandle {
531        &self.buffer
532    }
533
534    pub fn from_buffer_handle(handle: BufferHandle, ptype: PType, _validity: Validity) -> Self {
535        Self {
536            ptype,
537            buffer: handle,
538        }
539    }
540
541    pub fn from_byte_buffer(buffer: ByteBuffer, ptype: PType, validity: Validity) -> Self {
542        match_each_native_ptype!(ptype, |T| {
543            Self::new::<T>(Buffer::from_byte_buffer(buffer), validity)
544        })
545    }
546
547    /// Create a PrimitiveArray from a byte buffer containing only the valid elements.
548    pub fn from_values_byte_buffer(
549        valid_elems_buffer: ByteBuffer,
550        ptype: PType,
551        validity: Validity,
552        n_rows: usize,
553        ctx: &mut ExecutionCtx,
554    ) -> Self {
555        let byte_width = ptype.byte_width();
556        let alignment = Alignment::new(byte_width);
557        let buffer = match &validity {
558            Validity::AllValid | Validity::NonNullable => valid_elems_buffer.aligned(alignment),
559            Validity::AllInvalid => ByteBuffer::zeroed_aligned(n_rows * byte_width, alignment),
560            Validity::Array(is_valid) => {
561                let bool_array = is_valid
562                    .clone()
563                    .execute::<BoolArray>(ctx)
564                    .vortex_expect("must be a bool array");
565                let bool_buffer = bool_array.bit_buffer_view();
566                let mut bytes = ByteBufferMut::zeroed_aligned(n_rows * byte_width, alignment);
567                for (i, valid_i) in bool_buffer.set_indices().enumerate() {
568                    bytes[valid_i * byte_width..(valid_i + 1) * byte_width]
569                        .copy_from_slice(&valid_elems_buffer[i * byte_width..(i + 1) * byte_width])
570                }
571                bytes.freeze()
572            }
573        };
574
575        Self::from_byte_buffer(buffer, ptype, validity)
576    }
577
578    /// Get a buffer in host memory holding all the values.
579    ///
580    /// NOTE: some values may be nonsense if the validity buffer indicates that the value is null.
581    pub fn to_buffer<T: NativePType>(&self) -> Buffer<T> {
582        if T::PTYPE != self.ptype() {
583            vortex_panic!(
584                "Attempted to get buffer of type {} from array of type {}",
585                T::PTYPE,
586                self.ptype()
587            )
588        }
589        Buffer::from_byte_buffer(self.buffer_handle().to_host_sync())
590    }
591
592    /// Consume the array and get a host Buffer containing the data values.
593    pub fn into_buffer<T: NativePType>(self) -> Buffer<T> {
594        if T::PTYPE != self.ptype() {
595            vortex_panic!(
596                "Attempted to get buffer of type {} from array of type {}",
597                T::PTYPE,
598                self.ptype()
599            )
600        }
601        Buffer::from_byte_buffer(self.buffer.into_host_sync())
602    }
603
604    /// Extract a mutable buffer from the PrimitiveData. Attempts to do this with zero-copy
605    /// if the buffer is uniquely owned, otherwise will make a copy.
606    pub fn into_buffer_mut<T: NativePType>(self) -> BufferMut<T> {
607        self.try_into_buffer_mut()
608            .unwrap_or_else(|buffer| BufferMut::<T>::copy_from(&buffer))
609    }
610
611    /// Try to extract a mutable buffer from the PrimitiveData with zero copy.
612    ///
613    /// # Panic
614    /// If the buffer is not of type T this will panic
615    pub fn try_into_buffer_mut<T: NativePType>(self) -> Result<BufferMut<T>, Buffer<T>> {
616        if T::PTYPE != self.ptype() {
617            vortex_panic!(
618                "Attempted to get buffer_mut of type {} from array of type {}",
619                T::PTYPE,
620                self.ptype()
621            )
622        }
623        let buffer = Buffer::<T>::from_byte_buffer(self.buffer.into_host_sync());
624        buffer.try_into_mut()
625    }
626}