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 byteorder::{ByteOrder, LittleEndian};
22use lance_bitpacking::BitPacking;
23
24use lance_core::{Error, Result};
25
26use crate::buffer::LanceBuffer;
27use crate::compression::{BlockCompressor, BlockDecompressor, MiniBlockDecompressor};
28use crate::data::BlockInfo;
29use crate::data::{DataBlock, FixedWidthDataBlock};
30use crate::encodings::logical::primitive::miniblock::{
31    MiniBlockChunk, MiniBlockCompressed, MiniBlockCompressor,
32};
33use crate::format::pb21::CompressiveEncoding;
34use crate::format::{ProtobufUtils21, pb21};
35use crate::statistics::{GetStat, Stat};
36use bytemuck::{AnyBitPattern, cast_slice};
37
38const LOG_ELEMS_PER_CHUNK: u8 = 10;
39const 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 + BitPacking>(
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                output.set_len(output_len + *packed_chunk_size);
115                BitPacking::unchecked_pack(
116                    bit_width,
117                    &data_buffer[start_elem..][..ELEMS_PER_CHUNK as usize],
118                    &mut output[output_len..][..*packed_chunk_size],
119                );
120            }
121            chunks.push(MiniBlockChunk {
122                buffer_sizes: vec![((1 + *packed_chunk_size) * std::mem::size_of::<T>()) as u32],
123                log_num_values: LOG_ELEMS_PER_CHUNK,
124            });
125        }
126
127        // Handle the last chunk
128        let last_chunk_elem_num = if data.num_values.is_multiple_of(ELEMS_PER_CHUNK) {
129            ELEMS_PER_CHUNK
130        } else {
131            data.num_values % ELEMS_PER_CHUNK
132        };
133        let mut last_chunk: Vec<T> = vec![T::from_usize(0).unwrap(); ELEMS_PER_CHUNK as usize];
134        last_chunk[..last_chunk_elem_num as usize].clone_from_slice(
135            &data_buffer[data.num_values as usize - last_chunk_elem_num as usize..],
136        );
137        let bit_width = bit_widths_array.value(bit_widths_array.len() - 1) as usize;
138        output.push(T::from_usize(bit_width).unwrap());
139        let output_len = output.len();
140        unsafe {
141            output.set_len(output_len + packed_chunk_sizes[bit_widths_array.len() - 1]);
142            BitPacking::unchecked_pack(
143                bit_width,
144                &last_chunk,
145                &mut output[output_len..][..packed_chunk_sizes[bit_widths_array.len() - 1]],
146            );
147        }
148        chunks.push(MiniBlockChunk {
149            buffer_sizes: vec![
150                ((1 + packed_chunk_sizes[bit_widths_array.len() - 1]) * std::mem::size_of::<T>())
151                    as u32,
152            ],
153            log_num_values: 0,
154        });
155
156        MiniBlockCompressed {
157            data: vec![LanceBuffer::reinterpret_vec(output)],
158            chunks,
159            num_values: data.num_values,
160        }
161    }
162
163    fn chunk_data(&self, data: FixedWidthDataBlock) -> (MiniBlockCompressed, CompressiveEncoding) {
164        assert!(data.bits_per_value.is_multiple_of(8));
165        assert_eq!(data.bits_per_value, self.uncompressed_bit_width);
166        let bits_per_value = data.bits_per_value;
167        let compressed = match bits_per_value {
168            8 => Self::bitpack_chunked::<u8>(data),
169            16 => Self::bitpack_chunked::<u16>(data),
170            32 => Self::bitpack_chunked::<u32>(data),
171            64 => Self::bitpack_chunked::<u64>(data),
172            _ => unreachable!(),
173        };
174        (
175            compressed,
176            ProtobufUtils21::inline_bitpacking(
177                bits_per_value,
178                // TODO: Could potentially compress the data here
179                None,
180            ),
181        )
182    }
183
184    fn unchunk<T: ArrowNativeType + BitPacking + AnyBitPattern>(
185        data: LanceBuffer,
186        num_values: u64,
187    ) -> Result<DataBlock> {
188        // Ensure at least the header is present
189        assert!(data.len() >= std::mem::size_of::<T>());
190        assert!(num_values <= ELEMS_PER_CHUNK);
191
192        // This macro decompresses a chunk(1024 values) of bitpacked values.
193        let uncompressed_bit_width = std::mem::size_of::<T>() * 8;
194        let mut decompressed = vec![T::from_usize(0).unwrap(); ELEMS_PER_CHUNK as usize];
195
196        // Copy for memory alignment
197        let chunk_in_u8: Vec<u8> = data.to_vec();
198        let bit_width_bytes = &chunk_in_u8[..std::mem::size_of::<T>()];
199        let bit_width_value = LittleEndian::read_uint(bit_width_bytes, std::mem::size_of::<T>());
200        let chunk = cast_slice(&chunk_in_u8[std::mem::size_of::<T>()..]);
201        // The bit-packed chunk should have number of bytes (bit_width_value * ELEMS_PER_CHUNK / 8)
202        assert!(std::mem::size_of_val(chunk) == (bit_width_value * ELEMS_PER_CHUNK) as usize / 8);
203        unsafe {
204            BitPacking::unchecked_unpack(bit_width_value as usize, chunk, &mut decompressed);
205        }
206
207        decompressed.truncate(num_values as usize);
208        Ok(DataBlock::FixedWidth(FixedWidthDataBlock {
209            data: LanceBuffer::reinterpret_vec(decompressed),
210            bits_per_value: uncompressed_bit_width as u64,
211            num_values,
212            block_info: BlockInfo::new(),
213        }))
214    }
215}
216
217impl MiniBlockCompressor for InlineBitpacking {
218    fn compress(&self, chunk: DataBlock) -> Result<(MiniBlockCompressed, CompressiveEncoding)> {
219        match chunk {
220            DataBlock::FixedWidth(fixed_width) => Ok(self.chunk_data(fixed_width)),
221            _ => Err(Error::invalid_input_source(
222                format!(
223                    "Cannot compress a data block of type {} with BitpackMiniBlockEncoder",
224                    chunk.name()
225                )
226                .into(),
227            )),
228        }
229    }
230}
231
232impl BlockCompressor for InlineBitpacking {
233    fn compress(&self, data: DataBlock) -> Result<LanceBuffer> {
234        let fixed_width = data.as_fixed_width().unwrap();
235        let (chunked, _) = self.chunk_data(fixed_width);
236        Ok(chunked.data.into_iter().next().unwrap())
237    }
238}
239
240impl MiniBlockDecompressor for InlineBitpacking {
241    fn decompress(&self, data: Vec<LanceBuffer>, num_values: u64) -> Result<DataBlock> {
242        assert_eq!(data.len(), 1);
243        let data = data.into_iter().next().unwrap();
244        if num_values == 0 {
245            // Empty mini-blocks have no inline bit-width header to decode.
246            return Ok(DataBlock::FixedWidth(FixedWidthDataBlock {
247                data: LanceBuffer::empty(),
248                bits_per_value: self.uncompressed_bit_width,
249                num_values: 0,
250                block_info: BlockInfo::new(),
251            }));
252        }
253        match self.uncompressed_bit_width {
254            8 => Self::unchunk::<u8>(data, num_values),
255            16 => Self::unchunk::<u16>(data, num_values),
256            32 => Self::unchunk::<u32>(data, num_values),
257            64 => Self::unchunk::<u64>(data, num_values),
258            _ => unimplemented!("Bitpacking word size must be 8, 16, 32, or 64"),
259        }
260    }
261}
262
263impl BlockDecompressor for InlineBitpacking {
264    fn decompress(&self, data: LanceBuffer, num_values: u64) -> Result<DataBlock> {
265        match self.uncompressed_bit_width {
266            8 => Self::unchunk::<u8>(data, num_values),
267            16 => Self::unchunk::<u16>(data, num_values),
268            32 => Self::unchunk::<u32>(data, num_values),
269            64 => Self::unchunk::<u64>(data, num_values),
270            _ => unimplemented!("Bitpacking word size must be 8, 16, 32, or 64"),
271        }
272    }
273}
274
275/// Bitpacks a FixedWidthDataBlock with a given bit width.
276///
277/// Each chunk of 1024 values is packed with a constant bit width. For the tail we compare the
278/// cost of padding and packing against storing the raw values: if padding yields a smaller
279/// representation we pack; otherwise we append the raw tail.
280fn bitpack_out_of_line<T: ArrowNativeType + BitPacking>(
281    data: FixedWidthDataBlock,
282    compressed_bits_per_value: usize,
283) -> LanceBuffer {
284    let data_buffer = data.data.borrow_to_typed_slice::<T>();
285    let data_buffer = data_buffer.as_ref();
286
287    let num_chunks = data_buffer.len().div_ceil(ELEMS_PER_CHUNK as usize);
288    let last_chunk_is_runt = data_buffer.len() % ELEMS_PER_CHUNK as usize != 0;
289    let words_per_chunk = (ELEMS_PER_CHUNK as usize * compressed_bits_per_value)
290        .div_ceil(data.bits_per_value as usize);
291    #[allow(clippy::uninit_vec)]
292    let mut output: Vec<T> = Vec::with_capacity(num_chunks * words_per_chunk);
293    #[allow(clippy::uninit_vec)]
294    unsafe {
295        output.set_len(num_chunks * words_per_chunk);
296    }
297
298    let num_whole_chunks = if last_chunk_is_runt {
299        num_chunks - 1
300    } else {
301        num_chunks
302    };
303
304    // Simple case for complete chunks
305    for i in 0..num_whole_chunks {
306        let input_start = i * ELEMS_PER_CHUNK as usize;
307        let input_end = input_start + ELEMS_PER_CHUNK as usize;
308        let output_start = i * words_per_chunk;
309        let output_end = output_start + words_per_chunk;
310        unsafe {
311            BitPacking::unchecked_pack(
312                compressed_bits_per_value,
313                &data_buffer[input_start..input_end],
314                &mut output[output_start..output_end],
315            );
316        }
317    }
318
319    if !last_chunk_is_runt {
320        return LanceBuffer::reinterpret_vec(output);
321    }
322
323    let last_chunk_start = num_whole_chunks * ELEMS_PER_CHUNK as usize;
324    // Safety: output ensures to have those values.
325    unsafe {
326        output.set_len(num_whole_chunks * words_per_chunk);
327    }
328    let remaining_items = data_buffer.len() - last_chunk_start;
329
330    let uncompressed_bits = data.bits_per_value as usize;
331    let tail_bit_savings = uncompressed_bits.saturating_sub(compressed_bits_per_value);
332    let padding_cost = compressed_bits_per_value * (ELEMS_PER_CHUNK as usize - remaining_items);
333    let tail_pack_savings = tail_bit_savings.saturating_mul(remaining_items);
334    debug_assert!(remaining_items > 0, "remaining_items must be non-zero");
335    debug_assert!(tail_bit_savings > 0, "tail_bit_savings must be non-zero");
336
337    if padding_cost < tail_pack_savings {
338        // Padding buys us more than it costs: pad to 1024 values and pack them as a normal chunk.
339        let mut last_chunk: Vec<T> = vec![T::from_usize(0).unwrap(); ELEMS_PER_CHUNK as usize];
340        last_chunk[..remaining_items].copy_from_slice(&data_buffer[last_chunk_start..]);
341        let start = output.len();
342        unsafe {
343            // Capacity reserves a full chunk for each block; extend the visible length and fill it immediately.
344            output.set_len(start + words_per_chunk);
345            BitPacking::unchecked_pack(
346                compressed_bits_per_value,
347                &last_chunk,
348                &mut output[start..start + words_per_chunk],
349            );
350        }
351    } else {
352        // Padding would waste space; append tail values as-is.
353        output.extend_from_slice(&data_buffer[last_chunk_start..]);
354    }
355
356    LanceBuffer::reinterpret_vec(output)
357}
358
359/// Unpacks a FixedWidthDataBlock that has been bitpacked with a constant bit width.
360///
361/// The compressed bit width is provided while the uncompressed width comes from `T`.
362/// Depending on the encoding decision the final chunk may be fully packed (with padding)
363/// or stored as raw tail values. We infer the layout from the buffer length.
364fn unpack_out_of_line<T: ArrowNativeType + BitPacking>(
365    data: FixedWidthDataBlock,
366    num_values: usize,
367    compressed_bits_per_value: usize,
368) -> FixedWidthDataBlock {
369    let words_per_chunk = (ELEMS_PER_CHUNK as usize * compressed_bits_per_value)
370        .div_ceil(data.bits_per_value as usize);
371    let compressed_words = data.data.borrow_to_typed_slice::<T>();
372
373    let num_whole_chunks = num_values / ELEMS_PER_CHUNK as usize;
374    let tail_values = num_values % ELEMS_PER_CHUNK as usize;
375    let expected_full_words = num_whole_chunks * words_per_chunk;
376    let expected_new_len = expected_full_words + tail_values;
377    let tail_is_raw = tail_values > 0 && compressed_words.len() == expected_new_len;
378
379    let extra_tail_capacity = ELEMS_PER_CHUNK as usize;
380    #[allow(clippy::uninit_vec)]
381    let mut decompressed: Vec<T> =
382        Vec::with_capacity(num_values.saturating_add(extra_tail_capacity));
383    let chunk_value_len = num_whole_chunks * ELEMS_PER_CHUNK as usize;
384    unsafe {
385        decompressed.set_len(chunk_value_len);
386    }
387
388    for chunk_idx in 0..num_whole_chunks {
389        let input_start = chunk_idx * words_per_chunk;
390        let input_end = input_start + words_per_chunk;
391        let output_start = chunk_idx * ELEMS_PER_CHUNK as usize;
392        let output_end = output_start + ELEMS_PER_CHUNK as usize;
393        unsafe {
394            BitPacking::unchecked_unpack(
395                compressed_bits_per_value,
396                &compressed_words[input_start..input_end],
397                &mut decompressed[output_start..output_end],
398            );
399        }
400    }
401
402    if tail_values > 0 {
403        // The tail might be padded and bit packed or it might be appended raw.  We infer the
404        // layout from the buffer length to decode appropriately.
405        if tail_is_raw {
406            let tail_start = expected_full_words;
407            decompressed.extend_from_slice(&compressed_words[tail_start..tail_start + tail_values]);
408        } else {
409            let tail_start = expected_full_words;
410            let output_start = decompressed.len();
411            unsafe {
412                decompressed.set_len(output_start + ELEMS_PER_CHUNK as usize);
413            }
414            unsafe {
415                BitPacking::unchecked_unpack(
416                    compressed_bits_per_value,
417                    &compressed_words[tail_start..tail_start + words_per_chunk],
418                    &mut decompressed[output_start..output_start + ELEMS_PER_CHUNK as usize],
419                );
420            }
421            decompressed.truncate(output_start + tail_values);
422        }
423    }
424
425    debug_assert_eq!(decompressed.len(), num_values);
426
427    FixedWidthDataBlock {
428        data: LanceBuffer::reinterpret_vec(decompressed),
429        bits_per_value: data.bits_per_value,
430        num_values: num_values as u64,
431        block_info: BlockInfo::new(),
432    }
433}
434
435/// A transparent compressor that bit packs data
436///
437/// In order for the encoding to be transparent we must have a fixed bit width
438/// across the entire array.  Chunking within the buffer is not supported.  This
439/// means that we will be slightly less efficient than something like the mini-block
440/// approach.
441///
442/// This was an interesting experiment but it can't be used as a per-value compressor
443/// at the moment.  The resulting data IS transparent but it's not quite so simple.  We
444/// compress in blocks of 1024 and each block has a fixed size but also has some padding.
445///
446/// We do use this as a block compressor currently.
447///
448/// In other words, if we try the simple math to access the item at index `i` we will be
449/// out of luck because `bits_per_value * i` is not the location.  What we need is something
450/// like:
451///
452/// ```ignore
453/// let chunk_idx = i / 1024;
454/// let chunk_offset = i % 1024;
455/// bits_per_chunk * chunk_idx + bits_per_value * chunk_offset
456/// ```
457///
458/// However, this logic isn't expressible with the per-value traits we have today.  We can
459/// enhance these traits should we need to support it at some point in the future.
460#[derive(Debug)]
461pub struct OutOfLineBitpacking {
462    compressed_bit_width: u64,
463    uncompressed_bit_width: u64,
464}
465
466impl OutOfLineBitpacking {
467    pub fn new(compressed_bit_width: u64, uncompressed_bit_width: u64) -> Self {
468        Self {
469            compressed_bit_width,
470            uncompressed_bit_width,
471        }
472    }
473}
474
475impl BlockCompressor for OutOfLineBitpacking {
476    fn compress(&self, data: DataBlock) -> Result<LanceBuffer> {
477        let fixed_width = data.as_fixed_width().unwrap();
478        let compressed = match fixed_width.bits_per_value {
479            8 => bitpack_out_of_line::<u8>(fixed_width, self.compressed_bit_width as usize),
480            16 => bitpack_out_of_line::<u16>(fixed_width, self.compressed_bit_width as usize),
481            32 => bitpack_out_of_line::<u32>(fixed_width, self.compressed_bit_width as usize),
482            64 => bitpack_out_of_line::<u64>(fixed_width, self.compressed_bit_width as usize),
483            _ => panic!("Bitpacking word size must be 8,16,32,64"),
484        };
485        Ok(compressed)
486    }
487}
488
489impl BlockDecompressor for OutOfLineBitpacking {
490    fn decompress(&self, data: LanceBuffer, num_values: u64) -> Result<DataBlock> {
491        let word_size = match self.uncompressed_bit_width {
492            8 => std::mem::size_of::<u8>(),
493            16 => std::mem::size_of::<u16>(),
494            32 => std::mem::size_of::<u32>(),
495            64 => std::mem::size_of::<u64>(),
496            _ => panic!("Bitpacking word size must be 8,16,32,64"),
497        };
498        debug_assert_eq!(data.len() % word_size, 0);
499        let total_words = (data.len() / word_size) as u64;
500        let block = FixedWidthDataBlock {
501            data,
502            bits_per_value: self.uncompressed_bit_width,
503            num_values: total_words,
504            block_info: BlockInfo::new(),
505        };
506
507        let unpacked = match self.uncompressed_bit_width {
508            8 => unpack_out_of_line::<u8>(
509                block,
510                num_values as usize,
511                self.compressed_bit_width as usize,
512            ),
513            16 => unpack_out_of_line::<u16>(
514                block,
515                num_values as usize,
516                self.compressed_bit_width as usize,
517            ),
518            32 => unpack_out_of_line::<u32>(
519                block,
520                num_values as usize,
521                self.compressed_bit_width as usize,
522            ),
523            64 => unpack_out_of_line::<u64>(
524                block,
525                num_values as usize,
526                self.compressed_bit_width as usize,
527            ),
528            _ => unreachable!(),
529        };
530        Ok(DataBlock::FixedWidth(unpacked))
531    }
532}
533
534#[cfg(test)]
535mod test {
536    use std::{collections::HashMap, sync::Arc};
537
538    use arrow_array::{Array, Int8Array, Int64Array};
539    use arrow_schema::DataType;
540    use rstest::rstest;
541
542    use super::{ELEMS_PER_CHUNK, InlineBitpacking, bitpack_out_of_line, unpack_out_of_line};
543    use crate::{
544        buffer::LanceBuffer,
545        compression::MiniBlockDecompressor,
546        data::{BlockInfo, DataBlock, FixedWidthDataBlock},
547        testing::{TestCases, check_round_trip_encoding_of_data},
548        version::LanceFileVersion,
549    };
550
551    #[rstest]
552    #[case::u8(8)]
553    #[case::u16(16)]
554    #[case::u32(32)]
555    #[case::u64(64)]
556    fn test_inline_bitpacking_decompress_empty_miniblock(#[case] bit_width: u64) {
557        let decompressor = InlineBitpacking::new(bit_width);
558        let decompressed =
559            MiniBlockDecompressor::decompress(&decompressor, vec![LanceBuffer::empty()], 0)
560                .unwrap();
561
562        let DataBlock::FixedWidth(block) = decompressed else {
563            panic!("Expected FixedWidth block");
564        };
565        assert_eq!(block.bits_per_value, bit_width);
566        assert_eq!(block.num_values, 0);
567        assert_eq!(block.data.len(), 0);
568    }
569
570    #[test_log::test(tokio::test)]
571    async fn test_miniblock_bitpack() {
572        let test_cases = TestCases::default().with_min_file_version(LanceFileVersion::V2_1);
573
574        let arrays = vec![
575            Arc::new(Int8Array::from(vec![100; 1024])) as Arc<dyn Array>,
576            Arc::new(Int8Array::from(vec![1; 1024])) as Arc<dyn Array>,
577            Arc::new(Int8Array::from(vec![16; 1024])) as Arc<dyn Array>,
578            Arc::new(Int8Array::from(vec![-1; 1024])) as Arc<dyn Array>,
579            Arc::new(Int8Array::from(vec![5; 1])) as Arc<dyn Array>,
580        ];
581        check_round_trip_encoding_of_data(arrays, &test_cases, HashMap::new()).await;
582
583        for data_type in [DataType::Int16, DataType::Int32, DataType::Int64] {
584            let int64_arrays = vec![
585                Int64Array::from(vec![3; 1024]),
586                Int64Array::from(vec![8; 1024]),
587                Int64Array::from(vec![16; 1024]),
588                Int64Array::from(vec![100; 1024]),
589                Int64Array::from(vec![512; 1024]),
590                Int64Array::from(vec![1000; 1024]),
591                Int64Array::from(vec![2000; 1024]),
592                Int64Array::from(vec![-1; 10]),
593            ];
594
595            let mut arrays = vec![];
596            for int64_array in int64_arrays {
597                arrays.push(arrow_cast::cast(&int64_array, &data_type).unwrap());
598            }
599
600            check_round_trip_encoding_of_data(arrays, &test_cases, HashMap::new()).await;
601        }
602    }
603
604    #[test_log::test(tokio::test)]
605    async fn test_bitpack_encoding_verification() {
606        use arrow_array::Int32Array;
607
608        // Test bitpacking encoding verification with varied small values that should trigger bitpacking
609        let test_cases = TestCases::default()
610            .with_expected_encoding("inline_bitpacking")
611            .with_min_file_version(LanceFileVersion::V2_1);
612
613        // Generate data with varied small values to avoid RLE
614        // Mix different values but keep them small to trigger bitpacking
615        let mut values = Vec::new();
616        for i in 0..2048 {
617            values.push(i % 16); // Values 0-15, varied enough to avoid RLE
618        }
619
620        let arrays = vec![Arc::new(Int32Array::from(values)) as Arc<dyn Array>];
621
622        // Explicitly disable BSS to ensure bitpacking is tested
623        let mut metadata = HashMap::new();
624        metadata.insert("lance-encoding:bss".to_string(), "off".to_string());
625
626        check_round_trip_encoding_of_data(arrays, &test_cases, metadata.clone()).await;
627    }
628
629    #[test_log::test(tokio::test)]
630    async fn test_miniblock_bitpack_zero_chunk_selection() {
631        use arrow_array::Int32Array;
632
633        let test_cases = TestCases::default()
634            .with_expected_encoding("inline_bitpacking")
635            .with_min_file_version(LanceFileVersion::V2_1);
636
637        // Build 2048 values: first 1024 all zeros (bit_width=0),
638        // next 1024 small varied values to avoid RLE and trigger bitpacking.
639        let mut vals = vec![0i32; 1024];
640        for i in 0..1024 {
641            vals.push(i % 16);
642        }
643
644        let arrays = vec![Arc::new(Int32Array::from(vals)) as Arc<dyn Array>];
645
646        // Disable BSS and RLE to prefer bitpacking in selection
647        let mut metadata = HashMap::new();
648        metadata.insert("lance-encoding:bss".to_string(), "off".to_string());
649        metadata.insert("lance-encoding:rle-threshold".to_string(), "0".to_string());
650
651        check_round_trip_encoding_of_data(arrays, &test_cases, metadata).await;
652    }
653
654    #[test]
655    fn test_out_of_line_bitpack_raw_tail_roundtrip() {
656        let bit_width = 8usize;
657        let word_bits = std::mem::size_of::<u32>() as u64 * 8;
658        let values: Vec<u32> = (0..1025).map(|i| (i % 200) as u32).collect();
659        let input = FixedWidthDataBlock {
660            data: LanceBuffer::reinterpret_vec(values.clone()),
661            bits_per_value: word_bits,
662            num_values: values.len() as u64,
663            block_info: BlockInfo::new(),
664        };
665
666        let compressed = bitpack_out_of_line::<u32>(input, bit_width);
667        let compressed_words = compressed.borrow_to_typed_slice::<u32>().to_vec();
668        let words_per_chunk = (ELEMS_PER_CHUNK as usize * bit_width).div_ceil(word_bits as usize);
669        assert_eq!(
670            compressed_words.len(),
671            words_per_chunk + (values.len() - ELEMS_PER_CHUNK as usize),
672        );
673
674        let compressed_block = FixedWidthDataBlock {
675            data: LanceBuffer::reinterpret_vec(compressed_words.clone()),
676            bits_per_value: word_bits,
677            num_values: compressed_words.len() as u64,
678            block_info: BlockInfo::new(),
679        };
680
681        let decoded = unpack_out_of_line::<u32>(compressed_block, values.len(), bit_width);
682        let decoded_values = decoded.data.borrow_to_typed_slice::<u32>();
683        assert_eq!(decoded_values.as_ref(), values.as_slice());
684    }
685}