@@ -34,7 +34,7 @@ use std::task::{Context, Poll};
3434
3535use arrow:: array:: {
3636 Array , ArrayRef , BinaryViewArray , BufferSpec , GenericByteViewArray , StringViewArray ,
37- layout, make_array,
37+ builder :: GenericByteViewBuilder , layout, make_array,
3838} ;
3939use arrow:: buffer:: Buffer ;
4040use arrow:: datatypes:: DataType ;
@@ -420,6 +420,20 @@ pub(crate) fn gc_view_arrays(batch: &RecordBatch) -> Result<RecordBatch> {
420420 }
421421}
422422
423+ /// Garbage collect and deduplicate a string view before writing it out to disk
424+ ///
425+ /// This is to avoid inflating disk usage and also to ensure that deduplication reduces memory pressure when reading back.
426+ fn gc_dedup_view < T : ByteViewType > (
427+ array : & GenericByteViewArray < T > ,
428+ ) -> GenericByteViewArray < T > {
429+ let mut builder = GenericByteViewBuilder :: < T > :: with_capacity ( array. len ( ) )
430+ . with_deduplicate_strings ( ) ;
431+ for v in array. iter ( ) {
432+ builder. append_option ( v) ;
433+ }
434+ builder. finish ( )
435+ }
436+
423437fn gc_array ( array : & ArrayRef ) -> Result < ( ArrayRef , bool ) > {
424438 match array. data_type ( ) {
425439 DataType :: Utf8View => {
@@ -428,7 +442,7 @@ fn gc_array(array: &ArrayRef) -> Result<(ArrayRef, bool)> {
428442 . downcast_ref :: < StringViewArray > ( )
429443 . expect ( "Utf8View array should downcast to StringViewArray" ) ;
430444 if should_gc_view_array ( string_view) {
431- Ok ( ( Arc :: new ( string_view . gc ( ) ) as ArrayRef , true ) )
445+ Ok ( ( Arc :: new ( gc_dedup_view ( string_view ) ) as ArrayRef , true ) )
432446 } else {
433447 Ok ( ( Arc :: clone ( array) , false ) )
434448 }
@@ -439,7 +453,7 @@ fn gc_array(array: &ArrayRef) -> Result<(ArrayRef, bool)> {
439453 . downcast_ref :: < BinaryViewArray > ( )
440454 . expect ( "BinaryView array should downcast to BinaryViewArray" ) ;
441455 if should_gc_view_array ( binary_view) {
442- Ok ( ( Arc :: new ( binary_view . gc ( ) ) as ArrayRef , true ) )
456+ Ok ( ( Arc :: new ( gc_dedup_view ( binary_view ) ) as ArrayRef , true ) )
443457 } else {
444458 Ok ( ( Arc :: clone ( array) , false ) )
445459 }
@@ -1345,7 +1359,7 @@ mod tests {
13451359 let size_without_gc: usize = array_without_gc
13461360 . data_buffers ( )
13471361 . iter ( )
1348- . map ( |buffer| buffer. capacity ( ) )
1362+ . map ( |buffer| buffer. len ( ) )
13491363 . sum ( ) ;
13501364
13511365 let gc_batch = gc_view_arrays ( & sliced_batch) ?;
@@ -1357,7 +1371,7 @@ mod tests {
13571371 let size_with_gc: usize = array_with_gc
13581372 . data_buffers ( )
13591373 . iter ( )
1360- . map ( |buffer| buffer. capacity ( ) )
1374+ . map ( |buffer| buffer. len ( ) )
13611375 . sum ( ) ;
13621376
13631377 let reduction_percent =
@@ -1376,9 +1390,18 @@ mod tests {
13761390 use arrow:: array:: { DictionaryArray , Int32Array } ;
13771391 use arrow:: buffer:: Buffer ;
13781392
1379- let strings: Vec < String > = ( 0 ..200 )
1393+ // A small pool of distinct, non-inlined (> 12 byte) strings.
1394+ let distinct: Vec < String > = ( 0 ..8 )
13801395 . map ( |i| format ! ( "http://example.com/nested/path/that/is/not/inlined/{i}" ) )
13811396 . collect ( ) ;
1397+
1398+ // Bytes stored once each string is deduplicated (all references collapse
1399+ // to a single copy of every distinct value).
1400+ let distinct_bytes: usize = distinct. iter ( ) . map ( |s| s. len ( ) ) . sum ( ) ;
1401+
1402+ let strings: Vec < String > = ( 0 ..200 )
1403+ . map ( |i| distinct[ i % distinct. len ( ) ] . clone ( ) )
1404+ . collect ( ) ;
13821405 let string_values = Arc :: new ( StringViewArray :: from ( strings) ) as ArrayRef ;
13831406
13841407 let list_data = ArrayDataBuilder :: new ( DataType :: List ( Arc :: new (
@@ -1418,9 +1441,11 @@ mod tests {
14181441 . as_any ( )
14191442 . downcast_ref :: < StringViewArray > ( )
14201443 . unwrap ( ) ;
1421- assert ! (
1422- calculate_string_view_waste_ratio( gc_list_values) < 0.2 ,
1423- "GC should compact nested List child views"
1444+ let list_stored_bytes: usize =
1445+ gc_list_values. data_buffers ( ) . iter ( ) . map ( |b| b. len ( ) ) . sum ( ) ;
1446+ assert_eq ! (
1447+ list_stored_bytes, distinct_bytes,
1448+ "GC should deduplicate nested List child views (regression: dedup not applied)"
14241449 ) ;
14251450
14261451 let gc_dictionary_values = gc_batch. column ( 1 ) . to_data ( ) . child_data ( ) [ 0 ] . clone ( ) ;
@@ -1429,9 +1454,14 @@ mod tests {
14291454 . as_any ( )
14301455 . downcast_ref :: < StringViewArray > ( )
14311456 . unwrap ( ) ;
1432- assert ! (
1433- calculate_string_view_waste_ratio( gc_dictionary_values) < 0.2 ,
1434- "GC should compact nested Dictionary values"
1457+ let dictionary_stored_bytes: usize = gc_dictionary_values
1458+ . data_buffers ( )
1459+ . iter ( )
1460+ . map ( |b| b. len ( ) )
1461+ . sum ( ) ;
1462+ assert_eq ! (
1463+ dictionary_stored_bytes, distinct_bytes,
1464+ "GC should deduplicate nested Dictionary values (regression: dedup not applied)"
14351465 ) ;
14361466
14371467 Ok ( ( ) )
0 commit comments