@@ -61,6 +61,8 @@ use crate::traits::{
6161} ;
6262#[ cfg( all( feature = "views" , feature = "select" ) ) ]
6363use crate :: Bitmask ;
64+ #[ cfg( all( feature = "views" , feature = "chunked" , feature = "size" ) ) ]
65+ use crate :: { ByteSize , SuperTableV } ;
6466#[ cfg( feature = "views" ) ]
6567use crate :: { Array , BitmaskV , NumericArrayV , TableV , TextArrayV } ;
6668
@@ -466,6 +468,38 @@ impl Table {
466468 TableV :: from_table ( self . clone ( ) , offset, len)
467469 }
468470
471+ /// Splits this table into a [`SuperTableV`] of zero-copy row views
472+ /// sized towards `target_bytes` each.
473+ ///
474+ /// The row count per view derives from the table's average bytes per
475+ /// row via [`ByteSize::est_bytes`]. Views carry row bounds rather
476+ /// than buffers, so no column data moves and the parent's buffers
477+ /// stay untouched. The final view holds the remaining rows, and a
478+ /// table that fits within `target_bytes` returns a single view.
479+ ///
480+ /// `target_bytes` is a sizing target rather than a strict bound, as
481+ /// a view's actual bytes follow its rows' variable-width content.
482+ #[ cfg( all( feature = "views" , feature = "chunked" , feature = "size" ) ) ]
483+ pub fn get_views_for_target_batch_size ( & self , target_bytes : usize ) -> SuperTableV {
484+ let rows = self . n_rows ;
485+ if rows == 0 {
486+ return SuperTableV {
487+ slices : vec ! [ self . slice( 0 , 0 ) ] ,
488+ len : 0 ,
489+ } ;
490+ }
491+ let per_row = ( self . est_bytes ( ) / rows) . max ( 1 ) ;
492+ let stride = ( target_bytes / per_row) . clamp ( 1 , rows) ;
493+ let mut slices = Vec :: with_capacity ( rows. div_ceil ( stride) ) ;
494+ let mut offset = 0 ;
495+ while offset < rows {
496+ let len = stride. min ( rows - offset) ;
497+ slices. push ( self . slice ( offset, len) ) ;
498+ offset += stride;
499+ }
500+ SuperTableV { slices, len : rows }
501+ }
502+
469503 /// Gather the rows at the given indices into a new materialised Table.
470504 #[ cfg( all( feature = "views" , feature = "select" ) ) ]
471505 pub fn gather_rows ( & self , indices : & [ usize ] ) -> Table {
@@ -1309,8 +1343,97 @@ mod tests {
13091343 #[ cfg( all( feature = "views" , feature = "select" ) ) ]
13101344 use crate :: traits:: selection:: ColumnSelection ;
13111345 use crate :: { Array , BooleanArray , IntegerArray , NumericArray } ;
1346+ #[ cfg( all( feature = "views" , feature = "chunked" , feature = "size" ) ) ]
1347+ use crate :: { ByteSize , FloatArray , StringArray } ;
13121348 use crate :: { fa_bool, fa_i32, fa_i64, fa_u32} ;
13131349
1350+ #[ cfg( all( feature = "views" , feature = "chunked" , feature = "size" ) ) ]
1351+ fn view_batch_table ( rows : usize ) -> Table {
1352+ let ids: Vec < i32 > = ( 0 ..rows as i32 ) . collect ( ) ;
1353+ let values: Vec < f64 > = ( 0 ..rows) . map ( |i| i as f64 * 0.5 ) . collect ( ) ;
1354+ let labels: Vec < String > = ( 0 ..rows) . map ( |i| format ! ( "row_{}" , i) ) . collect ( ) ;
1355+ let label_refs: Vec < & str > = labels. iter ( ) . map ( |s| s. as_str ( ) ) . collect ( ) ;
1356+ Table :: new (
1357+ "bench" . to_string ( ) ,
1358+ Some ( vec ! [
1359+ FieldArray :: from_arr( "ids" , IntegerArray :: <i32 >:: from_slice( & ids) ) ,
1360+ FieldArray :: from_arr( "values" , FloatArray :: <f64 >:: from_slice( & values) ) ,
1361+ FieldArray :: from_arr( "labels" , StringArray :: <u32 >:: from_slice( & label_refs) ) ,
1362+ ] ) ,
1363+ )
1364+ }
1365+
1366+ #[ test]
1367+ #[ cfg( all( feature = "views" , feature = "chunked" , feature = "size" ) ) ]
1368+ fn views_tile_the_table_with_a_remainder ( ) {
1369+ let rows = 1300 ;
1370+ let table = view_batch_table ( rows) ;
1371+ let per_row = ( table. est_bytes ( ) / rows) . max ( 1 ) ;
1372+ let st = table. get_views_for_target_batch_size ( 500 * per_row) ;
1373+ let counts: Vec < usize > = st. slices . iter ( ) . map ( |s| s. len ) . collect ( ) ;
1374+ assert_eq ! ( counts, vec![ 500 , 500 , 300 ] ) ;
1375+ assert_eq ! ( st. len, rows) ;
1376+ let offsets: Vec < usize > = st. slices . iter ( ) . map ( |s| s. offset ) . collect ( ) ;
1377+ assert_eq ! ( offsets, vec![ 0 , 500 , 1000 ] ) ;
1378+ }
1379+
1380+ #[ test]
1381+ #[ cfg( all( feature = "views" , feature = "chunked" , feature = "size" ) ) ]
1382+ fn views_read_the_parent_rows_in_place ( ) {
1383+ let rows = 1200 ;
1384+ let table = view_batch_table ( rows) ;
1385+ let per_row = ( table. est_bytes ( ) / rows) . max ( 1 ) ;
1386+ let st = table. get_views_for_target_batch_size ( 500 * per_row) ;
1387+ let parent_ptr = match & table. cols [ 0 ] . array {
1388+ Array :: NumericArray ( NumericArray :: Int32 ( a) ) => a. data . as_slice ( ) . as_ptr ( ) ,
1389+ _ => panic ! ( "expected Int32 column" ) ,
1390+ } ;
1391+ let mut row = 0usize ;
1392+ for view in & st. slices {
1393+ // The view's array is the parent array behind a reference
1394+ // count, so its buffer pointer matches the parent's.
1395+ let ( array, offset, len) = view. cols [ 0 ] . as_tuple_ref ( ) ;
1396+ assert_eq ! ( offset, row) ;
1397+ match array {
1398+ Array :: NumericArray ( NumericArray :: Int32 ( a) ) => {
1399+ assert_eq ! ( a. data. as_slice( ) . as_ptr( ) , parent_ptr) ;
1400+ for i in 0 ..len {
1401+ assert_eq ! ( a. data. as_slice( ) [ offset + i] , ( row + i) as i32 ) ;
1402+ }
1403+ }
1404+ _ => panic ! ( "expected Int32 column" ) ,
1405+ }
1406+ for i in 0 ..view. len {
1407+ assert_eq ! (
1408+ view. cols[ 2 ] . get_str( i) . unwrap( ) ,
1409+ format!( "row_{}" , row + i)
1410+ ) ;
1411+ }
1412+ row += view. len ;
1413+ }
1414+ assert_eq ! ( row, rows) ;
1415+ }
1416+
1417+ #[ test]
1418+ #[ cfg( all( feature = "views" , feature = "chunked" , feature = "size" ) ) ]
1419+ fn table_within_target_returns_one_view ( ) {
1420+ let rows = 700 ;
1421+ let table = view_batch_table ( rows) ;
1422+ let st = table. get_views_for_target_batch_size ( usize:: MAX ) ;
1423+ assert_eq ! ( st. slices. len( ) , 1 ) ;
1424+ assert_eq ! ( st. slices[ 0 ] . len, rows) ;
1425+ assert_eq ! ( st. len, rows) ;
1426+ }
1427+
1428+ #[ test]
1429+ #[ cfg( all( feature = "views" , feature = "chunked" , feature = "size" ) ) ]
1430+ fn empty_table_returns_one_empty_view ( ) {
1431+ let table = Table :: new ( "empty" . to_string ( ) , None ) ;
1432+ let st = table. get_views_for_target_batch_size ( 1024 ) ;
1433+ assert_eq ! ( st. slices. len( ) , 1 ) ;
1434+ assert_eq ! ( st. len, 0 ) ;
1435+ }
1436+
13141437 #[ test]
13151438 fn test_new_table ( ) {
13161439 let t = Table :: new_empty ( ) ;
0 commit comments