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