Skip to main content

vortex_array/arrays/chunked/
array.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! First-class chunked arrays.
5//!
6//! Vortex is a chunked array library that's able to
7
8use std::fmt::Debug;
9use std::fmt::Display;
10use std::fmt::Formatter;
11
12use futures::stream;
13use vortex_buffer::BufferMut;
14use vortex_error::VortexExpect;
15use vortex_error::VortexResult;
16use vortex_error::vortex_bail;
17
18use crate::ArrayRef;
19use crate::ArraySlots;
20use crate::Canonical;
21use crate::ExecutionCtx;
22use crate::IntoArray;
23use crate::array::Array;
24use crate::array::ArrayParts;
25use crate::array::TypedArrayRef;
26use crate::array_slots;
27use crate::arrays::Chunked;
28use crate::arrays::PrimitiveArray;
29use crate::dtype::DType;
30use crate::iter::ArrayIterator;
31use crate::iter::ArrayIteratorAdapter;
32use crate::search_sorted::SearchSorted;
33use crate::search_sorted::SearchSortedSide;
34use crate::stream::ArrayStream;
35use crate::stream::ArrayStreamAdapter;
36use crate::validity::Validity;
37
38/// Slot layout of a [`Chunked`] array: `[chunk_offsets, chunks...]`.
39#[array_slots(Chunked)]
40pub struct ChunkedSlots {
41    /// The non-nullable `u64` array of cumulative chunk offsets.
42    #[slot(0)]
43    pub chunk_offsets: ArrayRef,
44    /// The chunk arrays, each sharing the outer dtype.
45    #[slot(1..)]
46    pub chunks: Vec<ArrayRef>,
47}
48
49#[derive(Clone, Debug)]
50pub struct ChunkedData {
51    pub(super) chunk_offsets: Vec<usize>,
52    /// This is used to find the next child to execute when in executing into a builder.
53    pub(super) next_builder_slot: usize,
54}
55
56impl Display for ChunkedData {
57    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
58        write!(f, "nchunks: {}", self.chunk_offsets.len().saturating_sub(1))
59    }
60}
61
62pub trait ChunkedArrayExt: TypedArrayRef<Chunked> {
63    fn chunk_offsets_array(&self) -> &ArrayRef {
64        self.as_ref().slots()[ChunkedSlots::CHUNK_OFFSETS]
65            .as_ref()
66            .vortex_expect("validated chunk offsets slot")
67    }
68
69    fn nchunks(&self) -> usize {
70        self.as_ref()
71            .slots()
72            .len()
73            .saturating_sub(ChunkedSlots::CHUNKS_OFFSET)
74    }
75
76    fn chunk(&self, idx: usize) -> &ArrayRef {
77        self.as_ref().slots()[ChunkedSlots::CHUNKS_OFFSET + idx]
78            .as_ref()
79            .vortex_expect("validated chunk slot")
80    }
81
82    fn iter_chunks<'a>(&'a self) -> Box<dyn Iterator<Item = &'a ArrayRef> + 'a> {
83        Box::new(
84            self.as_ref().slots()[ChunkedSlots::CHUNKS_OFFSET..]
85                .iter()
86                .map(|slot| slot.as_ref().vortex_expect("validated chunk slot")),
87        )
88    }
89
90    fn chunks(&self) -> Vec<ArrayRef> {
91        self.iter_chunks().cloned().collect()
92    }
93
94    fn non_empty_chunks<'a>(&'a self) -> Box<dyn Iterator<Item = &'a ArrayRef> + 'a> {
95        Box::new(self.iter_chunks().filter(|chunk| !chunk.is_empty()))
96    }
97
98    /// Returns the cached chunk boundary offsets.
99    fn chunk_offset_values(&self) -> &[usize] {
100        &self.chunk_offsets
101    }
102
103    fn find_chunk_idx(&self, index: usize) -> VortexResult<(usize, usize)> {
104        assert!(
105            index <= self.as_ref().len(),
106            "Index out of bounds of the array"
107        );
108        let chunk_offset_values = self.chunk_offset_values();
109        let index_chunk = chunk_offset_values
110            .search_sorted(&index, SearchSortedSide::Right)?
111            .to_ends_index(self.nchunks() + 1)
112            .saturating_sub(1);
113        let chunk_start = chunk_offset_values[index_chunk];
114        let index_in_chunk = index - chunk_start;
115        Ok((index_chunk, index_in_chunk))
116    }
117
118    fn array_iterator(&self) -> impl ArrayIterator + '_ {
119        ArrayIteratorAdapter::new(
120            self.as_ref().dtype().clone(),
121            self.iter_chunks().map(|chunk| Ok(chunk.clone())),
122        )
123    }
124
125    fn array_stream(&self) -> impl ArrayStream + '_ {
126        ArrayStreamAdapter::new(
127            self.as_ref().dtype().clone(),
128            stream::iter(self.iter_chunks().map(|chunk| Ok(chunk.clone()))),
129        )
130    }
131}
132impl<T: TypedArrayRef<Chunked>> ChunkedArrayExt for T {}
133
134impl ChunkedData {
135    pub(super) fn new(chunk_offsets: Vec<usize>) -> Self {
136        Self {
137            chunk_offsets,
138            next_builder_slot: ChunkedSlots::CHUNKS_OFFSET,
139        }
140    }
141
142    pub(super) fn make_chunk_offsets_array(chunk_offsets: &[usize]) -> ArrayRef {
143        let mut chunk_offsets_buf = BufferMut::<u64>::with_capacity(chunk_offsets.len());
144        for &offset in chunk_offsets {
145            let offset = u64::try_from(offset)
146                .vortex_expect("chunk offset must fit in u64 for serialization");
147            unsafe { chunk_offsets_buf.push_unchecked(offset) }
148        }
149        PrimitiveArray::new(chunk_offsets_buf.freeze(), Validity::NonNullable).into_array()
150    }
151
152    /// Validates the components that would be used to create a `ChunkedArray`.
153    ///
154    /// This function checks all the invariants required by `ChunkedArray::new_unchecked`.
155    pub fn validate(chunks: &[ArrayRef], dtype: &DType) -> VortexResult<()> {
156        for chunk in chunks {
157            if chunk.dtype() != dtype {
158                vortex_bail!(MismatchedTypes: dtype, chunk.dtype());
159            }
160        }
161
162        Ok(())
163    }
164}
165
166impl Array<Chunked> {
167    fn parts_from_chunks<const VALIDATE: bool>(
168        chunks: impl IntoIterator<Item = ArrayRef>,
169        dtype: DType,
170    ) -> VortexResult<ArrayParts<Chunked>> {
171        let chunks = chunks.into_iter();
172        let (lower, _) = chunks.size_hint();
173        let mut slots = ArraySlots::with_capacity(ChunkedSlots::CHUNKS_OFFSET + lower);
174        slots.push(None);
175        let mut chunk_offsets = Vec::with_capacity(lower + 1);
176        chunk_offsets.push(0);
177        let mut len = 0usize;
178
179        for chunk in chunks {
180            if VALIDATE && chunk.dtype() != &dtype {
181                vortex_bail!(MismatchedTypes: &dtype, chunk.dtype());
182            }
183            len += chunk.len();
184            chunk_offsets.push(len);
185            slots.push(Some(chunk));
186        }
187
188        slots[ChunkedSlots::CHUNK_OFFSETS] =
189            Some(ChunkedData::make_chunk_offsets_array(&chunk_offsets));
190        Ok(ArrayParts::new(
191            Chunked,
192            dtype,
193            len,
194            ChunkedData::new(chunk_offsets),
195            slots,
196        ))
197    }
198
199    pub(super) fn with_next_builder_slot(mut self, next_builder_slot: usize) -> Self {
200        if let Some(data) = self.data_mut() {
201            data.next_builder_slot = next_builder_slot;
202            return self;
203        }
204        // This is the slow path that will be hit at most once per execution since the second one
205        // *MUST* have execlusive access due to this copy.
206        let stats = self.statistics().to_owned();
207        let mut data = self.data().clone();
208        data.next_builder_slot = next_builder_slot;
209        // SAFETY: we only modified next_builder_slot which doesn't affect array invariants.
210        unsafe {
211            Array::from_parts_unchecked(ArrayParts::new(
212                Chunked,
213                self.dtype().clone(),
214                self.len(),
215                data,
216                self.slots().iter().cloned().collect::<ArraySlots>(),
217            ))
218        }
219        .with_stats_set(stats)
220    }
221
222    /// Constructs a new `ChunkedArray`.
223    pub fn try_new(chunks: impl IntoIterator<Item = ArrayRef>, dtype: DType) -> VortexResult<Self> {
224        Array::try_from_parts(Self::parts_from_chunks::<true>(chunks, dtype)?)
225    }
226
227    pub fn rechunk(
228        &self,
229        target_bytesize: u64,
230        target_rowsize: usize,
231        ctx: &mut ExecutionCtx,
232    ) -> VortexResult<Self> {
233        let mut new_chunks = Vec::new();
234        let mut chunks_to_combine = Vec::new();
235        let mut new_chunk_n_bytes = 0;
236        let mut new_chunk_n_elements = 0;
237        for chunk in self.iter_chunks() {
238            let n_bytes = chunk.nbytes();
239            let n_elements = chunk.len();
240
241            if (new_chunk_n_bytes + n_bytes > target_bytesize
242                || new_chunk_n_elements + n_elements > target_rowsize)
243                && !chunks_to_combine.is_empty()
244            {
245                let canonical = unsafe {
246                    Array::<Chunked>::new_unchecked(chunks_to_combine, self.dtype().clone())
247                }
248                .into_array()
249                .execute::<Canonical>(ctx)?
250                .into_array();
251                new_chunks.push(canonical);
252
253                new_chunk_n_bytes = 0;
254                new_chunk_n_elements = 0;
255                chunks_to_combine = Vec::new();
256            }
257
258            if n_bytes > target_bytesize || n_elements > target_rowsize {
259                new_chunks.push(chunk.clone());
260            } else {
261                new_chunk_n_bytes += n_bytes;
262                new_chunk_n_elements += n_elements;
263                chunks_to_combine.push(chunk.clone());
264            }
265        }
266
267        if !chunks_to_combine.is_empty() {
268            let canonical =
269                unsafe { Array::<Chunked>::new_unchecked(chunks_to_combine, self.dtype().clone()) }
270                    .into_array()
271                    .execute::<Canonical>(ctx)?
272                    .into_array();
273            new_chunks.push(canonical);
274        }
275
276        unsafe { Ok(Self::new_unchecked(new_chunks, self.dtype().clone())) }
277    }
278
279    /// Creates a new `ChunkedArray` without validation.
280    ///
281    /// # Safety
282    ///
283    /// All chunks must have exactly the same [`DType`] as the provided `dtype`.
284    pub unsafe fn new_unchecked(chunks: impl IntoIterator<Item = ArrayRef>, dtype: DType) -> Self {
285        let parts = Self::parts_from_chunks::<false>(chunks, dtype)
286            .vortex_expect("unchecked chunked construction cannot fail");
287        unsafe { Array::from_parts_unchecked(parts) }
288    }
289}
290
291impl FromIterator<ArrayRef> for Array<Chunked> {
292    fn from_iter<T: IntoIterator<Item = ArrayRef>>(iter: T) -> Self {
293        let chunks: Vec<ArrayRef> = iter.into_iter().collect();
294        let dtype = chunks
295            .first()
296            .map(|c| c.dtype().clone())
297            .vortex_expect("Cannot infer DType from an empty iterator");
298        Array::<Chunked>::try_new(chunks, dtype)
299            .vortex_expect("Failed to create chunked array from iterator")
300    }
301}
302
303#[cfg(test)]
304mod test {
305    use vortex_buffer::buffer;
306    use vortex_error::VortexResult;
307
308    use crate::IntoArray;
309    use crate::VortexSessionExecute;
310    use crate::array_session;
311    use crate::arrays::ChunkedArray;
312    use crate::arrays::PrimitiveArray;
313    use crate::arrays::chunked::ChunkedArrayExt;
314    use crate::assert_arrays_eq;
315    use crate::dtype::DType;
316    use crate::dtype::Nullability;
317    use crate::dtype::PType;
318    use crate::validity::Validity;
319
320    #[test]
321    fn test_rechunk_one_chunk() {
322        let mut ctx = array_session().create_execution_ctx();
323        let chunked = ChunkedArray::try_new(
324            vec![buffer![0u64].into_array()],
325            DType::Primitive(PType::U64, Nullability::NonNullable),
326        )
327        .unwrap();
328
329        let rechunked = chunked.rechunk(1 << 16, 1 << 16, &mut ctx).unwrap();
330
331        assert_arrays_eq!(chunked, rechunked, &mut ctx);
332    }
333
334    #[test]
335    fn test_rechunk_two_chunks() {
336        let mut ctx = array_session().create_execution_ctx();
337        let chunked = ChunkedArray::try_new(
338            vec![buffer![0u64].into_array(), buffer![5u64].into_array()],
339            DType::Primitive(PType::U64, Nullability::NonNullable),
340        )
341        .unwrap();
342
343        let rechunked = chunked.rechunk(1 << 16, 1 << 16, &mut ctx).unwrap();
344
345        assert_eq!(rechunked.nchunks(), 1);
346        assert_arrays_eq!(chunked, rechunked, &mut ctx);
347    }
348
349    #[test]
350    fn test_rechunk_tiny_target_chunks() {
351        let mut ctx = array_session().create_execution_ctx();
352        let chunked = ChunkedArray::try_new(
353            vec![
354                buffer![0u64, 1, 2, 3].into_array(),
355                buffer![4u64, 5].into_array(),
356            ],
357            DType::Primitive(PType::U64, Nullability::NonNullable),
358        )
359        .unwrap();
360
361        let rechunked = chunked.rechunk(1 << 16, 5, &mut ctx).unwrap();
362
363        assert_eq!(rechunked.nchunks(), 2);
364        assert!(rechunked.iter_chunks().all(|c| c.len() < 5));
365        assert_arrays_eq!(chunked, rechunked, &mut ctx);
366    }
367
368    #[test]
369    fn test_rechunk_with_too_big_chunk() {
370        let mut ctx = array_session().create_execution_ctx();
371        let chunked = ChunkedArray::try_new(
372            vec![
373                buffer![0u64, 1, 2].into_array(),
374                buffer![42_u64; 6].into_array(),
375                buffer![4u64, 5].into_array(),
376                buffer![6u64, 7].into_array(),
377                buffer![8u64, 9].into_array(),
378            ],
379            DType::Primitive(PType::U64, Nullability::NonNullable),
380        )
381        .unwrap();
382
383        let rechunked = chunked.rechunk(1 << 16, 5, &mut ctx).unwrap();
384        // greedy so should be: [0, 1, 2] [42, 42, 42, 42, 42, 42] [4, 5, 6, 7] [8, 9]
385
386        assert_eq!(rechunked.nchunks(), 4);
387        assert_arrays_eq!(chunked, rechunked, &mut ctx);
388    }
389
390    #[test]
391    fn test_empty_chunks_all_valid() -> VortexResult<()> {
392        let mut ctx = array_session().create_execution_ctx();
393        // Create chunks where some are empty but all non-empty chunks have all valid values
394        let chunks = vec![
395            PrimitiveArray::new(buffer![1u64, 2, 3], Validity::AllValid).into_array(),
396            PrimitiveArray::new(buffer![0u64; 0], Validity::AllValid).into_array(), // empty chunk
397            PrimitiveArray::new(buffer![4u64, 5], Validity::AllValid).into_array(),
398            PrimitiveArray::new(buffer![0u64; 0], Validity::AllValid).into_array(), // empty chunk
399        ];
400
401        let chunked =
402            ChunkedArray::try_new(chunks, DType::Primitive(PType::U64, Nullability::Nullable))?;
403
404        // Should be all_valid since all non-empty chunks are all_valid
405        assert!(chunked.all_valid(&mut ctx)?);
406        assert!(!chunked.into_array().all_invalid(&mut ctx)?);
407
408        Ok(())
409    }
410
411    #[test]
412    fn test_empty_chunks_all_invalid() -> VortexResult<()> {
413        let mut ctx = array_session().create_execution_ctx();
414        // Create chunks where some are empty but all non-empty chunks have all invalid values
415        let chunks = vec![
416            PrimitiveArray::new(buffer![1u64, 2], Validity::AllInvalid).into_array(),
417            PrimitiveArray::new(buffer![0u64; 0], Validity::AllInvalid).into_array(), /* empty chunk */
418            PrimitiveArray::new(buffer![3u64, 4, 5], Validity::AllInvalid).into_array(),
419            PrimitiveArray::new(buffer![0u64; 0], Validity::AllInvalid).into_array(), /* empty chunk */
420        ];
421
422        let chunked =
423            ChunkedArray::try_new(chunks, DType::Primitive(PType::U64, Nullability::Nullable))?;
424
425        // Should be all_invalid since all non-empty chunks are all_invalid
426        assert!(!chunked.all_valid(&mut ctx)?);
427        assert!(chunked.into_array().all_invalid(&mut ctx)?);
428
429        Ok(())
430    }
431
432    #[test]
433    fn test_empty_chunks_mixed_validity() -> VortexResult<()> {
434        let mut ctx = array_session().create_execution_ctx();
435        // Create chunks with mixed validity including empty chunks
436        let chunks = vec![
437            PrimitiveArray::new(buffer![1u64, 2], Validity::AllValid).into_array(),
438            PrimitiveArray::new(buffer![0u64; 0], Validity::AllValid).into_array(), // empty chunk
439            PrimitiveArray::new(buffer![3u64, 4], Validity::AllInvalid).into_array(),
440            PrimitiveArray::new(buffer![0u64; 0], Validity::AllInvalid).into_array(), /* empty chunk */
441        ];
442
443        let chunked =
444            ChunkedArray::try_new(chunks, DType::Primitive(PType::U64, Nullability::Nullable))?;
445
446        // Should be neither all_valid nor all_invalid
447        assert!(!chunked.all_valid(&mut ctx)?);
448        assert!(!chunked.into_array().all_invalid(&mut ctx)?);
449
450        Ok(())
451    }
452}