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