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