22// SPDX-FileCopyrightText: Copyright the Vortex contributors
33
44use itertools:: Itertools as _;
5- use vortex_buffer:: Buffer ;
6- use vortex_error:: VortexExpect ;
75use vortex_error:: VortexResult ;
86use vortex_error:: vortex_ensure;
97use vortex_error:: vortex_err;
@@ -15,23 +13,11 @@ use crate::IntoArray;
1513use crate :: array:: ArrayView ;
1614use crate :: arrays:: Chunked ;
1715use crate :: arrays:: ChunkedArray ;
18- use crate :: arrays:: FixedSizeListArray ;
19- use crate :: arrays:: ListViewArray ;
20- use crate :: arrays:: PrimitiveArray ;
21- use crate :: arrays:: StructArray ;
2216use crate :: arrays:: VariantArray ;
2317use crate :: arrays:: chunked:: ChunkedArrayExt ;
24- use crate :: arrays:: fixed_size_list:: FixedSizeListArraySlotsExt ;
25- use crate :: arrays:: listview:: ListViewArraySlotsExt ;
26- use crate :: arrays:: listview:: ListViewRebuildMode ;
2718use crate :: arrays:: variant:: VariantArraySlotsExt ;
2819use crate :: builders:: builder_with_capacity_in;
29- use crate :: builtins:: ArrayBuiltins ;
3020use crate :: dtype:: DType ;
31- use crate :: dtype:: Nullability ;
32- use crate :: dtype:: PType ;
33- use crate :: memory:: HostAllocatorExt ;
34- use crate :: validity:: Validity ;
3521
3622pub ( super ) fn _canonicalize (
3723 array : ArrayView < ' _ , Chunked > ,
@@ -48,28 +34,11 @@ pub(super) fn _canonicalize(
4834 return array. chunk ( 0 ) . clone ( ) . execute :: < Canonical > ( ctx) ;
4935 }
5036
51- let owned_chunks: Vec < ArrayRef > = array. iter_chunks ( ) . cloned ( ) . collect ( ) ;
5237 Ok ( match array. dtype ( ) {
53- DType :: Struct ( .. ) => {
54- let struct_array = pack_struct_chunks ( owned_chunks , ctx ) ? ;
55- Canonical :: Struct ( struct_array )
38+ DType :: Variant ( _ ) => {
39+ let owned_chunks : Vec < ArrayRef > = array . iter_chunks ( ) . cloned ( ) . collect ( ) ;
40+ Canonical :: Variant ( pack_variant_chunks ( owned_chunks , ctx ) ? )
5641 }
57- DType :: List ( elem_dtype, _) => Canonical :: List ( swizzle_list_chunks (
58- & owned_chunks,
59- array. array ( ) . validity ( ) ?,
60- elem_dtype,
61- ctx,
62- ) ?) ,
63- DType :: FixedSizeList ( elem_dtype, list_size, _) => {
64- Canonical :: FixedSizeList ( swizzle_fixed_size_list_chunks (
65- & owned_chunks,
66- array. array ( ) . validity ( ) ?,
67- elem_dtype,
68- * list_size,
69- ctx,
70- ) ?)
71- }
72- DType :: Variant ( _) => Canonical :: Variant ( pack_variant_chunks ( owned_chunks, ctx) ?) ,
7342 _ => {
7443 let mut builder = builder_with_capacity_in ( ctx. allocator ( ) , array. dtype ( ) , array. len ( ) ) ;
7544 array. array ( ) . append_to_builder ( builder. as_mut ( ) , ctx) ?;
@@ -78,17 +47,6 @@ pub(super) fn _canonicalize(
7847 } )
7948}
8049
81- /// Packs many [`StructArray`]s to instead be a single [`StructArray`], where the [`DynArrayData`](crate::array::DynArrayData) for each
82- /// field is a [`ChunkedArray`].
83- ///
84- /// The caller guarantees there are at least 2 chunks.
85- fn pack_struct_chunks ( chunks : Vec < ArrayRef > , ctx : & mut ExecutionCtx ) -> VortexResult < StructArray > {
86- chunks
87- . into_iter ( )
88- . map ( |c| c. execute :: < StructArray > ( ctx) )
89- . process_results ( |iter| StructArray :: try_concat ( iter) ) ?
90- }
91-
9250/// Packs many [`VariantArray`]s into one [`VariantArray`] with chunked children.
9351///
9452/// The caller guarantees there are at least 2 chunks.
@@ -147,151 +105,14 @@ fn pack_variant_chunks(
147105 VariantArray :: try_new ( core_storage, shredded)
148106}
149107
150- /// Packs [`ListViewArray`]s together into a chunked `ListViewArray`.
151- ///
152- /// We use the existing arrays (chunks) to form a chunked array of `elements` (the child array).
153- ///
154- /// The caller guarantees there are at least 2 chunks.
155- fn swizzle_list_chunks (
156- chunks : & [ ArrayRef ] ,
157- validity : Validity ,
158- elem_dtype : & DType ,
159- ctx : & mut ExecutionCtx ,
160- ) -> VortexResult < ListViewArray > {
161- let len: usize = chunks. iter ( ) . map ( |c| c. len ( ) ) . sum ( ) ;
162-
163- assert_eq ! (
164- chunks[ 0 ]
165- . dtype( )
166- . as_list_element_opt( )
167- . vortex_expect( "DType was somehow not a list" )
168- . as_ref( ) ,
169- elem_dtype
170- ) ;
171-
172- // Since each list array in `chunks` has offsets local to each array, we can reuse the existing
173- // array's child `elements` as the chunks and recompute offsets.
174-
175- let mut list_elements_chunks = Vec :: with_capacity ( chunks. len ( ) ) ;
176- let mut num_elements = 0 ;
177-
178- // TODO(connor)[ListView]: We could potentially choose a smaller type here, but that would make
179- // this much more complicated.
180- // We (somewhat arbitrarily) choose `u64` for our offsets and sizes here. These can always be
181- // narrowed later by the compressor.
182- let allocator = ctx. allocator ( ) ;
183- let mut offsets = allocator. allocate_typed :: < u64 > ( len) ?;
184- let mut sizes = allocator. allocate_typed :: < u64 > ( len) ?;
185- let offsets_out: & mut [ u64 ] = offsets. as_mut_slice_typed :: < u64 > ( ) ?;
186- let sizes_slice_out: & mut [ u64 ] = sizes. as_mut_slice_typed :: < u64 > ( ) ?;
187- let mut next_list = 0usize ;
188-
189- for chunk in chunks {
190- let chunk_array = chunk. clone ( ) . execute :: < ListViewArray > ( ctx) ?;
191- // By rebuilding as zero-copy to `List` and trimming all elements (to prevent gaps), we make
192- // the final output `ListView` also zero-copyable to `List`.
193- let chunk_array = chunk_array. rebuild ( ListViewRebuildMode :: MakeExact , ctx) ?;
194-
195- // Add the `elements` of the current array as a new chunk.
196- list_elements_chunks. push ( chunk_array. elements ( ) . clone ( ) ) ;
197-
198- // Cast offsets and sizes to `u64`.
199- let offsets_arr = chunk_array
200- . offsets ( )
201- . clone ( )
202- . cast ( DType :: Primitive ( PType :: U64 , Nullability :: NonNullable ) )
203- . vortex_expect ( "Must be able to fit array offsets in u64" )
204- . execute :: < PrimitiveArray > ( ctx) ?;
205-
206- let sizes_arr = chunk_array
207- . sizes ( )
208- . clone ( )
209- . cast ( DType :: Primitive ( PType :: U64 , Nullability :: NonNullable ) )
210- . vortex_expect ( "Must be able to fit array offsets in u64" )
211- . execute :: < PrimitiveArray > ( ctx) ?;
212-
213- let offsets_slice = offsets_arr. as_slice :: < u64 > ( ) ;
214- let sizes_slice = sizes_arr. as_slice :: < u64 > ( ) ;
215-
216- // Append offsets and sizes, adjusting offsets to point into the combined array.
217- for ( & offset, & size) in offsets_slice. iter ( ) . zip ( sizes_slice. iter ( ) ) {
218- offsets_out[ next_list] = offset + num_elements;
219- sizes_slice_out[ next_list] = size;
220- next_list += 1 ;
221- }
222-
223- num_elements += chunk_array. elements ( ) . len ( ) as u64 ;
224- }
225- debug_assert_eq ! ( next_list, len) ;
226-
227- // SAFETY: elements are sliced from valid `ListViewArray`s (from `to_listview()`).
228- let chunked_elements =
229- unsafe { ChunkedArray :: new_unchecked ( list_elements_chunks, elem_dtype. clone ( ) ) }
230- . into_array ( ) ;
231-
232- let offsets = PrimitiveArray :: new (
233- Buffer :: < u64 > :: from_byte_buffer ( offsets. freeze ( ) ) ,
234- Validity :: NonNullable ,
235- )
236- . into_array ( ) ;
237- let sizes = PrimitiveArray :: new (
238- Buffer :: < u64 > :: from_byte_buffer ( sizes. freeze ( ) ) ,
239- Validity :: NonNullable ,
240- )
241- . into_array ( ) ;
242-
243- // SAFETY:
244- // - `offsets` and `sizes` are non-nullable u64 arrays of the same length
245- // - Each `offset[i] + size[i]` list view is within bounds of elements array because it came
246- // from valid chunks
247- // - Validity came from the outer chunked array so it must have the same length
248- // - Since we made sure that all chunks were zero-copyable to a list above, we know that the
249- // final concatenated output is also zero-copyable to a list.
250- Ok ( unsafe {
251- ListViewArray :: new_unchecked ( chunked_elements, offsets, sizes, validity)
252- . with_zero_copy_to_list ( true )
253- } )
254- }
255-
256- /// Packs [`FixedSizeListArray`]s together into a single [`FixedSizeListArray`] whose `elements`
257- /// child is a [`ChunkedArray`].
258- ///
259- /// Every chunk shares the same `list_size`, and each chunk's `elements` child is exactly
260- /// `list_size * chunk.len()` long and starts at the first list, so we can reuse the chunks'
261- /// `elements` children directly as the chunks of a combined `elements` array without copying.
262- ///
263- /// The caller guarantees there are at least 2 chunks.
264- fn swizzle_fixed_size_list_chunks (
265- chunks : & [ ArrayRef ] ,
266- validity : Validity ,
267- elem_dtype : & DType ,
268- list_size : u32 ,
269- ctx : & mut ExecutionCtx ,
270- ) -> VortexResult < FixedSizeListArray > {
271- let len: usize = chunks. iter ( ) . map ( |c| c. len ( ) ) . sum ( ) ;
272-
273- let mut element_chunks = Vec :: with_capacity ( chunks. len ( ) ) ;
274- for chunk in chunks {
275- let chunk_array = chunk. clone ( ) . execute :: < FixedSizeListArray > ( ctx) ?;
276- // A canonical `FixedSizeListArray` keeps its `elements` child trimmed to exactly
277- // `list_size * chunk.len()` starting at the first list, so the children concatenate
278- // cleanly into the combined `elements` array.
279- element_chunks. push ( chunk_array. elements ( ) . clone ( ) ) ;
280- }
281-
282- let chunked_elements = ChunkedArray :: try_new ( element_chunks, elem_dtype. clone ( ) ) ?. into_array ( ) ;
283-
284- FixedSizeListArray :: try_new ( chunked_elements, list_size, validity, len)
285- }
286-
287108#[ cfg( test) ]
288109mod tests {
289110 use std:: sync:: Arc ;
290111 use std:: sync:: LazyLock ;
291- use std:: sync:: atomic:: AtomicUsize ;
292- use std:: sync:: atomic:: Ordering ;
293112
113+ use rstest:: rstest;
294114 use vortex_buffer:: buffer;
115+ use vortex_error:: VortexExpect ;
295116 use vortex_error:: VortexResult ;
296117 use vortex_error:: vortex_bail;
297118 use vortex_error:: vortex_err;
@@ -301,15 +122,22 @@ mod tests {
301122 use crate :: Canonical ;
302123 use crate :: IntoArray ;
303124 use crate :: VortexSessionExecute ;
125+ use crate :: arrays:: Chunked ;
304126 use crate :: arrays:: ChunkedArray ;
305127 use crate :: arrays:: ConstantArray ;
128+ use crate :: arrays:: FixedSizeList ;
306129 use crate :: arrays:: FixedSizeListArray ;
307130 use crate :: arrays:: ListArray ;
131+ use crate :: arrays:: ListView ;
308132 use crate :: arrays:: ListViewArray ;
309133 use crate :: arrays:: PrimitiveArray ;
134+ use crate :: arrays:: Struct ;
310135 use crate :: arrays:: StructArray ;
311136 use crate :: arrays:: VarBinViewArray ;
312137 use crate :: arrays:: VariantArray ;
138+ use crate :: arrays:: chunked:: ChunkedArrayExt ;
139+ use crate :: arrays:: fixed_size_list:: FixedSizeListArraySlotsExt ;
140+ use crate :: arrays:: listview:: ListViewArraySlotsExt ;
313141 use crate :: arrays:: struct_:: StructArrayExt ;
314142 use crate :: arrays:: variant:: VariantArraySlotsExt ;
315143 use crate :: assert_arrays_eq;
@@ -318,32 +146,12 @@ mod tests {
318146 use crate :: dtype:: DType :: Variant as VariantDType ;
319147 use crate :: dtype:: Nullability :: NonNullable ;
320148 use crate :: dtype:: PType :: I32 ;
321- use crate :: memory:: DefaultHostAllocator ;
322- use crate :: memory:: HostAllocator ;
323- use crate :: memory:: MemorySessionExt ;
324- use crate :: memory:: WritableHostBuffer ;
325149 use crate :: scalar:: Scalar ;
326150 use crate :: validity:: Validity ;
327151
328152 /// A shared session for these chunked-array tests, used to create execution contexts.
329153 static SESSION : LazyLock < VortexSession > = LazyLock :: new ( crate :: array_session) ;
330154
331- #[ derive( Debug ) ]
332- struct CountingAllocator {
333- allocations : Arc < AtomicUsize > ,
334- }
335-
336- impl HostAllocator for CountingAllocator {
337- fn allocate (
338- & self ,
339- len : usize ,
340- alignment : vortex_buffer:: Alignment ,
341- ) -> VortexResult < WritableHostBuffer > {
342- self . allocations . fetch_add ( 1 , Ordering :: Relaxed ) ;
343- DefaultHostAllocator . allocate ( len, alignment)
344- }
345- }
346-
347155 fn variant_scalar ( value : i32 ) -> Scalar {
348156 Scalar :: variant ( Scalar :: primitive ( value, NonNullable ) )
349157 }
@@ -660,38 +468,53 @@ mod tests {
660468 Ok ( ( ) )
661469 }
662470
663- #[ test]
664- fn list_canonicalize_uses_memory_session_allocator ( ) {
665- let allocations = Arc :: new ( AtomicUsize :: new ( 0 ) ) ;
666- let session = crate :: array_session ( ) . with_allocator ( Arc :: new ( CountingAllocator {
667- allocations : Arc :: clone ( & allocations) ,
668- } ) ) ;
669- let mut ctx = session. create_execution_ctx ( ) ;
670-
671- let l1 = ListArray :: try_new (
672- buffer ! [ 1 , 2 , 3 , 4 ] . into_array ( ) ,
673- buffer ! [ 0 , 3 ] . into_array ( ) ,
674- Validity :: NonNullable ,
675- )
676- . unwrap ( ) ;
677- let l2 = ListArray :: try_new (
678- buffer ! [ 5 , 6 ] . into_array ( ) ,
471+ /// Canonicalizing a `ChunkedArray` reuses each chunk's children instead of concatenating
472+ /// them: a nested builder keeps a child chunked on exactly the boundaries it was appended on,
473+ /// however short the appended chunk is.
474+ #[ rstest]
475+ #[ case:: struct_(
476+ StructArray :: try_from_iter( [ ( "a" , buffer![ 1i32 , 2 ] ) ] )
477+ . vortex_expect( "struct array" )
478+ . into_array( ) ,
479+ |array: & ArrayRef | array. as_:: <Struct >( ) . unmasked_field( 0 ) . clone( )
480+ ) ]
481+ #[ case:: fixed_size_list(
482+ FixedSizeListArray :: new( buffer![ 1i32 , 2 ] . into_array( ) , 2 , Validity :: NonNullable , 1 )
483+ . into_array( ) ,
484+ |array: & ArrayRef | array. as_:: <FixedSizeList >( ) . elements( ) . clone( )
485+ ) ]
486+ #[ case:: list(
487+ ListArray :: try_new(
488+ buffer![ 1i32 , 2 ] . into_array( ) ,
679489 buffer![ 0 , 2 ] . into_array( ) ,
680490 Validity :: NonNullable ,
681491 )
682- . unwrap ( ) ;
492+ . vortex_expect( "list array" )
493+ . into_array( ) ,
494+ |array: & ArrayRef | array. as_:: <ListView >( ) . elements( ) . clone( )
495+ ) ]
496+ fn canonicalize_reuses_chunk_children (
497+ #[ case] chunk : ArrayRef ,
498+ #[ case] child_of : fn ( & ArrayRef ) -> ArrayRef ,
499+ ) -> VortexResult < ( ) > {
500+ let mut ctx = SESSION . create_execution_ctx ( ) ;
683501
684- let chunked_list = ChunkedArray :: try_new (
685- vec ! [ l1. into_array( ) , l2. into_array( ) ] ,
686- List ( Arc :: new ( Primitive ( I32 , NonNullable ) ) , NonNullable ) ,
687- )
688- . unwrap ( )
689- . into_array ( ) ;
502+ let chunked =
503+ ChunkedArray :: try_new ( vec ! [ chunk. clone( ) , chunk. clone( ) ] , chunk. dtype ( ) . clone ( ) ) ?
504+ . into_array ( ) ;
505+ let canonical = chunked. execute :: < Canonical > ( & mut ctx) ?. into_array ( ) ;
690506
691- drop ( chunked_list. execute :: < Canonical > ( & mut ctx) . unwrap ( ) ) ;
692- assert ! (
693- allocations. load( Ordering :: Relaxed ) >= 2 ,
694- "expected offset+size allocations through MemorySession"
507+ let child = child_of ( & canonical) ;
508+ assert_eq ! (
509+ child. as_:: <Chunked >( ) . nchunks( ) ,
510+ 2 ,
511+ "each chunk's child should have become a chunk of the combined child" ,
695512 ) ;
513+
514+ let expected =
515+ ChunkedArray :: try_new ( vec ! [ chunk. clone( ) , chunk] , canonical. dtype ( ) . clone ( ) ) ?;
516+ assert_arrays_eq ! ( & canonical, & expected, & mut ctx) ;
517+
518+ Ok ( ( ) )
696519 }
697520}
0 commit comments