@@ -45,6 +45,8 @@ pub fn use_experimental_list_layout() -> bool {
4545 * USE_EXPERIMENTAL_LIST_LAYOUT
4646}
4747
48+ type ListLayoutFactory = Arc < dyn Fn ( ListLayoutStrategy ) -> Arc < dyn LayoutStrategy > + Send + Sync > ;
49+
4850/// A configurable strategy for writing nested tabular data, dispatching each (sub)stream to the
4951/// structural writer for its dtype.
5052///
@@ -67,10 +69,11 @@ pub struct TableStrategy {
6769 validity : Arc < dyn LayoutStrategy > ,
6870 /// The writer for leaf fields, i.e. anything that is not a struct.
6971 leaf : Arc < dyn LayoutStrategy > ,
70- /// Whether to write list fields using [`ListLayoutStrategy`].
72+ /// Optional factory applied to each dynamically constructed [`ListLayoutStrategy`].
73+ /// Its presence also enables list decomposition.
7174 ///
7275 /// [`ListLayoutStrategy`]: crate::layouts::list::writer::ListLayoutStrategy
73- use_list_layout : bool ,
76+ list_layout_factory : Option < ListLayoutFactory > ,
7477}
7578
7679impl TableStrategy {
@@ -97,7 +100,7 @@ impl TableStrategy {
97100 leaf_writers : Default :: default ( ) ,
98101 validity,
99102 leaf : fallback,
100- use_list_layout : false ,
103+ list_layout_factory : None ,
101104 }
102105 }
103106
@@ -166,8 +169,18 @@ impl TableStrategy {
166169 }
167170
168171 /// Enable writing list fields with [`ListLayoutStrategy`].
169- pub fn with_list_layout ( mut self ) -> Self {
170- self . use_list_layout = true ;
172+ pub fn with_list_layout ( self ) -> Self {
173+ self . with_list_layout_factory ( |strategy| Arc :: new ( strategy) )
174+ }
175+
176+ /// Enable list layouts and transform each structural list writer into a physical strategy.
177+ /// The factory receives a fully configured writer and is invoked independently for every list,
178+ /// including nested lists.
179+ pub fn with_list_layout_factory (
180+ mut self ,
181+ factory : impl Fn ( ListLayoutStrategy ) -> Arc < dyn LayoutStrategy > + Send + Sync + ' static ,
182+ ) -> Self {
183+ self . list_layout_factory = Some ( Arc :: new ( factory) ) ;
171184 self
172185 }
173186}
@@ -210,12 +223,14 @@ impl TableStrategy {
210223 /// The `elements` sub-column is routed back through a clean descended dispatcher so nested
211224 /// structs/lists recurse; `offsets` go straight to the leaf (they are always a primitive
212225 /// column); and `validity` uses the shared validity strategy.
213- fn list_strategy ( & self ) -> ListLayoutStrategy {
214- ListLayoutStrategy :: default ( )
226+ fn list_strategy ( & self ) -> Option < Arc < dyn LayoutStrategy > > {
227+ let factory = self . list_layout_factory . as_ref ( ) ?;
228+ let list_layout = ListLayoutStrategy :: default ( )
215229 . with_elements ( Arc :: new ( self . descend_clean ( ) ) )
216230 . with_offsets ( Arc :: clone ( & self . leaf ) )
217231 . with_validity ( Arc :: clone ( & self . validity ) )
218- . with_fallback ( Arc :: clone ( & self . leaf ) )
232+ . with_fallback ( Arc :: clone ( & self . leaf ) ) ;
233+ Some ( factory ( list_layout) )
219234 }
220235
221236 /// Descend into a subfield, retaining only the overrides that apply beneath it (rebased to be
@@ -236,7 +251,7 @@ impl TableStrategy {
236251 leaf_writers : new_writers,
237252 validity : Arc :: clone ( & self . validity ) ,
238253 leaf : Arc :: clone ( & self . leaf ) ,
239- use_list_layout : self . use_list_layout ,
254+ list_layout_factory : self . list_layout_factory . clone ( ) ,
240255 }
241256 }
242257
@@ -247,7 +262,7 @@ impl TableStrategy {
247262 leaf_writers : HashMap :: default ( ) ,
248263 validity : Arc :: clone ( & self . validity ) ,
249264 leaf : Arc :: clone ( & self . leaf ) ,
250- use_list_layout : self . use_list_layout ,
265+ list_layout_factory : self . list_layout_factory . clone ( ) ,
251266 }
252267 }
253268
@@ -290,9 +305,10 @@ impl LayoutStrategy for TableStrategy {
290305 . await ;
291306 }
292307
293- if dtype. is_list ( ) && self . use_list_layout {
294- return self
295- . list_strategy ( )
308+ if dtype. is_list ( )
309+ && let Some ( list_strategy) = self . list_strategy ( )
310+ {
311+ return list_strategy
296312 . write_stream ( ctx, segment_sink, stream, eof, session)
297313 . await ;
298314 }
@@ -306,6 +322,7 @@ impl LayoutStrategy for TableStrategy {
306322
307323#[ cfg( test) ]
308324mod tests {
325+ use std:: num:: NonZeroUsize ;
309326 use std:: sync:: Arc ;
310327 use std:: task:: Poll ;
311328
@@ -325,6 +342,7 @@ mod tests {
325342 use vortex_array:: field_path;
326343 use vortex_array:: validity:: Validity ;
327344 use vortex_buffer:: buffer;
345+ use vortex_error:: VortexExpect ;
328346 use vortex_error:: VortexResult ;
329347 use vortex_io:: runtime:: single:: block_on;
330348 use vortex_io:: session:: RuntimeSessionExt ;
@@ -333,7 +351,13 @@ mod tests {
333351 use crate :: LayoutStrategy ;
334352 use crate :: layouts:: chunked:: writer:: ChunkedLayoutStrategy ;
335353 use crate :: layouts:: flat:: writer:: FlatLayoutStrategy ;
354+ use crate :: layouts:: list:: List ;
355+ use crate :: layouts:: repartition:: RepartitionStrategy ;
356+ use crate :: layouts:: repartition:: RepartitionWriterOptions ;
336357 use crate :: layouts:: table:: TableStrategy ;
358+ use crate :: layouts:: zoned:: Zoned ;
359+ use crate :: layouts:: zoned:: writer:: ZonedLayoutOptions ;
360+ use crate :: layouts:: zoned:: writer:: ZonedStrategy ;
337361 use crate :: segments:: TestSegments ;
338362 use crate :: sequence:: SequenceId ;
339363 use crate :: sequence:: SequentialArrayStreamExt ;
@@ -478,6 +502,54 @@ mod tests {
478502 Ok ( ( ) )
479503 }
480504
505+ /// A wrapper can repartition and zone lists in outer-row space before decomposition.
506+ #[ tokio:: test]
507+ async fn wraps_list_strategy_before_decomposition ( ) -> VortexResult < ( ) > {
508+ let list = ListArray :: try_new (
509+ PrimitiveArray :: from_iter ( 0 ..9_i32 ) . into_array ( ) ,
510+ PrimitiveArray :: from_iter ( 0 ..=9_u32 ) . into_array ( ) ,
511+ Validity :: NonNullable ,
512+ ) ?
513+ . into_array ( ) ;
514+
515+ let row_block_size = NonZeroUsize :: new ( 4 ) . vortex_expect ( "4 is non-zero" ) ;
516+ let flat: Arc < dyn LayoutStrategy > = Arc :: new ( FlatLayoutStrategy :: default ( ) ) ;
517+ let stats = Arc :: clone ( & flat) ;
518+ let chunked: Arc < dyn LayoutStrategy > =
519+ Arc :: new ( ChunkedLayoutStrategy :: new ( FlatLayoutStrategy :: default ( ) ) ) ;
520+ let dispatcher = TableStrategy :: new ( Arc :: clone ( & flat) , chunked) . with_list_layout_factory (
521+ move |list_layout| {
522+ let zoned = ZonedStrategy :: new (
523+ list_layout,
524+ Arc :: clone ( & stats) ,
525+ ZonedLayoutOptions {
526+ block_size : row_block_size,
527+ ..Default :: default ( )
528+ } ,
529+ ) ;
530+ Arc :: new ( RepartitionStrategy :: new (
531+ zoned,
532+ RepartitionWriterOptions {
533+ block_size_minimum : 0 ,
534+ block_len_multiple : row_block_size. get ( ) ,
535+ block_size_target : None ,
536+ canonicalize : false ,
537+ } ,
538+ ) ) as Arc < dyn LayoutStrategy >
539+ } ,
540+ ) ;
541+
542+ let layout = write ( & dispatcher, list) . await ?;
543+ let zoned = layout. as_ :: < Zoned > ( ) ;
544+ assert_eq ! ( zoned. zone_len( ) , 4 ) ;
545+ assert_eq ! ( zoned. nzones( ) , 3 ) ;
546+
547+ let data = layout. child ( 0 ) ?;
548+ assert ! ( data. is:: <List >( ) ) ;
549+ assert_eq ! ( data. row_count( ) , 9 ) ;
550+ Ok ( ( ) )
551+ }
552+
481553 /// A non-struct stream is not shredded; it is handed straight to the leaf strategy.
482554 #[ tokio:: test]
483555 async fn non_struct_input_uses_leaf ( ) -> VortexResult < ( ) > {
0 commit comments