@@ -1990,3 +1990,233 @@ async fn test_can_prune_composite_predicates() -> VortexResult<()> {
19901990
19911991 Ok ( ( ) )
19921992}
1993+
1994+ #[ tokio:: test]
1995+ #[ cfg_attr( miri, ignore) ]
1996+ async fn test_segment_ordering_array_trees_consolidated_and_after_data ( ) -> VortexResult < ( ) > {
1997+ // Multi-column struct large enough to produce chunked data, so each column will have
1998+ // many ArrayTreeFlat leaves. The collector should consolidate their compact trees into a
1999+ // single segment per ArrayTreeLayout.
2000+ let n = 100_000 ;
2001+ let values: Vec < & str > = ( 0 ..n) . map ( |i| [ "alpha" , "beta" , "gamma" ] [ i % 3 ] ) . collect ( ) ;
2002+ let strings = VarBinArray :: from ( values) . into_array ( ) ;
2003+ let numbers = PrimitiveArray :: from_iter ( 0 ..n as i32 ) . into_array ( ) ;
2004+ let floats = PrimitiveArray :: from_iter ( ( 0 ..n) . map ( |i| i as f64 * 0.1 ) ) . into_array ( ) ;
2005+
2006+ let st = StructArray :: from_fields ( & [
2007+ ( "strings" , strings) ,
2008+ ( "numbers" , numbers) ,
2009+ ( "floats" , floats) ,
2010+ ] )
2011+ . unwrap ( ) ;
2012+
2013+ let mut buf = ByteBufferMut :: empty ( ) ;
2014+ let strategy = crate :: WriteStrategyBuilder :: default ( )
2015+ . with_array_tree ( true )
2016+ . build ( ) ;
2017+ let summary = SESSION
2018+ . write_options ( )
2019+ . with_strategy ( strategy)
2020+ . write ( & mut buf, st. into_array ( ) . to_array_stream ( ) )
2021+ . await ?;
2022+
2023+ let footer = summary. footer ( ) ;
2024+ let segment_specs = footer. segment_map ( ) ;
2025+ let root = footer. layout ( ) ;
2026+
2027+ // For each ArrayTreeLayout in the tree, assert:
2028+ // 1. **Consolidation:** the auxiliary `array_trees` child writes exactly one segment.
2029+ // 2. **Per-column ordering:** every data segment under child 0 appears before the
2030+ // array_trees segment under child 1.
2031+ fn check_array_tree_layouts (
2032+ layout : & dyn Layout ,
2033+ segment_specs : & [ SegmentSpec ] ,
2034+ found_any : & mut bool ,
2035+ ) {
2036+ if layout. encoding_id ( ) . as_ref ( ) == "vortex.array_tree" {
2037+ * found_any = true ;
2038+
2039+ let data_child = layout. child ( 0 ) . unwrap ( ) ;
2040+ let array_trees_child = layout. child ( 1 ) . unwrap ( ) ;
2041+
2042+ let data_offsets = collect_segment_offsets ( data_child. as_ref ( ) , segment_specs) ;
2043+ let array_trees_offsets =
2044+ collect_segment_offsets ( array_trees_child. as_ref ( ) , segment_specs) ;
2045+
2046+ assert_eq ! (
2047+ array_trees_offsets. len( ) ,
2048+ 1 ,
2049+ "array_tree: auxiliary child must consolidate to exactly 1 segment, got {} segments at offsets {:?}" ,
2050+ array_trees_offsets. len( ) ,
2051+ array_trees_offsets,
2052+ ) ;
2053+
2054+ assert ! (
2055+ !data_offsets. is_empty( ) ,
2056+ "array_tree: data child must have at least one segment"
2057+ ) ;
2058+
2059+ assert_offsets_ordered (
2060+ & data_offsets,
2061+ & array_trees_offsets,
2062+ "array_tree: all data segments should come before the array_trees segment" ,
2063+ ) ;
2064+ }
2065+
2066+ for child in layout. children ( ) . unwrap ( ) {
2067+ check_array_tree_layouts ( child. as_ref ( ) , segment_specs, found_any) ;
2068+ }
2069+ }
2070+
2071+ let mut found_any = false ;
2072+ check_array_tree_layouts ( root. as_ref ( ) , segment_specs, & mut found_any) ;
2073+ assert ! (
2074+ found_any,
2075+ "test setup expected the default write strategy to produce at least one ArrayTreeLayout"
2076+ ) ;
2077+
2078+ Ok ( ( ) )
2079+ }
2080+
2081+ #[ tokio:: test]
2082+ #[ cfg_attr( miri, ignore) ]
2083+ async fn test_segment_ordering_array_trees_before_zones ( ) -> VortexResult < ( ) > {
2084+ // The write strategy wraps every column in `ZonedStrategy { data: ArrayTree, zones }`.
2085+ // Assert per-Zoned-layout that the array_trees segment (inside the data child) appears
2086+ // before every zone-map segment in the same column.
2087+ let n = 100_000 ;
2088+ let values: Vec < & str > = ( 0 ..n) . map ( |i| [ "alpha" , "beta" , "gamma" ] [ i % 3 ] ) . collect ( ) ;
2089+ let strings = VarBinArray :: from ( values) . into_array ( ) ;
2090+ let numbers = PrimitiveArray :: from_iter ( 0 ..n as i32 ) . into_array ( ) ;
2091+ let floats = PrimitiveArray :: from_iter ( ( 0 ..n) . map ( |i| i as f64 * 0.1 ) ) . into_array ( ) ;
2092+
2093+ let st = StructArray :: from_fields ( & [
2094+ ( "strings" , strings) ,
2095+ ( "numbers" , numbers) ,
2096+ ( "floats" , floats) ,
2097+ ] )
2098+ . unwrap ( ) ;
2099+
2100+ let mut buf = ByteBufferMut :: empty ( ) ;
2101+ let strategy = crate :: WriteStrategyBuilder :: default ( )
2102+ . with_array_tree ( true )
2103+ . build ( ) ;
2104+ let summary = SESSION
2105+ . write_options ( )
2106+ . with_strategy ( strategy)
2107+ . write ( & mut buf, st. into_array ( ) . to_array_stream ( ) )
2108+ . await ?;
2109+
2110+ let footer = summary. footer ( ) ;
2111+ let segment_specs = footer. segment_map ( ) ;
2112+ let root = footer. layout ( ) ;
2113+
2114+ fn check_zoned_with_array_tree (
2115+ layout : & dyn Layout ,
2116+ segment_specs : & [ SegmentSpec ] ,
2117+ found_any : & mut bool ,
2118+ ) {
2119+ if layout. encoding_id ( ) . as_ref ( ) == "vortex.stats" {
2120+ let data_child = layout. child ( 0 ) . unwrap ( ) ;
2121+ let zones_child = layout. child ( 1 ) . unwrap ( ) ;
2122+
2123+ if data_child. encoding_id ( ) . as_ref ( ) == "vortex.array_tree" {
2124+ * found_any = true ;
2125+ let array_trees_offsets =
2126+ collect_segment_offsets ( data_child. child ( 1 ) . unwrap ( ) . as_ref ( ) , segment_specs) ;
2127+ let zones_offsets = collect_segment_offsets ( zones_child. as_ref ( ) , segment_specs) ;
2128+
2129+ assert_offsets_ordered (
2130+ & array_trees_offsets,
2131+ & zones_offsets,
2132+ "zoned wrapping array_tree: the array_trees segment should come before zone-map segments" ,
2133+ ) ;
2134+ }
2135+ }
2136+
2137+ for child in layout. children ( ) . unwrap ( ) {
2138+ check_zoned_with_array_tree ( child. as_ref ( ) , segment_specs, found_any) ;
2139+ }
2140+ }
2141+
2142+ let mut found_any = false ;
2143+ check_zoned_with_array_tree ( root. as_ref ( ) , segment_specs, & mut found_any) ;
2144+ assert ! (
2145+ found_any,
2146+ "test setup expected the default write strategy to produce at least one Zoned wrapping an ArrayTree"
2147+ ) ;
2148+
2149+ Ok ( ( ) )
2150+ }
2151+
2152+ #[ tokio:: test]
2153+ #[ cfg_attr( miri, ignore) ]
2154+ async fn test_roundtrip_array_tree_layout ( ) -> VortexResult < ( ) > {
2155+ // End-to-end coverage: write with `with_array_tree(true)`, then read back through the
2156+ // source-publishing reader-ctx path and assert the data matches. Exercises:
2157+ // - ArrayTreeCollectorStrategy collecting compact trees from leaf transient state
2158+ // - ArrayTreeLayout::new_reader publishing the ArrayTreesSource into the reader ctx
2159+ // - ArrayTreeFlatReader pulling the source and resolving its tree by segment_id
2160+ // - ColumnarSerializedArray::from_segment_and_tree decoding the data segment
2161+ let mut ctx = SESSION . create_execution_ctx ( ) ;
2162+
2163+ let n = 10_000 ;
2164+ let strings_in: Vec < & str > = ( 0 ..n) . map ( |i| [ "alpha" , "beta" , "gamma" ] [ i % 3 ] ) . collect ( ) ;
2165+ let strings = VarBinArray :: from ( strings_in. clone ( ) ) . into_array ( ) ;
2166+ let numbers_in: Vec < i32 > = ( 0 ..n as i32 ) . collect ( ) ;
2167+ let numbers = PrimitiveArray :: from_iter ( numbers_in. iter ( ) . copied ( ) ) . into_array ( ) ;
2168+
2169+ let st = StructArray :: from_fields ( & [ ( "strings" , strings) , ( "numbers" , numbers) ] ) ?. into_array ( ) ;
2170+ let dtype = st. dtype ( ) . clone ( ) ;
2171+
2172+ let mut buf = ByteBufferMut :: empty ( ) ;
2173+ let strategy = crate :: WriteStrategyBuilder :: default ( )
2174+ . with_array_tree ( true )
2175+ . build ( ) ;
2176+ SESSION
2177+ . write_options ( )
2178+ . with_strategy ( strategy)
2179+ . write ( & mut buf, st. to_array_stream ( ) )
2180+ . await ?;
2181+
2182+ // Sanity-check we actually wrote ArrayTreeLayout nodes — otherwise the test would
2183+ // silently pass on the default code path.
2184+ let file = SESSION . open_options ( ) . open_buffer ( buf) ?;
2185+ fn has_array_tree ( layout : & dyn Layout ) -> bool {
2186+ if layout. encoding_id ( ) . as_ref ( ) == "vortex.array_tree" {
2187+ return true ;
2188+ }
2189+ layout
2190+ . children ( )
2191+ . map ( |cs| cs. iter ( ) . any ( |c| has_array_tree ( c. as_ref ( ) ) ) )
2192+ . unwrap_or ( false )
2193+ }
2194+ assert ! (
2195+ has_array_tree( file. footer( ) . layout( ) . as_ref( ) ) ,
2196+ "test expected ArrayTreeLayout in the written file"
2197+ ) ;
2198+
2199+ let result = file. scan ( ) ?. into_array_stream ( ) ?. read_all ( ) . await ?;
2200+ assert_eq ! ( result. len( ) , n) ;
2201+ assert_eq ! ( result. dtype( ) , & dtype) ;
2202+
2203+ let struct_array = result. execute :: < StructArray > ( & mut ctx) ?;
2204+
2205+ let read_numbers = struct_array. unmasked_field_by_name ( "numbers" ) . cloned ( ) ?;
2206+ let expected_numbers = PrimitiveArray :: from_iter ( numbers_in. iter ( ) . copied ( ) ) . into_array ( ) ;
2207+ assert_arrays_eq ! ( read_numbers, expected_numbers) ;
2208+
2209+ let read_strings = struct_array
2210+ . unmasked_field_by_name ( "strings" )
2211+ . cloned ( ) ?
2212+ . execute :: < VarBinViewArray > ( & mut ctx) ?
2213+ . with_iterator ( |iter| {
2214+ iter. map ( |s| s. map ( |st| unsafe { String :: from_utf8_unchecked ( st. to_vec ( ) ) } ) )
2215+ . collect :: < Vec < _ > > ( )
2216+ } ) ;
2217+ let expected_strings: Vec < Option < String > > =
2218+ strings_in. iter ( ) . map ( |s| Some ( ( * s) . to_string ( ) ) ) . collect ( ) ;
2219+ assert_eq ! ( read_strings, expected_strings) ;
2220+
2221+ Ok ( ( ) )
2222+ }
0 commit comments