1use 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;
38pub 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 pub fn min_size_bytes(compressed_bit_width: u64) -> u64 {
65 (ELEMS_PER_CHUNK * compressed_bit_width).div_ceil(8)
66 }
67
68 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 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 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 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 None,
183 ),
184 )
185 }
186
187 fn unchunk<T: ArrowNativeType + BitPackingUninit + Pod>(
188 data: LanceBuffer,
189 num_values: u64,
190 ) -> Result<DataBlock> {
191 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 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 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 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 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 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
364fn 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 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 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 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 output.set_len(start + words_per_chunk);
431 }
432 } else {
433 output.extend_from_slice(&data_buffer[last_chunk_start..]);
435 }
436
437 LanceBuffer::reinterpret_vec(output)
438}
439
440fn 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 decompressed.set_len(output_start + ELEMS_PER_CHUNK as usize);
476 }
477 }
478
479 if tail_values > 0 {
480 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 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#[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 #[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 let test_cases = TestCases::default()
780 .with_expected_encoding("inline_bitpacking")
781 .with_structural_encodings();
782
783 let mut values = Vec::new();
786 for i in 0..2048 {
787 values.push(i % 16); }
789
790 let arrays = vec![Arc::new(Int32Array::from(values)) as Arc<dyn Array>];
791
792 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 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 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}