@@ -8,6 +8,7 @@ use futures::FutureExt;
88use futures:: future:: BoxFuture ;
99use futures:: try_join;
1010use vortex_array:: ArrayRef ;
11+ use vortex_array:: Canonical ;
1112use vortex_array:: IntoArray ;
1213use vortex_array:: MaskFuture ;
1314use vortex_array:: VortexSessionExecute ;
@@ -140,37 +141,36 @@ impl ListReader {
140141 /// Projection for [`ListChildrenNeeded::All`] expressions. Materializes the list and applies
141142 /// the expression.
142143 ///
143- /// Dispatches between a bounded read for a strict sub-range and a concurrent read for the full
144- /// column. The bounded read is valid for any elements layout and allows transparent wrappers,
145- /// such as zoned or structural layouts, to push the range down to their children.
144+ /// An all-true mask over the full local range reads every child concurrently. Otherwise, the
145+ /// read is bounded to the first and last selected list.
146146 fn project_all (
147147 & self ,
148148 row_range : & Range < u64 > ,
149149 expr : & Expression ,
150150 mask : MaskFuture ,
151151 ) -> VortexResult < ArrayFuture > {
152152 let is_full_range = row_range. start == 0 && row_range. end == self . layout . row_count ( ) ;
153- if is_full_range {
154- self . project_all_concurrent ( expr, mask)
155- } else {
156- self . project_all_elements_bounded ( row_range, expr, mask)
153+ let reader = self . clone ( ) ;
154+ let row_range = row_range. clone ( ) ;
155+ let expr = expr. clone ( ) ;
156+ Ok ( async move {
157+ let mask = mask. await ?;
158+ if is_full_range && mask. all_true ( ) {
159+ reader. project_all_full ( & expr) ?. await
160+ } else {
161+ reader. project_all_bounded ( & row_range, & expr, mask) ?. await
162+ }
157163 }
164+ . boxed ( ) )
158165 }
159166
160- /// Full-column read: fetches the entire `elements`, `offsets`, and `validity` children
161- /// concurrently — no offsets→elements round-trip, since the elements bound is the whole buffer.
162- /// The materialized list is filtered by the caller mask.
163- fn project_all_concurrent (
164- & self ,
165- expr : & Expression ,
166- mask : MaskFuture ,
167- ) -> VortexResult < ArrayFuture > {
167+ /// Fetch the complete `elements`, `offsets`, and `validity` children concurrently.
168+ fn project_all_full ( & self , expr : & Expression ) -> VortexResult < ArrayFuture > {
168169 let row_count = self . layout . row_count ( ) ;
169170 let elements_row_count = self . elements . row_count ( ) ;
170171 let nullability = self . layout . dtype ( ) . nullability ( ) ;
171172 let expr = expr. clone ( ) ;
172173
173- // Fire all three child reads up front so they run concurrently and overlap the mask await.
174174 let offsets_fut = self . fetch_raw_offsets ( & ( 0 ..row_count) ) ?;
175175 let elements_fut = self . fetch_raw_elements ( & ( 0 ..elements_row_count) ) ?;
176176 let validity_fut = fetch_validity (
@@ -187,65 +187,60 @@ impl ListReader {
187187 ListArray :: new_unchecked ( elements, offsets, create_validity ( validity, nullability) )
188188 }
189189 . into_array ( ) ;
190-
191- // Filter before applying the expression: the expression may depend on the filtered
192- // rows being removed (e.g. `cast(a, u8) where a < 256`).
193- let mask = mask. await ?;
194- let list = if mask. all_true ( ) {
195- list
196- } else {
197- list. filter ( mask) ?
198- } ;
199190 list. apply ( & expr)
200191 }
201192 . boxed ( ) )
202193 }
203194
204- /// Bounded read for a strict sub-range. Reads `offsets[row_range]`, decodes the first and last
205- /// offset to bound the elements read to `[first..last)`, then rebases the offsets to index into
206- /// that sliced buffer. Range-aware elements layouts can skip non-overlapping segments at the
207- /// cost of one offsets→elements round-trip.
208- fn project_all_elements_bounded (
195+ /// Bounded read for a sub-range or selective mask. Crops leading and trailing unselected lists,
196+ /// reads the element range they cover, and delegates any interior filtering to the reconstructed
197+ /// list array.
198+ fn project_all_bounded (
209199 & self ,
210200 row_range : & Range < u64 > ,
211201 expr : & Expression ,
212- mask : MaskFuture ,
202+ mask : Mask ,
213203 ) -> VortexResult < ArrayFuture > {
204+ // Crop to the smallest contiguous row range containing every selected list.
205+ let Some ( selected_rows) = selected_row_range ( & mask) else {
206+ let empty = Canonical :: empty ( self . layout . dtype ( ) ) . into_array ( ) ;
207+ let expr = expr. clone ( ) ;
208+ return Ok ( async move { empty. apply ( & expr) } . boxed ( ) ) ;
209+ } ;
210+
211+ let selected_mask = mask. slice ( selected_rows. clone ( ) ) ;
212+ let selected_row_range = ( row_range. start + u64:: try_from ( selected_rows. start ) ?)
213+ ..( row_range. start + u64:: try_from ( selected_rows. end ) ?) ;
214+
214215 let nullability = self . layout . dtype ( ) . nullability ( ) ;
215216 let expr = expr. clone ( ) ;
216- let row_count = usize:: try_from ( row_range. end - row_range. start ) ?;
217217 let reader = self . clone ( ) ;
218-
219- let offsets_fut = self . fetch_raw_offsets ( row_range) ?;
220- let validity_fut = fetch_validity (
221- self . validity . as_ref ( ) ,
222- row_range,
223- MaskFuture :: new_true ( row_count) ,
224- ) ?;
218+ let offsets_fut = self . fetch_raw_offsets ( & selected_row_range) ?;
225219
226220 Ok ( async move {
227- let ( offsets, validity) = try_join ! ( offsets_fut, validity_fut) ?;
228- let elements_range = elements_range_from_offsets ( & offsets, & reader. session ) ?;
221+ let offsets = offsets_fut. await ?;
229222
230- // Read only the elements this range covers.
231- let elements = reader. fetch_raw_elements ( & elements_range) ?. await ?;
223+ let elements_range = elements_range_from_offsets ( & offsets, & reader. session ) ?;
224+ let elements_fut = reader. fetch_raw_elements ( & elements_range) ?;
225+ let validity_fut = fetch_validity (
226+ reader. validity . as_ref ( ) ,
227+ & selected_row_range,
228+ MaskFuture :: new_true ( selected_mask. len ( ) ) ,
229+ ) ?;
230+ let ( elements, validity) = try_join ! ( elements_fut, validity_fut) ?;
232231
233- // Rebase the offsets to index into the sliced elements buffer.
234232 let offsets = rebase_offsets ( offsets, elements_range. start ) ?;
235- // SAFETY: the ListLayout children were written from a valid ListArray. Slicing the
236- // elements to the first and last offsets and rebasing every offset by the first one
237- // preserves the list invariants .
233+ // SAFETY: the selected offsets remain monotonically increasing, rebasing them against
234+ // the selected element range preserves their lengths, and validity covers the same
235+ // cropped list rows .
238236 let list = unsafe {
239237 ListArray :: new_unchecked ( elements, offsets, create_validity ( validity, nullability) )
240238 }
241239 . into_array ( ) ;
242-
243- // Filter before applying the expression (see `project_all_concurrent`).
244- let mask = mask. await ?;
245- let list = if mask. all_true ( ) {
240+ let list = if selected_mask. all_true ( ) {
246241 list
247242 } else {
248- list. filter ( mask ) ?
243+ list. filter ( selected_mask ) ?
249244 } ;
250245 list. apply ( & expr)
251246 }
@@ -315,6 +310,10 @@ impl ListReader {
315310 }
316311}
317312
313+ fn selected_row_range ( mask : & Mask ) -> Option < Range < usize > > {
314+ Some ( mask. first ( ) ?..mask. last ( ) ? + 1 )
315+ }
316+
318317fn create_validity ( validity_array : Option < ArrayRef > , nullability : Nullability ) -> Validity {
319318 match validity_array {
320319 Some ( arr) => Validity :: Array ( arr) ,
@@ -502,6 +501,8 @@ fn predicate_array_to_mask(array: ArrayRef, session: &VortexSession) -> VortexRe
502501#[ cfg( test) ]
503502mod tests {
504503 use std:: ops:: Range ;
504+ use std:: sync:: atomic:: AtomicUsize ;
505+ use std:: sync:: atomic:: Ordering ;
505506
506507 use rstest:: rstest;
507508 use vortex_array:: ArrayContext ;
@@ -527,6 +528,7 @@ mod tests {
527528 use crate :: layouts:: list:: writer:: ListLayoutStrategy ;
528529 use crate :: layouts:: repartition:: RepartitionStrategy ;
529530 use crate :: layouts:: repartition:: RepartitionWriterOptions ;
531+ use crate :: segments:: SegmentFuture ;
530532 use crate :: segments:: SegmentSource ;
531533 use crate :: segments:: TestSegments ;
532534 use crate :: sequence:: SequenceId ;
@@ -930,6 +932,18 @@ mod tests {
930932 ListLayoutStrategy :: default ( ) . with_elements ( chunked_elements)
931933 }
932934
935+ struct CountingSegmentSource {
936+ inner : Arc < dyn SegmentSource > ,
937+ request_count : Arc < AtomicUsize > ,
938+ }
939+
940+ impl SegmentSource for CountingSegmentSource {
941+ fn request ( & self , id : crate :: segments:: SegmentId ) -> SegmentFuture {
942+ self . request_count . fetch_add ( 1 , Ordering :: Relaxed ) ;
943+ self . inner . request ( id)
944+ }
945+ }
946+
933947 /// The chunked-elements strategy must actually produce a chunked `elements` layout, otherwise
934948 /// the reader would silently take the whole-chunk path and the bounded read would be untested.
935949 #[ tokio:: test]
@@ -945,6 +959,34 @@ mod tests {
945959 Ok ( ( ) )
946960 }
947961
962+ /// A sparse mask must remain selective even when the requested row range covers the complete
963+ /// local list layout. The selected first list touches one element chunk plus the offsets
964+ /// segment; reading all five element chunks would make the count six.
965+ #[ tokio:: test]
966+ async fn full_range_sparse_mask_crops_element_read ( ) -> VortexResult < ( ) > {
967+ let list = create_wider_list_array ( false ) ;
968+ let ctx = LayoutReaderContext :: new ( ) ;
969+ let ( segments, layout, session) =
970+ write_layout ( & chunked_elements_list_strategy ( ) , list. clone ( ) ) . await ?;
971+ let request_count = Arc :: new ( AtomicUsize :: new ( 0 ) ) ;
972+ let source = Arc :: new ( CountingSegmentSource {
973+ inner : segments,
974+ request_count : Arc :: clone ( & request_count) ,
975+ } ) ;
976+ let reader = layout. new_reader ( "" . into ( ) , source, & session, & ctx) ?;
977+
978+ let mask = Mask :: from_iter ( [ true , false , false , false , false ] ) ;
979+ let result = reader
980+ . projection_evaluation ( & ( 0 ..5 ) , & root ( ) , MaskFuture :: ready ( mask. clone ( ) ) ) ?
981+ . await ?;
982+
983+ let expected = list. filter ( mask) ?;
984+ let mut exec_ctx = session. create_execution_ctx ( ) ;
985+ assert_arrays_eq ! ( result, expected, & mut exec_ctx) ;
986+ assert_eq ! ( request_count. load( Ordering :: Relaxed ) , 2 ) ;
987+ Ok ( ( ) )
988+ }
989+
948990 /// With chunked elements, sub-range projections take the bounded read path. Every
949991 /// range/mask/nullability combination must match the same projection over the ground-truth
950992 /// array (`list.slice(range).filter(mask)`).
@@ -958,6 +1000,7 @@ mod tests {
9581000 #[ case:: single_empty_row( 2 ..3 , Mask :: from_iter( [ true ] ) , false ) ]
9591001 #[ case:: empty_range( 2 ..2 , Mask :: new_true( 0 ) , false ) ]
9601002 #[ case:: subrange_all_false( 1 ..4 , Mask :: new_false( 3 ) , false ) ]
1003+ #[ case:: subrange_single_interior( 0 ..4 , Mask :: from_iter( [ false , true , false , false ] ) , false ) ]
9611004 #[ case:: subrange_sparse_nullable( 1 ..4 , Mask :: from_iter( [ true , false , true ] ) , true ) ]
9621005 #[ case:: partial_end_nullable( 2 ..5 , Mask :: new_true( 3 ) , true ) ]
9631006 #[ tokio:: test]
0 commit comments