Skip to main content

lance_encoding/encodings/physical/
bitpacking.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Bitpacking encodings
5//!
6//! These encodings look for unused higher order bits and discard them.  For example, if we
7//! have a u32 array and all values are between 0 and 5000 then we only need 12 bits to store
8//! each value.  The encoding will discard the upper 20 bits and only store 12 bits.
9//!
10//! This is a simple encoding that works well for data that has a small range.
11//!
12//! In order to decode the values we need to know the bit width of the values.  This can be stored
13//! inline with the data (miniblock) or in the encoding description, out of line (full zip).
14//!
15//! The encoding is transparent because the output has a fixed width (just like the input) and
16//! we can easily jump to the correct value.
17
18use arrow_array::types::UInt64Type;
19use arrow_array::{Array, PrimitiveArray};
20use arrow_buffer::ArrowNativeType;
21use lance_bitpacking::BitPackingUninit;
22
23use lance_core::{Error, Result};
24
25use crate::buffer::LanceBuffer;
26use crate::compression::{BlockCompressor, BlockDecompressor, MiniBlockDecompressor};
27use crate::data::BlockInfo;
28use crate::data::{DataBlock, FixedWidthDataBlock};
29use crate::encodings::logical::primitive::miniblock::{
30    MiniBlockChunk, MiniBlockCompressed, MiniBlockCompressionContext, MiniBlockCompressor,
31};
32use crate::format::pb21::CompressiveEncoding;
33use crate::format::{ProtobufUtils21, pb21};
34use crate::statistics::{GetStat, Stat};
35use bytemuck::Pod;
36
37pub(crate) const LOG_ELEMS_PER_CHUNK: u8 = 10;
38/// Number of values encoded in each inline bitpacking chunk.
39pub const ELEMS_PER_CHUNK: u64 = 1 << LOG_ELEMS_PER_CHUNK;
40
41#[derive(Debug, Default)]
42pub struct InlineBitpacking {
43    uncompressed_bit_width: u64,
44}
45
46impl InlineBitpacking {
47    pub fn new(uncompressed_bit_width: u64) -> Self {
48        Self {
49            uncompressed_bit_width,
50        }
51    }
52
53    pub fn from_description(description: &pb21::InlineBitpacking) -> Self {
54        Self {
55            uncompressed_bit_width: description.uncompressed_bits_per_value,
56        }
57    }
58
59    /// The minimum number of bytes required to actually get compression
60    ///
61    /// We have to compress in blocks of 1024 values.  For example, we can compress 500 2-byte (1000 bytes)
62    /// values into 1024 2-bit values (256 bytes) for a win but we don't want to compress 10 2-byte values
63    /// into 1024 2-bit values because that's not a win.
64    pub fn min_size_bytes(compressed_bit_width: u64) -> u64 {
65        (ELEMS_PER_CHUNK * compressed_bit_width).div_ceil(8)
66    }
67
68    /// Bitpacks a FixedWidthDataBlock into compressed chunks of 1024 values
69    ///
70    /// Each chunk can have a different bit width
71    ///
72    /// Each chunk has the compressed bit width stored inline in the chunk itself.
73    fn bitpack_chunked<T: ArrowNativeType + BitPackingUninit>(
74        data: FixedWidthDataBlock,
75    ) -> MiniBlockCompressed {
76        debug_assert!(data.num_values > 0);
77        let data_buffer = data.data.borrow_to_typed_slice::<T>();
78        let data_buffer = data_buffer.as_ref();
79
80        let bit_widths = data.expect_stat(Stat::BitWidth);
81        let bit_widths_array = bit_widths
82            .as_any()
83            .downcast_ref::<PrimitiveArray<UInt64Type>>()
84            .unwrap();
85
86        let (packed_chunk_sizes, total_size) = bit_widths_array
87            .values()
88            .iter()
89            .map(|&bit_width| {
90                let chunk_size = ((1024 * bit_width) / data.bits_per_value) as usize;
91                (chunk_size, chunk_size + 1)
92            })
93            .fold(
94                (Vec::with_capacity(bit_widths_array.len()), 0),
95                |(mut sizes, total), (size, inc)| {
96                    sizes.push(size);
97                    (sizes, total + inc)
98                },
99            );
100
101        let mut output: Vec<T> = Vec::with_capacity(total_size);
102        let mut chunks = Vec::with_capacity(bit_widths_array.len());
103
104        for (i, packed_chunk_size) in packed_chunk_sizes
105            .iter()
106            .enumerate()
107            .take(bit_widths_array.len() - 1)
108        {
109            let start_elem = i * ELEMS_PER_CHUNK as usize;
110            let bit_width = bit_widths_array.value(i) as usize;
111            output.push(T::from_usize(bit_width).unwrap());
112            let output_len = output.len();
113            unsafe {
114                BitPackingUninit::unchecked_pack_uninit(
115                    bit_width,
116                    &data_buffer[start_elem..][..ELEMS_PER_CHUNK as usize],
117                    &mut output.spare_capacity_mut()[..*packed_chunk_size],
118                );
119                // The bitpacking kernel initialized every reserved output word.
120                output.set_len(output_len + *packed_chunk_size);
121            }
122            chunks.push(MiniBlockChunk {
123                buffer_sizes: vec![((1 + *packed_chunk_size) * std::mem::size_of::<T>()) as u32],
124                log_num_values: LOG_ELEMS_PER_CHUNK,
125            });
126        }
127
128        // Handle the last chunk
129        let last_chunk_elem_num = if data.num_values.is_multiple_of(ELEMS_PER_CHUNK) {
130            ELEMS_PER_CHUNK
131        } else {
132            data.num_values % ELEMS_PER_CHUNK
133        };
134        let mut last_chunk: Vec<T> = vec![T::from_usize(0).unwrap(); ELEMS_PER_CHUNK as usize];
135        last_chunk[..last_chunk_elem_num as usize].clone_from_slice(
136            &data_buffer[data.num_values as usize - last_chunk_elem_num as usize..],
137        );
138        let bit_width = bit_widths_array.value(bit_widths_array.len() - 1) as usize;
139        output.push(T::from_usize(bit_width).unwrap());
140        let output_len = output.len();
141        let packed_chunk_size = packed_chunk_sizes[bit_widths_array.len() - 1];
142        unsafe {
143            BitPackingUninit::unchecked_pack_uninit(
144                bit_width,
145                &last_chunk,
146                &mut output.spare_capacity_mut()[..packed_chunk_size],
147            );
148            // The bitpacking kernel initialized every reserved output word.
149            output.set_len(output_len + packed_chunk_size);
150        }
151        chunks.push(MiniBlockChunk {
152            buffer_sizes: vec![
153                ((1 + packed_chunk_sizes[bit_widths_array.len() - 1]) * std::mem::size_of::<T>())
154                    as u32,
155            ],
156            log_num_values: 0,
157        });
158
159        MiniBlockCompressed {
160            data: vec![LanceBuffer::reinterpret_vec(output)],
161            chunks,
162            num_values: data.num_values,
163        }
164    }
165
166    fn chunk_data(&self, data: FixedWidthDataBlock) -> (MiniBlockCompressed, CompressiveEncoding) {
167        assert!(data.bits_per_value.is_multiple_of(8));
168        assert_eq!(data.bits_per_value, self.uncompressed_bit_width);
169        let bits_per_value = data.bits_per_value;
170        let compressed = match bits_per_value {
171            8 => Self::bitpack_chunked::<u8>(data),
172            16 => Self::bitpack_chunked::<u16>(data),
173            32 => Self::bitpack_chunked::<u32>(data),
174            64 => Self::bitpack_chunked::<u64>(data),
175            _ => unreachable!(),
176        };
177        (
178            compressed,
179            ProtobufUtils21::inline_bitpacking(
180                bits_per_value,
181                // TODO: Could potentially compress the data here
182                None,
183            ),
184        )
185    }
186
187    fn unchunk<T: ArrowNativeType + BitPackingUninit + Pod>(
188        data: LanceBuffer,
189        num_values: u64,
190    ) -> Result<DataBlock> {
191        // This macro decompresses a chunk(1024 values) of bitpacked values.
192        let uncompressed_bit_width = std::mem::size_of::<T>() * 8;
193        let word_size = std::mem::size_of::<T>();
194
195        if data.len() < word_size {
196            return Err(Error::corrupt_file_named(
197                "inline_bitpacking",
198                format!(
199                    "Inline bitpacking chunk is too small for {}-byte header: {} bytes",
200                    word_size,
201                    data.len()
202                ),
203            ));
204        }
205        if !data.len().is_multiple_of(word_size) {
206            return Err(Error::corrupt_file_named(
207                "inline_bitpacking",
208                format!(
209                    "Inline bitpacking chunk size must be a multiple of {} bytes, got {} bytes",
210                    word_size,
211                    data.len()
212                ),
213            ));
214        }
215        if num_values > ELEMS_PER_CHUNK {
216            return Err(Error::corrupt_file_named(
217                "inline_bitpacking",
218                format!(
219                    "Inline bitpacking chunk has {} values, expected at most {}",
220                    num_values, ELEMS_PER_CHUNK
221                ),
222            ));
223        }
224
225        let chunk_words = data.borrow_to_typed_view::<T>();
226        let bit_width_value = chunk_words[0].as_usize();
227        if bit_width_value > uncompressed_bit_width {
228            return Err(Error::corrupt_file_named(
229                "inline_bitpacking",
230                format!(
231                    "Inline bitpacking width {} exceeds {}-bit values",
232                    bit_width_value, uncompressed_bit_width
233                ),
234            ));
235        }
236        let chunk = &chunk_words[1..];
237        // bit_width_value has already been verified to be <= uncompressed_bit_width
238        // (8/16/32/64), so bit_width_value * ELEMS_PER_CHUNK (1024) can never
239        // overflow usize on supported targets. Keep checked_mul as defense in depth.
240        let expected_num_bits = bit_width_value
241            .checked_mul(ELEMS_PER_CHUNK as usize)
242            .ok_or_else(|| {
243                Error::corrupt_file_named(
244                    "inline_bitpacking",
245                    format!(
246                        "Inline bitpacking width {} overflows chunk bit count",
247                        bit_width_value
248                    ),
249                )
250            })?;
251        let expected_num_bytes = expected_num_bits / 8;
252        let actual_num_bytes = std::mem::size_of_val(chunk);
253        if actual_num_bytes != expected_num_bytes {
254            return Err(Error::corrupt_file_named(
255                "inline_bitpacking",
256                format!(
257                    "Inline bitpacking payload has {} bytes, expected {} bytes for bit width {}",
258                    actual_num_bytes, expected_num_bytes, bit_width_value
259                ),
260            ));
261        }
262
263        let mut decompressed = Vec::with_capacity(ELEMS_PER_CHUNK as usize);
264        unsafe {
265            BitPackingUninit::unchecked_unpack_uninit(
266                bit_width_value,
267                chunk,
268                &mut decompressed.spare_capacity_mut()[..ELEMS_PER_CHUNK as usize],
269            );
270            // The bitpacking kernel initialized all 1024 decoded values.
271            decompressed.set_len(ELEMS_PER_CHUNK as usize);
272        }
273
274        decompressed.truncate(num_values as usize);
275        Ok(DataBlock::FixedWidth(FixedWidthDataBlock {
276            data: LanceBuffer::reinterpret_vec(decompressed),
277            bits_per_value: uncompressed_bit_width as u64,
278            num_values,
279            block_info: BlockInfo::new(),
280        }))
281    }
282
283    /// An empty fixed-width block, used for the `num_values == 0` short-circuit in
284    /// both decompressor entry points so empty blocks skip chunk validation entirely.
285    fn empty_block(&self) -> DataBlock {
286        DataBlock::FixedWidth(FixedWidthDataBlock {
287            data: LanceBuffer::empty(),
288            bits_per_value: self.uncompressed_bit_width,
289            num_values: 0,
290            block_info: BlockInfo::new(),
291        })
292    }
293}
294
295impl MiniBlockCompressor for InlineBitpacking {
296    fn compress(
297        &self,
298        _context: MiniBlockCompressionContext,
299        chunk: DataBlock,
300    ) -> Result<(MiniBlockCompressed, CompressiveEncoding)> {
301        match chunk {
302            DataBlock::FixedWidth(fixed_width) => Ok(self.chunk_data(fixed_width)),
303            _ => Err(Error::invalid_input_source(
304                format!(
305                    "Cannot compress a data block of type {} with BitpackMiniBlockEncoder",
306                    chunk.name()
307                )
308                .into(),
309            )),
310        }
311    }
312}
313
314impl BlockCompressor for InlineBitpacking {
315    fn compress(&self, data: DataBlock) -> Result<LanceBuffer> {
316        let fixed_width = data.as_fixed_width().unwrap();
317        let (chunked, _) = self.chunk_data(fixed_width);
318        Ok(chunked.data.into_iter().next().unwrap())
319    }
320}
321
322impl MiniBlockDecompressor for InlineBitpacking {
323    fn decompress(&self, data: Vec<LanceBuffer>, num_values: u64) -> Result<DataBlock> {
324        assert_eq!(data.len(), 1);
325        let data = data.into_iter().next().unwrap();
326        if num_values == 0 {
327            // Empty mini-blocks have no inline bit-width header to decode.
328            return Ok(self.empty_block());
329        }
330        match self.uncompressed_bit_width {
331            8 => Self::unchunk::<u8>(data, num_values),
332            16 => Self::unchunk::<u16>(data, num_values),
333            32 => Self::unchunk::<u32>(data, num_values),
334            64 => Self::unchunk::<u64>(data, num_values),
335            _ => unimplemented!("Bitpacking word size must be 8, 16, 32, or 64"),
336        }
337    }
338
339    fn decoded_size_bytes(&self, num_values: u64) -> Option<u64> {
340        num_values
341            .checked_mul(self.uncompressed_bit_width)
342            .map(|bits| bits.div_ceil(8))
343    }
344}
345
346impl BlockDecompressor for InlineBitpacking {
347    fn decompress(&self, data: LanceBuffer, num_values: u64) -> Result<DataBlock> {
348        if num_values == 0 {
349            // Empty blocks carry no inline bit-width header to decode; avoid
350            // spurious "too small for header" corrupt-file errors and mirror
351            // the MiniBlockDecompressor path. See #7794.
352            return Ok(self.empty_block());
353        }
354        match self.uncompressed_bit_width {
355            8 => Self::unchunk::<u8>(data, num_values),
356            16 => Self::unchunk::<u16>(data, num_values),
357            32 => Self::unchunk::<u32>(data, num_values),
358            64 => Self::unchunk::<u64>(data, num_values),
359            _ => unimplemented!("Bitpacking word size must be 8, 16, 32, or 64"),
360        }
361    }
362}
363
364/// Bitpacks a FixedWidthDataBlock with a given bit width.
365///
366/// Each chunk of 1024 values is packed with a constant bit width. For the tail we compare the
367/// cost of padding and packing against storing the raw values: if padding yields a smaller
368/// representation we pack; otherwise we append the raw tail.
369fn bitpack_out_of_line<T: ArrowNativeType + BitPackingUninit>(
370    data: FixedWidthDataBlock,
371    compressed_bits_per_value: usize,
372) -> LanceBuffer {
373    let data_buffer = data.data.borrow_to_typed_slice::<T>();
374    let data_buffer = data_buffer.as_ref();
375
376    let num_chunks = data_buffer.len().div_ceil(ELEMS_PER_CHUNK as usize);
377    let last_chunk_is_runt = data_buffer.len() % ELEMS_PER_CHUNK as usize != 0;
378    let words_per_chunk = (ELEMS_PER_CHUNK as usize * compressed_bits_per_value)
379        .div_ceil(data.bits_per_value as usize);
380    let mut output: Vec<T> = Vec::with_capacity(num_chunks * words_per_chunk);
381
382    let num_whole_chunks = if last_chunk_is_runt {
383        num_chunks - 1
384    } else {
385        num_chunks
386    };
387
388    // Simple case for complete chunks
389    for i in 0..num_whole_chunks {
390        let input_start = i * ELEMS_PER_CHUNK as usize;
391        let input_end = input_start + ELEMS_PER_CHUNK as usize;
392        let output_start = output.len();
393        unsafe {
394            BitPackingUninit::unchecked_pack_uninit(
395                compressed_bits_per_value,
396                &data_buffer[input_start..input_end],
397                &mut output.spare_capacity_mut()[..words_per_chunk],
398            );
399            // The bitpacking kernel initialized this complete packed chunk.
400            output.set_len(output_start + words_per_chunk);
401        }
402    }
403
404    if !last_chunk_is_runt {
405        return LanceBuffer::reinterpret_vec(output);
406    }
407
408    let last_chunk_start = num_whole_chunks * ELEMS_PER_CHUNK as usize;
409    let remaining_items = data_buffer.len() - last_chunk_start;
410
411    let uncompressed_bits = data.bits_per_value as usize;
412    let tail_bit_savings = uncompressed_bits.saturating_sub(compressed_bits_per_value);
413    let padding_cost = compressed_bits_per_value * (ELEMS_PER_CHUNK as usize - remaining_items);
414    let tail_pack_savings = tail_bit_savings.saturating_mul(remaining_items);
415    debug_assert!(remaining_items > 0, "remaining_items must be non-zero");
416    debug_assert!(tail_bit_savings > 0, "tail_bit_savings must be non-zero");
417
418    if padding_cost < tail_pack_savings {
419        // Padding buys us more than it costs: pad to 1024 values and pack them as a normal chunk.
420        let mut last_chunk: Vec<T> = vec![T::from_usize(0).unwrap(); ELEMS_PER_CHUNK as usize];
421        last_chunk[..remaining_items].copy_from_slice(&data_buffer[last_chunk_start..]);
422        let start = output.len();
423        unsafe {
424            BitPackingUninit::unchecked_pack_uninit(
425                compressed_bits_per_value,
426                &last_chunk,
427                &mut output.spare_capacity_mut()[..words_per_chunk],
428            );
429            // The bitpacking kernel initialized the padded tail chunk.
430            output.set_len(start + words_per_chunk);
431        }
432    } else {
433        // Padding would waste space; append tail values as-is.
434        output.extend_from_slice(&data_buffer[last_chunk_start..]);
435    }
436
437    LanceBuffer::reinterpret_vec(output)
438}
439
440/// Unpacks a FixedWidthDataBlock that has been bitpacked with a constant bit width.
441///
442/// The compressed bit width is provided while the uncompressed width comes from `T`.
443/// Depending on the encoding decision the final chunk may be fully packed (with padding)
444/// or stored as raw tail values. We infer the layout from the buffer length.
445fn unpack_out_of_line<T: ArrowNativeType + BitPackingUninit>(
446    data: FixedWidthDataBlock,
447    num_values: usize,
448    compressed_bits_per_value: usize,
449) -> FixedWidthDataBlock {
450    let words_per_chunk = (ELEMS_PER_CHUNK as usize * compressed_bits_per_value)
451        .div_ceil(data.bits_per_value as usize);
452    let compressed_words = data.data.borrow_to_typed_slice::<T>();
453
454    let num_whole_chunks = num_values / ELEMS_PER_CHUNK as usize;
455    let tail_values = num_values % ELEMS_PER_CHUNK as usize;
456    let expected_full_words = num_whole_chunks * words_per_chunk;
457    let expected_new_len = expected_full_words + tail_values;
458    let tail_is_raw = tail_values > 0 && compressed_words.len() == expected_new_len;
459
460    let extra_tail_capacity = ELEMS_PER_CHUNK as usize;
461    let mut decompressed: Vec<T> =
462        Vec::with_capacity(num_values.saturating_add(extra_tail_capacity));
463
464    for chunk_idx in 0..num_whole_chunks {
465        let input_start = chunk_idx * words_per_chunk;
466        let input_end = input_start + words_per_chunk;
467        let output_start = decompressed.len();
468        unsafe {
469            BitPackingUninit::unchecked_unpack_uninit(
470                compressed_bits_per_value,
471                &compressed_words[input_start..input_end],
472                &mut decompressed.spare_capacity_mut()[..ELEMS_PER_CHUNK as usize],
473            );
474            // The bitpacking kernel initialized this complete decoded chunk.
475            decompressed.set_len(output_start + ELEMS_PER_CHUNK as usize);
476        }
477    }
478
479    if tail_values > 0 {
480        // The tail might be padded and bit packed or it might be appended raw.  We infer the
481        // layout from the buffer length to decode appropriately.
482        if tail_is_raw {
483            let tail_start = expected_full_words;
484            decompressed.extend_from_slice(&compressed_words[tail_start..tail_start + tail_values]);
485        } else {
486            let tail_start = expected_full_words;
487            let output_start = decompressed.len();
488            unsafe {
489                BitPackingUninit::unchecked_unpack_uninit(
490                    compressed_bits_per_value,
491                    &compressed_words[tail_start..tail_start + words_per_chunk],
492                    &mut decompressed.spare_capacity_mut()[..ELEMS_PER_CHUNK as usize],
493                );
494                // The kernel initialized a full chunk; only the requested tail stays visible.
495                decompressed.set_len(output_start + ELEMS_PER_CHUNK as usize);
496            }
497            decompressed.truncate(output_start + tail_values);
498        }
499    }
500
501    debug_assert_eq!(decompressed.len(), num_values);
502
503    FixedWidthDataBlock {
504        data: LanceBuffer::reinterpret_vec(decompressed),
505        bits_per_value: data.bits_per_value,
506        num_values: num_values as u64,
507        block_info: BlockInfo::new(),
508    }
509}
510
511/// A transparent compressor that bit packs data
512///
513/// In order for the encoding to be transparent we must have a fixed bit width
514/// across the entire array.  Chunking within the buffer is not supported.  This
515/// means that we will be slightly less efficient than something like the mini-block
516/// approach.
517///
518/// This was an interesting experiment but it can't be used as a per-value compressor
519/// at the moment.  The resulting data IS transparent but it's not quite so simple.  We
520/// compress in blocks of 1024 and each block has a fixed size but also has some padding.
521///
522/// We do use this as a block compressor currently.
523///
524/// In other words, if we try the simple math to access the item at index `i` we will be
525/// out of luck because `bits_per_value * i` is not the location.  What we need is something
526/// like:
527///
528/// ```ignore
529/// let chunk_idx = i / 1024;
530/// let chunk_offset = i % 1024;
531/// bits_per_chunk * chunk_idx + bits_per_value * chunk_offset
532/// ```
533///
534/// However, this logic isn't expressible with the per-value traits we have today.  We can
535/// enhance these traits should we need to support it at some point in the future.
536#[derive(Debug)]
537pub struct OutOfLineBitpacking {
538    compressed_bit_width: u64,
539    uncompressed_bit_width: u64,
540}
541
542impl OutOfLineBitpacking {
543    pub fn new(compressed_bit_width: u64, uncompressed_bit_width: u64) -> Self {
544        Self {
545            compressed_bit_width,
546            uncompressed_bit_width,
547        }
548    }
549}
550
551impl BlockCompressor for OutOfLineBitpacking {
552    fn compress(&self, data: DataBlock) -> Result<LanceBuffer> {
553        let fixed_width = data.as_fixed_width().unwrap();
554        let compressed = match fixed_width.bits_per_value {
555            8 => bitpack_out_of_line::<u8>(fixed_width, self.compressed_bit_width as usize),
556            16 => bitpack_out_of_line::<u16>(fixed_width, self.compressed_bit_width as usize),
557            32 => bitpack_out_of_line::<u32>(fixed_width, self.compressed_bit_width as usize),
558            64 => bitpack_out_of_line::<u64>(fixed_width, self.compressed_bit_width as usize),
559            _ => panic!("Bitpacking word size must be 8,16,32,64"),
560        };
561        Ok(compressed)
562    }
563}
564
565impl BlockDecompressor for OutOfLineBitpacking {
566    fn decompress(&self, data: LanceBuffer, num_values: u64) -> Result<DataBlock> {
567        let word_size = match self.uncompressed_bit_width {
568            8 => std::mem::size_of::<u8>(),
569            16 => std::mem::size_of::<u16>(),
570            32 => std::mem::size_of::<u32>(),
571            64 => std::mem::size_of::<u64>(),
572            _ => panic!("Bitpacking word size must be 8,16,32,64"),
573        };
574        debug_assert_eq!(data.len() % word_size, 0);
575        let total_words = (data.len() / word_size) as u64;
576        let block = FixedWidthDataBlock {
577            data,
578            bits_per_value: self.uncompressed_bit_width,
579            num_values: total_words,
580            block_info: BlockInfo::new(),
581        };
582
583        let unpacked = match self.uncompressed_bit_width {
584            8 => unpack_out_of_line::<u8>(
585                block,
586                num_values as usize,
587                self.compressed_bit_width as usize,
588            ),
589            16 => unpack_out_of_line::<u16>(
590                block,
591                num_values as usize,
592                self.compressed_bit_width as usize,
593            ),
594            32 => unpack_out_of_line::<u32>(
595                block,
596                num_values as usize,
597                self.compressed_bit_width as usize,
598            ),
599            64 => unpack_out_of_line::<u64>(
600                block,
601                num_values as usize,
602                self.compressed_bit_width as usize,
603            ),
604            _ => unreachable!(),
605        };
606        Ok(DataBlock::FixedWidth(unpacked))
607    }
608}
609
610#[cfg(test)]
611mod test {
612    use std::{collections::HashMap, sync::Arc};
613
614    use arrow_array::{Array, Int8Array, Int64Array};
615    use arrow_buffer::ArrowNativeType;
616    use arrow_schema::DataType;
617    use bytemuck::Pod;
618    use lance_bitpacking::{BitPacking, BitPackingUninit};
619    use rstest::rstest;
620
621    use super::{ELEMS_PER_CHUNK, InlineBitpacking, bitpack_out_of_line, unpack_out_of_line};
622    use crate::{
623        buffer::LanceBuffer,
624        compression::{BlockDecompressor, MiniBlockDecompressor},
625        data::{BlockInfo, DataBlock, FixedWidthDataBlock},
626        testing::{TestCases, check_round_trip_encoding_of_data},
627    };
628
629    #[rstest]
630    #[case::u8(8)]
631    #[case::u16(16)]
632    #[case::u32(32)]
633    #[case::u64(64)]
634    fn test_inline_bitpacking_decompress_empty_miniblock(#[case] bit_width: u64) {
635        let decompressor = InlineBitpacking::new(bit_width);
636        let decompressed =
637            MiniBlockDecompressor::decompress(&decompressor, vec![LanceBuffer::empty()], 0)
638                .unwrap();
639
640        let DataBlock::FixedWidth(block) = decompressed else {
641            panic!("Expected FixedWidth block");
642        };
643        assert_eq!(block.bits_per_value, bit_width);
644        assert_eq!(block.num_values, 0);
645        assert_eq!(block.data.len(), 0);
646    }
647
648    // Regression test for #7794: the block-level decompressor must short-circuit
649    // on num_values == 0 the same way the mini-block decompressor does, instead
650    // of reporting a spurious "too small for header" corrupt-file error.
651    #[rstest]
652    #[case::u8(8)]
653    #[case::u16(16)]
654    #[case::u32(32)]
655    #[case::u64(64)]
656    fn test_inline_bitpacking_decompress_empty_block(#[case] bit_width: u64) {
657        let decompressor = InlineBitpacking::new(bit_width);
658        let decompressed =
659            BlockDecompressor::decompress(&decompressor, LanceBuffer::empty(), 0).unwrap();
660
661        let DataBlock::FixedWidth(block) = decompressed else {
662            panic!("Expected FixedWidth block");
663        };
664        assert_eq!(block.bits_per_value, bit_width);
665        assert_eq!(block.num_values, 0);
666        assert_eq!(block.data.len(), 0);
667    }
668
669    fn roundtrip_unchunk<T>(values: &[T], bit_width: usize)
670    where
671        T: ArrowNativeType + BitPackingUninit + Pod,
672    {
673        assert!(values.len() <= ELEMS_PER_CHUNK as usize);
674        let num_values = values.len() as u64;
675
676        let mut padded = vec![T::from_usize(0).unwrap(); ELEMS_PER_CHUNK as usize];
677        padded[..values.len()].copy_from_slice(values);
678
679        let packed_words = ELEMS_PER_CHUNK as usize * bit_width / (std::mem::size_of::<T>() * 8);
680        let mut chunk: Vec<T> = Vec::with_capacity(1 + packed_words);
681        chunk.push(T::from_usize(bit_width).unwrap());
682        let out_len = chunk.len();
683        chunk.resize(out_len + packed_words, T::from_usize(0).unwrap());
684        unsafe {
685            BitPacking::unchecked_pack(bit_width, &padded, &mut chunk[out_len..]);
686        }
687
688        let data = LanceBuffer::reinterpret_vec(chunk);
689        let decoded = InlineBitpacking::unchunk::<T>(data, num_values).unwrap();
690        let DataBlock::FixedWidth(fixed) = decoded else {
691            panic!("expected FixedWidth DataBlock");
692        };
693        let decoded_values = fixed.data.borrow_to_typed_view::<T>();
694        assert_eq!(decoded_values.as_ref(), values);
695    }
696
697    fn assert_corrupt_unchunk<T>(data: LanceBuffer, num_values: u64, expected_message: &str)
698    where
699        T: ArrowNativeType + BitPackingUninit + Pod,
700    {
701        let err = InlineBitpacking::unchunk::<T>(data, num_values).unwrap_err();
702        assert!(matches!(err, lance_core::Error::CorruptFile { .. }));
703        let err = err.to_string();
704        assert!(
705            err.contains(expected_message),
706            "expected error containing {expected_message:?}, got {err:?}"
707        );
708    }
709
710    #[test]
711    fn unchunk_u32_bw12_tail() {
712        let values: Vec<u32> = (0..500).map(|i| ((i * 7) % (1 << 12)) as u32).collect();
713        roundtrip_unchunk(&values, 12);
714    }
715
716    #[test]
717    fn unchunk_u64_bw23_full() {
718        let values: Vec<u64> = (0..1024).map(|i| ((i * 3) % (1 << 23)) as u64).collect();
719        roundtrip_unchunk(&values, 23);
720    }
721
722    #[rstest]
723    #[case::too_small_header(LanceBuffer::from(vec![1, 2, 3]), 1, "too small")]
724    #[case::misaligned_chunk_size(LanceBuffer::from(vec![0, 0, 0, 0, 0]), 1, "multiple")]
725    #[case::too_many_values(
726        LanceBuffer::reinterpret_vec(vec![0_u32]),
727        ELEMS_PER_CHUNK + 1,
728        "expected at most"
729    )]
730    #[case::payload_size_mismatch(LanceBuffer::reinterpret_vec(vec![12_u32]), 1, "payload")]
731    #[case::invalid_bit_width(LanceBuffer::reinterpret_vec(vec![33_u32]), 1, "exceeds")]
732    fn unchunk_rejects(
733        #[case] data: LanceBuffer,
734        #[case] num_values: u64,
735        #[case] expected_message: &str,
736    ) {
737        assert_corrupt_unchunk::<u32>(data, num_values, expected_message);
738    }
739
740    #[test_log::test(tokio::test)]
741    async fn test_miniblock_bitpack() {
742        let test_cases = TestCases::default().with_structural_encodings();
743
744        let arrays = vec![
745            Arc::new(Int8Array::from(vec![100; 1024])) as Arc<dyn Array>,
746            Arc::new(Int8Array::from(vec![1; 1024])) as Arc<dyn Array>,
747            Arc::new(Int8Array::from(vec![16; 1024])) as Arc<dyn Array>,
748            Arc::new(Int8Array::from(vec![-1; 1024])) as Arc<dyn Array>,
749            Arc::new(Int8Array::from(vec![5; 1])) as Arc<dyn Array>,
750        ];
751        check_round_trip_encoding_of_data(arrays, &test_cases, HashMap::new()).await;
752
753        for data_type in [DataType::Int16, DataType::Int32, DataType::Int64] {
754            let int64_arrays = vec![
755                Int64Array::from(vec![3; 1024]),
756                Int64Array::from(vec![8; 1024]),
757                Int64Array::from(vec![16; 1024]),
758                Int64Array::from(vec![100; 1024]),
759                Int64Array::from(vec![512; 1024]),
760                Int64Array::from(vec![1000; 1024]),
761                Int64Array::from(vec![2000; 1024]),
762                Int64Array::from(vec![-1; 10]),
763            ];
764
765            let mut arrays = vec![];
766            for int64_array in int64_arrays {
767                arrays.push(arrow_cast::cast(&int64_array, &data_type).unwrap());
768            }
769
770            check_round_trip_encoding_of_data(arrays, &test_cases, HashMap::new()).await;
771        }
772    }
773
774    #[test_log::test(tokio::test)]
775    async fn test_bitpack_encoding_verification() {
776        use arrow_array::Int32Array;
777
778        // Test bitpacking encoding verification with varied small values that should trigger bitpacking
779        let test_cases = TestCases::default()
780            .with_expected_encoding("inline_bitpacking")
781            .with_structural_encodings();
782
783        // Generate data with varied small values to avoid RLE
784        // Mix different values but keep them small to trigger bitpacking
785        let mut values = Vec::new();
786        for i in 0..2048 {
787            values.push(i % 16); // Values 0-15, varied enough to avoid RLE
788        }
789
790        let arrays = vec![Arc::new(Int32Array::from(values)) as Arc<dyn Array>];
791
792        // Explicitly disable BSS to ensure bitpacking is tested
793        let mut metadata = HashMap::new();
794        metadata.insert("lance-encoding:bss".to_string(), "off".to_string());
795
796        check_round_trip_encoding_of_data(arrays, &test_cases, metadata.clone()).await;
797    }
798
799    #[test_log::test(tokio::test)]
800    async fn test_miniblock_bitpack_zero_chunk_selection() {
801        use arrow_array::Int32Array;
802
803        let test_cases = TestCases::default()
804            .with_expected_encoding("inline_bitpacking")
805            .with_structural_encodings();
806
807        // Build 2048 values: first 1024 all zeros (bit_width=0),
808        // next 1024 small varied values to avoid RLE and trigger bitpacking.
809        let mut vals = vec![0i32; 1024];
810        for i in 0..1024 {
811            vals.push(i % 16);
812        }
813
814        let arrays = vec![Arc::new(Int32Array::from(vals)) as Arc<dyn Array>];
815
816        // Disable BSS and RLE to prefer bitpacking in selection
817        let mut metadata = HashMap::new();
818        metadata.insert("lance-encoding:bss".to_string(), "off".to_string());
819        metadata.insert("lance-encoding:rle-threshold".to_string(), "0".to_string());
820
821        check_round_trip_encoding_of_data(arrays, &test_cases, metadata).await;
822    }
823
824    #[test]
825    fn test_out_of_line_bitpack_raw_tail_roundtrip() {
826        let bit_width = 8usize;
827        let word_bits = std::mem::size_of::<u32>() as u64 * 8;
828        let values: Vec<u32> = (0..1025).map(|i| (i % 200) as u32).collect();
829        let input = FixedWidthDataBlock {
830            data: LanceBuffer::reinterpret_vec(values.clone()),
831            bits_per_value: word_bits,
832            num_values: values.len() as u64,
833            block_info: BlockInfo::new(),
834        };
835
836        let compressed = bitpack_out_of_line::<u32>(input, bit_width);
837        let compressed_words = compressed.borrow_to_typed_slice::<u32>().to_vec();
838        let words_per_chunk = (ELEMS_PER_CHUNK as usize * bit_width).div_ceil(word_bits as usize);
839        assert_eq!(
840            compressed_words.len(),
841            words_per_chunk + (values.len() - ELEMS_PER_CHUNK as usize),
842        );
843
844        let compressed_block = FixedWidthDataBlock {
845            data: LanceBuffer::reinterpret_vec(compressed_words.clone()),
846            bits_per_value: word_bits,
847            num_values: compressed_words.len() as u64,
848            block_info: BlockInfo::new(),
849        };
850
851        let decoded = unpack_out_of_line::<u32>(compressed_block, values.len(), bit_width);
852        let decoded_values = decoded.data.borrow_to_typed_slice::<u32>();
853        assert_eq!(decoded_values.as_ref(), values.as_slice());
854    }
855}