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