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::{
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;
40pub 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 pub fn min_size_bytes(compressed_bit_width: u64) -> u64 {
98 (ELEMS_PER_CHUNK * compressed_bit_width).div_ceil(8)
99 }
100
101 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 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 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 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 None,
216 ),
217 )
218 }
219
220 fn unchunk<T: ArrowNativeType + BitPackingUninit + Pod>(
221 data: LanceBuffer,
222 num_values: u64,
223 ) -> Result<DataBlock> {
224 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 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 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 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 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 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
419fn 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 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 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 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 output.set_len(start + words_per_chunk);
486 }
487 } else {
488 output.extend_from_slice(&data_buffer[last_chunk_start..]);
490 }
491
492 LanceBuffer::reinterpret_vec(output)
493}
494
495fn 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 decompressed.set_len(output_start + ELEMS_PER_CHUNK as usize);
531 }
532 }
533
534 if tail_values > 0 {
535 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 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#[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 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 #[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 let test_cases = TestCases::default()
867 .with_expected_encoding("inline_bitpacking")
868 .with_structural_encodings();
869
870 let mut values = Vec::new();
873 for i in 0..2048 {
874 values.push(i % 16); }
876
877 let arrays = vec![Arc::new(Int32Array::from(values)) as Arc<dyn Array>];
878
879 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 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 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 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}