@@ -7,17 +7,22 @@ use std::sync::Arc;
77
88use futures:: FutureExt ;
99use futures:: try_join;
10- use vortex_array:: Array ;
1110use vortex_array:: ArrayRef ;
1211use vortex_array:: IntoArray ;
1312use vortex_array:: MaskFuture ;
14- use vortex_array:: arrays:: List ;
13+ use vortex_array:: VortexSessionExecute ;
14+ use vortex_array:: arrays:: ConstantArray ;
15+ use vortex_array:: arrays:: ListArray ;
16+ use vortex_array:: builtins:: ArrayBuiltins ;
1517use vortex_array:: dtype:: DType ;
1618use vortex_array:: dtype:: FieldMask ;
1719use vortex_array:: dtype:: Nullability ;
1820use vortex_array:: expr:: Expression ;
21+ use vortex_array:: expr:: is_root;
1922use vortex_array:: expr:: root;
23+ use vortex_array:: scalar_fn:: fns:: operators:: Operator ;
2024use vortex_array:: validity:: Validity ;
25+ use vortex_error:: VortexExpect ;
2126use vortex_error:: VortexResult ;
2227use vortex_mask:: Mask ;
2328use vortex_session:: VortexSession ;
@@ -30,14 +35,10 @@ use crate::layouts::list::ListLayout;
3035use crate :: segments:: SegmentSource ;
3136
3237/// Reader for [`ListLayout`].
33- ///
34- /// Reads the underlying `elements`, `offsets`, and (optional) `validity` children in parallel
35- /// and reassembles them into a [`ListArray`](vortex_array::arrays::ListArray) before applying
36- /// the requested projection.
3738pub struct ListReader {
3839 layout : ListLayout ,
3940 name : Arc < str > ,
40- _session : VortexSession ,
41+ session : VortexSession ,
4142 elements : LayoutReaderRef ,
4243 offsets : LayoutReaderRef ,
4344 validity : Option < LayoutReaderRef > ,
@@ -74,12 +75,54 @@ impl ListReader {
7475 Ok ( Self {
7576 layout,
7677 name,
77- _session : session,
78+ session,
7879 elements,
7980 offsets,
8081 validity,
8182 } )
8283 }
84+
85+ fn fetch_offsets ( & self , row_range : & Range < u64 > ) -> VortexResult < ArrayFuture > {
86+ // The offsets child has an extra entry, so reading `row_range` maps to offsets in
87+ // `[row_range.start..row_range.end + 1)`.
88+ let offsets_range = row_range. start ..( row_range. end + 1 ) ;
89+ let offsets_count = usize:: try_from ( offsets_range. end - offsets_range. start ) ?;
90+
91+ self . offsets . projection_evaluation (
92+ & offsets_range,
93+ & root ( ) ,
94+ MaskFuture :: new_true ( offsets_count) ,
95+ )
96+ }
97+ }
98+
99+ /// Read `offsets[0]` and `offsets[-1]` and return the elements-buffer range they describe.
100+ fn calculate_elements_range (
101+ offsets : & ArrayRef ,
102+ session : & VortexSession ,
103+ ) -> VortexResult < Range < u64 > > {
104+ if offsets. is_empty ( ) {
105+ return Ok ( 0 ..0 ) ;
106+ }
107+ let mut exec_ctx = session. create_execution_ctx ( ) ;
108+ let start = offsets
109+ . execute_scalar ( 0 , & mut exec_ctx) ?
110+ . as_primitive ( )
111+ . as_ :: < u64 > ( )
112+ . vortex_expect ( "offset value fits in u64" ) ;
113+ let end = offsets
114+ . execute_scalar ( offsets. len ( ) - 1 , & mut exec_ctx) ?
115+ . as_primitive ( )
116+ . as_ :: < u64 > ( )
117+ . vortex_expect ( "offset value fits in u64" ) ;
118+ Ok ( start..end)
119+ }
120+
121+ /// Subtract `first` from every offset so the resulting offsets index into a sliced
122+ /// `elements[first..]` buffer starting at zero.
123+ fn rebase_offsets ( offsets : ArrayRef , first : u64 ) -> VortexResult < ArrayRef > {
124+ let constant = ConstantArray :: new ( first, offsets. len ( ) ) . into_array ( ) ;
125+ offsets. binary ( constant, Operator :: Sub )
83126}
84127
85128impl LayoutReader for ListReader {
@@ -105,10 +148,12 @@ impl LayoutReader for ListReader {
105148 split_range : & SplitRange ,
106149 splits : & mut BTreeSet < u64 > ,
107150 ) -> VortexResult < ( ) > {
108- // Offsets has one more row than the list itself but shares the list's chunking
109- // structure, so it's the appropriate child to drive scan splits.
110151 self . offsets
111- . register_splits ( field_mask, split_range, splits)
152+ . register_splits ( field_mask, split_range, splits) ?;
153+ if let Some ( validity) = & self . validity {
154+ validity. register_splits ( field_mask, split_range, splits) ?;
155+ }
156+ Ok ( ( ) )
112157 }
113158
114159 fn pruning_evaluation (
@@ -135,47 +180,55 @@ impl LayoutReader for ListReader {
135180 expr : & Expression ,
136181 mask : MaskFuture ,
137182 ) -> VortexResult < ArrayFuture > {
138- let elements_len = self . layout . elements ( ) . row_count ( ) ;
139- let elements_range = 0 ..elements_len;
140- let elements_mask = MaskFuture :: new_true ( usize:: try_from ( elements_len) ?) ;
141-
142- let row_count: usize = usize:: try_from ( row_range. end - row_range. start ) ?;
143- // The offsets child has n+1 entries; reading row_range maps to offsets in
144- // [row_range.start..row_range.end + 1).
145- let offsets_range = row_range. start ..row_range. end + 1 ;
146- let offsets_count = usize:: try_from ( offsets_range. end - offsets_range. start ) ?;
147-
148- let elements_fut =
149- self . elements
150- . projection_evaluation ( & elements_range, & root ( ) , elements_mask) ?;
151- let offsets_fut = self . offsets . projection_evaluation (
152- & offsets_range,
153- & root ( ) ,
154- MaskFuture :: new_true ( offsets_count) ,
155- ) ?;
183+ let offsets_fut = self . fetch_offsets ( row_range) ?;
184+ // Validity shares the list's row space, so the caller's mask is the right shape to
185+ // push down. Children get to fold it into their reads however they like.
156186 let validity_fut = self
157187 . validity
158188 . as_ref ( )
159- . map ( |v| v. projection_evaluation ( row_range, & root ( ) , MaskFuture :: new_true ( row_count ) ) )
189+ . map ( |v| v. projection_evaluation ( row_range, & root ( ) , mask . clone ( ) ) )
160190 . transpose ( ) ?;
161191
192+ let elements_reader = self . elements . clone ( ) ;
193+ let session = self . session . clone ( ) ;
162194 let nullability = self . layout . dtype ( ) . nullability ( ) ;
163195 let expr = expr. clone ( ) ;
164196
165197 Ok ( async move {
166- let ( elements, offsets) = try_join ! ( elements_fut, offsets_fut) ?;
167- let validity = match validity_fut {
168- Some ( fut) => Validity :: Array ( fut. await ?) ,
198+ // Fetch offsets and validity in parallel. Elements waits until we know
199+ // exactly which slice of the elements buffer it actually needs.
200+ let ( offsets, validity_array) = try_join ! ( offsets_fut, async move {
201+ match validity_fut {
202+ Some ( fut) => fut. await . map( Some ) ,
203+ None => Ok ( None ) ,
204+ }
205+ } , ) ?;
206+
207+ // Bound the elements read using offsets[0] and offsets[-1]
208+ let elements_range = calculate_elements_range ( & offsets, & session) ?;
209+
210+ // Rebase the offsets so they start at zero
211+ let rebased_offsets = rebase_offsets ( offsets, elements_range. start ) ?;
212+
213+ // Fetch only the elements we actually need.
214+ let elements_len = elements_range. end - elements_range. start ;
215+ let elements = elements_reader
216+ . projection_evaluation (
217+ & elements_range,
218+ & root ( ) ,
219+ MaskFuture :: new_true ( usize:: try_from ( elements_len) ?) ,
220+ ) ?
221+ . await ?;
222+
223+ let validity = match validity_array {
224+ Some ( arr) => Validity :: Array ( arr) ,
169225 None => match nullability {
170226 Nullability :: Nullable => Validity :: AllValid ,
171227 Nullability :: NonNullable => Validity :: NonNullable ,
172228 } ,
173229 } ;
174230
175- // SAFETY: the layout was validated at write time, so offsets are monotonic,
176- // non-nullable, integer, of length n+1.
177- let array: ArrayRef =
178- unsafe { Array :: < List > :: new_unchecked ( elements, offsets, validity) } . into_array ( ) ;
231+ let array = ListArray :: try_new ( elements, rebased_offsets, validity) ?. into_array ( ) ;
179232
180233 let mask = mask. await ?;
181234 let array = if !mask. all_true ( ) {
@@ -189,3 +242,130 @@ impl LayoutReader for ListReader {
189242 . boxed ( ) )
190243 }
191244}
245+
246+ #[ cfg( test) ]
247+ mod tests {
248+ use std:: collections:: BTreeSet ;
249+ use std:: ops:: Range ;
250+
251+ use vortex_array:: ArrayContext ;
252+ use vortex_array:: arrays:: BoolArray ;
253+ use vortex_array:: arrays:: ChunkedArray ;
254+ use vortex_array:: arrays:: ListArray ;
255+ use vortex_array:: dtype:: FieldMask ;
256+ use vortex_buffer:: buffer;
257+
258+ use super :: * ;
259+ use crate :: LayoutRef ;
260+ use crate :: LayoutStrategy ;
261+ use crate :: layouts:: chunked:: writer:: ChunkedLayoutStrategy ;
262+ use crate :: layouts:: flat:: writer:: FlatLayoutStrategy ;
263+ use crate :: layouts:: list:: writer:: ListLayoutStrategy ;
264+ use crate :: scan:: split_by:: SplitBy ;
265+ use crate :: segments:: SegmentSource ;
266+ use crate :: segments:: TestSegments ;
267+ use crate :: sequence:: SequenceId ;
268+ use crate :: sequence:: SequentialArrayStreamExt ;
269+ use crate :: test:: SESSION ;
270+
271+ fn flat_list_strategy ( ) -> ListLayoutStrategy {
272+ let flat: Arc < dyn LayoutStrategy > = Arc :: new ( FlatLayoutStrategy :: default ( ) ) ;
273+ ListLayoutStrategy :: new ( Arc :: clone ( & flat) , Arc :: clone ( & flat) , Arc :: clone ( & flat) )
274+ }
275+
276+ async fn write_layout < S : LayoutStrategy > (
277+ strategy : & S ,
278+ array : ArrayRef ,
279+ ) -> VortexResult < ( Arc < dyn SegmentSource > , LayoutRef ) > {
280+ let segments = Arc :: new ( TestSegments :: default ( ) ) ;
281+ let segments_ref: Arc < dyn SegmentSource > = Arc :: < TestSegments > :: clone ( & segments) ;
282+ let ( ptr, eof) = SequenceId :: root ( ) . split ( ) ;
283+ let stream = array. to_array_stream ( ) . sequenced ( ptr) ;
284+ let layout = strategy
285+ . write_stream ( ArrayContext :: empty ( ) , segments, stream, eof, & SESSION )
286+ . await ?;
287+ Ok ( ( segments_ref, layout) )
288+ }
289+
290+ fn collect_splits ( reader : & dyn LayoutReader , row_range : Range < u64 > ) -> BTreeSet < u64 > {
291+ SplitBy :: Layout
292+ . splits ( reader, & row_range, & [ FieldMask :: All ] )
293+ . unwrap ( )
294+ }
295+
296+ #[ tokio:: test]
297+ async fn splits_non_nullable_flat ( ) -> VortexResult < ( ) > {
298+ // 5 lists in one flat ListLayout. Offsets is flat (single segment), no validity.
299+ // Only the row_range endpoints should appear in the split set.
300+ let list = ListArray :: try_new (
301+ buffer ! [ 1i32 , 2 , 3 , 4 , 5 , 6 , 7 ] . into_array ( ) ,
302+ buffer ! [ 0u32 , 2 , 3 , 3 , 6 , 7 ] . into_array ( ) ,
303+ Validity :: NonNullable ,
304+ ) ?
305+ . into_array ( ) ;
306+
307+ let ( segments, layout) = write_layout ( & flat_list_strategy ( ) , list) . await ?;
308+ let reader = layout. new_reader ( "" . into ( ) , segments, & SESSION ) ?;
309+
310+ assert_eq ! (
311+ collect_splits( reader. as_ref( ) , 0 ..5 ) ,
312+ BTreeSet :: from( [ 0 , 5 ] )
313+ ) ;
314+ assert_eq ! (
315+ collect_splits( reader. as_ref( ) , 1 ..4 ) ,
316+ BTreeSet :: from( [ 1 , 4 ] )
317+ ) ;
318+ Ok ( ( ) )
319+ }
320+
321+ #[ tokio:: test]
322+ async fn splits_nullable_flat_dedups_validity_and_offsets ( ) -> VortexResult < ( ) > {
323+ // Nullable list with flat validity. Offsets and validity both end at list-row 3;
324+ // BTreeSet deduplicates, so the split set still has only the endpoints.
325+ let list = ListArray :: try_new (
326+ buffer ! [ 10i32 , 20 , 30 , 40 , 50 ] . into_array ( ) ,
327+ buffer ! [ 0u32 , 2 , 3 , 5 ] . into_array ( ) ,
328+ Validity :: Array ( BoolArray :: from_iter ( [ true , false , true ] ) . into_array ( ) ) ,
329+ ) ?
330+ . into_array ( ) ;
331+
332+ let ( segments, layout) = write_layout ( & flat_list_strategy ( ) , list) . await ?;
333+ let reader = layout. new_reader ( "" . into ( ) , segments, & SESSION ) ?;
334+
335+ assert_eq ! (
336+ collect_splits( reader. as_ref( ) , 0 ..3 ) ,
337+ BTreeSet :: from( [ 0 , 3 ] )
338+ ) ;
339+ Ok ( ( ) )
340+ }
341+
342+ #[ tokio:: test]
343+ async fn splits_chunked_wrap_exposes_chunk_boundaries ( ) -> VortexResult < ( ) > {
344+ // Two list chunks under a ChunkedLayoutStrategy. The chunk boundary at list-row 2
345+ // should appear as a split.
346+ let chunk0 = ListArray :: try_new (
347+ buffer ! [ 1i32 , 2 , 3 ] . into_array ( ) ,
348+ buffer ! [ 0u32 , 2 , 3 ] . into_array ( ) ,
349+ Validity :: NonNullable ,
350+ ) ?
351+ . into_array ( ) ;
352+ let chunk1 = ListArray :: try_new (
353+ buffer ! [ 4i32 , 5 , 6 , 7 ] . into_array ( ) ,
354+ buffer ! [ 0u32 , 1 , 4 ] . into_array ( ) ,
355+ Validity :: NonNullable ,
356+ ) ?
357+ . into_array ( ) ;
358+ let dtype = chunk0. dtype ( ) . clone ( ) ;
359+ let chunked = ChunkedArray :: try_new ( vec ! [ chunk0, chunk1] , dtype) ?. into_array ( ) ;
360+
361+ let strategy = ChunkedLayoutStrategy :: new ( flat_list_strategy ( ) ) ;
362+ let ( segments, layout) = write_layout ( & strategy, chunked) . await ?;
363+ let reader = layout. new_reader ( "" . into ( ) , segments, & SESSION ) ?;
364+
365+ assert_eq ! (
366+ collect_splits( reader. as_ref( ) , 0 ..4 ) ,
367+ BTreeSet :: from( [ 0 , 2 , 4 ] )
368+ ) ;
369+ Ok ( ( ) )
370+ }
371+ }
0 commit comments