@@ -3737,6 +3737,66 @@ impl PrimitiveStructuralEncoder {
37373737 Self :: is_narrow ( data_block)
37383738 }
37393739
3740+ /// Checks if the rep/def levels are too sparse for miniblock encoding.
3741+ ///
3742+ /// Miniblock chunks are limited to ~32KiB total. Data can use up to ~16KiB,
3743+ /// leaving ~16KiB for both rep and def buffers combined. Each chunk has at most
3744+ /// MAX_MINIBLOCK_VALUES (4096) data values, but when data has many empty/null
3745+ /// lists, the number of rep/def levels can far exceed the number of data values
3746+ /// (each empty list adds a level entry with no corresponding data value).
3747+ ///
3748+ /// We estimate the compressed bits per level by computing the max value in each
3749+ /// buffer and taking ceil(log2(max_val + 1)) — the minimum bits needed to
3750+ /// bitpack each level. We then calculate the maximum number of levels that fit
3751+ /// in 16KiB and compare against the actual levels-to-values ratio.
3752+ fn repdef_too_sparse_for_miniblock (
3753+ repdef : & crate :: repdef:: SerializedRepDefs ,
3754+ num_values : u64 ,
3755+ ) -> bool {
3756+ if num_values == 0 {
3757+ return false ;
3758+ }
3759+ let num_levels = repdef
3760+ . repetition_levels
3761+ . as_ref ( )
3762+ . map ( |r| r. len ( ) as u64 )
3763+ . max ( repdef. definition_levels . as_ref ( ) . map ( |d| d. len ( ) as u64 ) )
3764+ . unwrap_or ( 0 ) ;
3765+ if num_levels == 0 {
3766+ return false ;
3767+ }
3768+
3769+ // Compute bits needed per level for each buffer (ceil of log2(max+1))
3770+ let bits_per_rep = repdef
3771+ . repetition_levels
3772+ . as_ref ( )
3773+ . and_then ( |r| r. iter ( ) . max ( ) . copied ( ) )
3774+ . map ( |max_val| u16:: BITS - max_val. leading_zeros ( ) )
3775+ . unwrap_or ( 0 ) as u64 ;
3776+ let bits_per_def = repdef
3777+ . definition_levels
3778+ . as_ref ( )
3779+ . and_then ( |d| d. iter ( ) . max ( ) . copied ( ) )
3780+ . map ( |max_val| u16:: BITS - max_val. leading_zeros ( ) )
3781+ . unwrap_or ( 0 ) as u64 ;
3782+
3783+ let bits_per_level = bits_per_rep + bits_per_def;
3784+ if bits_per_level == 0 {
3785+ return false ;
3786+ }
3787+
3788+ // 16KiB budget for rep+def combined (half the ~32KiB chunk limit)
3789+ const REPDEF_BUDGET_BITS : u64 = 16 * 1024 * 8 ;
3790+ let max_levels_per_chunk = REPDEF_BUDGET_BITS / bits_per_level;
3791+
3792+ // A chunk has at most MAX_MINIBLOCK_VALUES data values. The levels-to-values
3793+ // ratio tells us how many levels a chunk of that size would need.
3794+ let levels_per_chunk =
3795+ ( num_levels as f64 / num_values as f64 ) * miniblock:: MAX_MINIBLOCK_VALUES as f64 ;
3796+
3797+ levels_per_chunk > max_levels_per_chunk as f64
3798+ }
3799+
37403800 fn prefers_fullzip ( encoding_metadata : & HashMap < String , String > ) -> bool {
37413801 // Fullzip is the backup option so the only reason we wouldn't use it is if the
37423802 // user specifically requested not to use it (in which case we're probably going
@@ -3798,7 +3858,7 @@ impl PrimitiveStructuralEncoder {
37983858 rep : Option < Vec < CompressedLevelsChunk > > ,
37993859 def : Option < Vec < CompressedLevelsChunk > > ,
38003860 support_large_chunk : bool ,
3801- ) -> SerializedMiniBlockPage {
3861+ ) -> Result < SerializedMiniBlockPage > {
38023862 let bytes_rep = rep
38033863 . as_ref ( )
38043864 . map ( |rep| rep. iter ( ) . map ( |r| r. data . len ( ) ) . sum :: < usize > ( ) )
@@ -3842,11 +3902,21 @@ impl PrimitiveStructuralEncoder {
38423902
38433903 // Write the buffer lengths
38443904 if let Some ( rep) = rep. as_ref ( ) {
3845- let bytes_rep = u16:: try_from ( rep. data . len ( ) ) . unwrap ( ) ;
3905+ let bytes_rep = u16:: try_from ( rep. data . len ( ) ) . map_err ( |_| {
3906+ Error :: internal ( format ! (
3907+ "Repetition buffer size ({} bytes) too large" ,
3908+ rep. data. len( )
3909+ ) )
3910+ } ) ?;
38463911 data_buffer. extend_from_slice ( & bytes_rep. to_le_bytes ( ) ) ;
38473912 }
38483913 if let Some ( def) = def. as_ref ( ) {
3849- let bytes_def = u16:: try_from ( def. data . len ( ) ) . unwrap ( ) ;
3914+ let bytes_def = u16:: try_from ( def. data . len ( ) ) . map_err ( |_| {
3915+ Error :: internal ( format ! (
3916+ "Definition buffer size ({} bytes) too large" ,
3917+ def. data. len( )
3918+ ) )
3919+ } ) ?;
38503920 data_buffer. extend_from_slice ( & bytes_def. to_le_bytes ( ) ) ;
38513921 }
38523922
@@ -3916,11 +3986,11 @@ impl PrimitiveStructuralEncoder {
39163986 let data_buffer = LanceBuffer :: from ( data_buffer) ;
39173987 let metadata_buffer = LanceBuffer :: from ( meta_buffer) ;
39183988
3919- SerializedMiniBlockPage {
3989+ Ok ( SerializedMiniBlockPage {
39203990 num_buffers : miniblocks. data . len ( ) as u64 ,
39213991 data : data_buffer,
39223992 metadata : metadata_buffer,
3923- }
3993+ } )
39243994 }
39253995
39263996 /// Compresses a buffer of levels into chunks
@@ -4448,7 +4518,7 @@ impl PrimitiveStructuralEncoder {
44484518 . map ( |cd| std:: mem:: take ( & mut cd. data ) ) ;
44494519
44504520 let serialized =
4451- Self :: serialize_miniblocks ( compressed_data, rep_data, def_data, support_large_chunk) ;
4521+ Self :: serialize_miniblocks ( compressed_data, rep_data, def_data, support_large_chunk) ? ;
44524522
44534523 // Metadata, Data, Dictionary, (maybe) Repetition Index
44544524 let mut data = Vec :: with_capacity ( 4 ) ;
@@ -5112,41 +5182,61 @@ impl PrimitiveStructuralEncoder {
51125182 ) ;
51135183 }
51145184
5115- if let DataBlock :: Dictionary ( dict) = data_block {
5116- log:: debug!( "Encoding column {} with {} items using dictionary encoding (already dictionary encoded)" , column_idx, num_values) ;
5117- let ( mut indices_data_block, dictionary_data_block) = dict. into_parts ( ) ;
5118- // TODO: https://github.com/lancedb/lance/issues/4809
5119- // If we compute stats on dictionary_data_block => panic.
5120- // If we don't compute stats on indices_data_block => panic.
5121- // This is messy. Don't make me call compute_stat ever.
5122- indices_data_block. compute_stat ( ) ;
5123- Self :: encode_miniblock (
5124- column_idx,
5125- & field,
5126- compression_strategy. as_ref ( ) ,
5127- indices_data_block,
5128- repdef,
5129- row_number,
5130- Some ( dictionary_data_block) ,
5131- num_rows,
5132- support_large_chunk,
5133- )
5185+ // If the rep/def levels are too sparse for miniblock (e.g. many empty
5186+ // lists with very few values), fall back to fullzip to avoid exceeding
5187+ // the u16 per-chunk rep/def buffer size limit.
5188+ let too_sparse = Self :: repdef_too_sparse_for_miniblock ( & repdef, num_values) ;
5189+
5190+ if !too_sparse {
5191+ if let DataBlock :: Dictionary ( dict) = data_block {
5192+ log:: debug!( "Encoding column {} with {} items using dictionary encoding (already dictionary encoded)" , column_idx, num_values) ;
5193+ let ( mut indices_data_block, dictionary_data_block) = dict. into_parts ( ) ;
5194+ // TODO: https://github.com/lancedb/lance/issues/4809
5195+ // If we compute stats on dictionary_data_block => panic.
5196+ // If we don't compute stats on indices_data_block => panic.
5197+ // This is messy. Don't make me call compute_stat ever.
5198+ indices_data_block. compute_stat ( ) ;
5199+ return Self :: encode_miniblock (
5200+ column_idx,
5201+ & field,
5202+ compression_strategy. as_ref ( ) ,
5203+ indices_data_block,
5204+ repdef,
5205+ row_number,
5206+ Some ( dictionary_data_block) ,
5207+ num_rows,
5208+ support_large_chunk,
5209+ ) ;
5210+ }
51345211 } else {
5212+ log:: debug!(
5213+ "Encoding column {} with {} items using full-zip layout \
5214+ (rep/def too sparse for mini-block)",
5215+ column_idx,
5216+ num_values
5217+ ) ;
5218+ }
5219+
5220+ {
51355221 // Try dictionary encoding first if applicable. If encoding aborts, fall back to the
51365222 // preferred structural encoding.
5137- let dict_result = Self :: should_dictionary_encode ( & data_block, & field, version)
5138- . and_then ( |budget| {
5139- log:: debug!(
5140- "Encoding column {} with {} items using dictionary encoding (mini-block layout)" ,
5141- column_idx,
5142- num_values
5143- ) ;
5144- dict:: dictionary_encode (
5145- & data_block,
5146- budget. max_dict_entries ,
5147- budget. max_encoded_size ,
5148- )
5149- } ) ;
5223+ let dict_result = if too_sparse {
5224+ None
5225+ } else {
5226+ Self :: should_dictionary_encode ( & data_block, & field, version)
5227+ . and_then ( |budget| {
5228+ log:: debug!(
5229+ "Encoding column {} with {} items using dictionary encoding (mini-block layout)" ,
5230+ column_idx,
5231+ num_values
5232+ ) ;
5233+ dict:: dictionary_encode (
5234+ & data_block,
5235+ budget. max_dict_entries ,
5236+ budget. max_encoded_size ,
5237+ )
5238+ } )
5239+ } ;
51505240
51515241 if let Some ( ( indices_data_block, dictionary_data_block) ) = dict_result {
51525242 Self :: encode_miniblock (
@@ -5160,7 +5250,7 @@ impl PrimitiveStructuralEncoder {
51605250 num_rows,
51615251 support_large_chunk,
51625252 )
5163- } else if Self :: prefers_miniblock ( & data_block, encoding_metadata. as_ref ( ) ) {
5253+ } else if !too_sparse && Self :: prefers_miniblock ( & data_block, encoding_metadata. as_ref ( ) ) {
51645254 log:: debug!(
51655255 "Encoding column {} with {} items using mini-block layout" ,
51665256 column_idx,
@@ -5177,7 +5267,7 @@ impl PrimitiveStructuralEncoder {
51775267 num_rows,
51785268 support_large_chunk,
51795269 )
5180- } else if Self :: prefers_fullzip ( encoding_metadata. as_ref ( ) ) {
5270+ } else if too_sparse || Self :: prefers_fullzip ( encoding_metadata. as_ref ( ) ) {
51815271 log:: debug!(
51825272 "Encoding column {} with {} items using full-zip layout" ,
51835273 column_idx,
0 commit comments