1use log::trace;
5
6use crate::{
7 Result,
8 buffer::LanceBuffer,
9 compression::MiniBlockDecompressor,
10 data::DataBlock,
11 encodings::{
12 logical::primitive::miniblock::{
13 MiniBlockCompressed, MiniBlockCompressionContext, MiniBlockCompressor,
14 },
15 physical::block::{CompressionConfig, GeneralBufferCompressor},
16 },
17 format::{ProtobufUtils21, pb21::CompressiveEncoding},
18};
19
20#[derive(Debug)]
23pub struct GeneralMiniBlockCompressor {
24 inner: Box<dyn MiniBlockCompressor>,
25 compression: CompressionConfig,
26}
27
28impl GeneralMiniBlockCompressor {
29 pub fn new(inner: Box<dyn MiniBlockCompressor>, compression: CompressionConfig) -> Self {
30 Self { inner, compression }
31 }
32}
33
34const MIN_BUFFER_SIZE_FOR_COMPRESSION: usize = 4 * 1024;
36
37use super::super::logical::primitive::miniblock::MiniBlockChunk;
38
39impl MiniBlockCompressor for GeneralMiniBlockCompressor {
40 fn compress(
41 &self,
42 context: MiniBlockCompressionContext,
43 page: DataBlock,
44 ) -> Result<(MiniBlockCompressed, CompressiveEncoding)> {
45 let (inner_compressed, inner_encoding) = self.inner.compress(context, page)?;
47
48 if inner_compressed.data.is_empty()
51 || inner_compressed.data[0].len() < MIN_BUFFER_SIZE_FOR_COMPRESSION
52 {
53 return Ok((inner_compressed, inner_encoding));
54 }
55
56 let first_buffer = &inner_compressed.data[0];
58 let mut compressed_first_buffer = Vec::new();
59 let mut new_chunks = Vec::with_capacity(inner_compressed.chunks.iter().len());
60 let mut offset = 0usize;
61 let mut total_original_size = 0usize;
62
63 let compressor = GeneralBufferCompressor::get_compressor(self.compression)?;
64
65 for chunk in &inner_compressed.chunks {
66 let chunk_first_buffer_size = chunk.buffer_sizes[0] as usize;
67
68 let chunk_data = &first_buffer.as_ref()[offset..offset + chunk_first_buffer_size];
69 total_original_size += chunk_first_buffer_size;
70
71 let compressed_start = compressed_first_buffer.len();
72 compressor.compress(chunk_data, &mut compressed_first_buffer)?;
73 let compressed_size = compressed_first_buffer.len() - compressed_start;
74
75 let mut new_buffer_sizes = chunk.buffer_sizes.clone();
77 new_buffer_sizes[0] = compressed_size as u32;
78
79 new_chunks.push(MiniBlockChunk {
80 buffer_sizes: new_buffer_sizes,
81 log_num_values: chunk.log_num_values,
82 });
83
84 offset += chunk_first_buffer_size;
85 }
86
87 let compressed_total_size = compressed_first_buffer.len();
89 if compressed_total_size >= total_original_size {
90 return Ok((inner_compressed, inner_encoding));
92 }
93
94 trace!(
95 "First buffer compressed from {} to {} bytes (ratio: {:.2})",
96 total_original_size,
97 compressed_total_size,
98 compressed_total_size as f32 / total_original_size as f32
99 );
100
101 let mut final_buffers = vec![LanceBuffer::from(compressed_first_buffer)];
103 final_buffers.extend(inner_compressed.data.into_iter().skip(1));
104
105 let compressed_result = MiniBlockCompressed {
106 data: final_buffers,
107 chunks: new_chunks,
108 num_values: inner_compressed.num_values,
109 };
110
111 let encoding = ProtobufUtils21::wrapped(self.compression, inner_encoding)?;
113 Ok((compressed_result, encoding))
114 }
115}
116
117#[derive(Debug)]
120pub struct GeneralMiniBlockDecompressor {
121 inner: Box<dyn MiniBlockDecompressor>,
122 compression: CompressionConfig,
123}
124
125impl GeneralMiniBlockDecompressor {
126 pub fn new(inner: Box<dyn MiniBlockDecompressor>, compression: CompressionConfig) -> Self {
127 Self { inner, compression }
128 }
129}
130
131impl MiniBlockDecompressor for GeneralMiniBlockDecompressor {
132 fn decompress(&self, mut data: Vec<LanceBuffer>, num_values: u64) -> Result<DataBlock> {
133 let mut decompressed_buffer = Vec::new();
134
135 let decompressor = GeneralBufferCompressor::get_compressor(self.compression)?;
136 decompressor.decompress(&data[0], &mut decompressed_buffer)?;
137 data[0] = LanceBuffer::from(decompressed_buffer);
138
139 self.inner.decompress(data, num_values)
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::compression::{DecompressionStrategy, DefaultDecompressionStrategy};
147 use crate::data::{BlockInfo, FixedWidthDataBlock};
148 use crate::encodings::physical::block::CompressionScheme;
149 use crate::encodings::physical::rle::RleEncoder;
150 use crate::encodings::physical::value::ValueEncoder;
151 use crate::format::pb21;
152 use crate::format::pb21::compressive_encoding::Compression;
153 use arrow_array::{Float64Array, Int32Array};
154
155 fn miniblock_context() -> MiniBlockCompressionContext {
156 MiniBlockCompressionContext::new(0, true, true)
157 }
158
159 #[derive(Debug)]
160 struct TestCase {
161 name: &'static str,
162 inner_encoder: Box<dyn MiniBlockCompressor>,
163 compression: CompressionConfig,
164 data: DataBlock,
165 expected_compressed: bool, min_compression_ratio: f32, }
168
169 fn create_test_cases() -> Vec<TestCase> {
170 vec![
171 TestCase {
173 name: "small_rle_data",
174 inner_encoder: Box::new(RleEncoder::new()),
175 compression: CompressionConfig {
176 scheme: CompressionScheme::Lz4,
177 level: None,
178 },
179 data: create_repeated_i32_block(vec![1, 1, 1, 1, 2, 2, 2, 2]),
180 expected_compressed: false,
181 min_compression_ratio: 1.0,
182 },
183 TestCase {
185 name: "large_rle_lz4",
186 inner_encoder: Box::new(RleEncoder::new()),
187 compression: CompressionConfig {
188 scheme: CompressionScheme::Lz4,
189 level: None,
190 },
191 data: create_pattern_i32_block(2048, |i| (i / 8) as i32),
192 expected_compressed: false, min_compression_ratio: 1.0,
194 },
195 TestCase {
197 name: "large_rle_zstd",
198 inner_encoder: Box::new(RleEncoder::new()),
199 compression: CompressionConfig {
200 scheme: CompressionScheme::Zstd,
201 level: Some(3),
202 },
203 data: create_pattern_i32_block(8192, |i| (i / 16) as i32),
204 expected_compressed: true, min_compression_ratio: 0.9, },
207 TestCase {
209 name: "sequential_value_lz4",
210 inner_encoder: Box::new(ValueEncoder {}),
211 compression: CompressionConfig {
212 scheme: CompressionScheme::Lz4,
213 level: None,
214 },
215 data: create_pattern_i32_block(1024, |i| i as i32),
216 expected_compressed: false, min_compression_ratio: 1.0,
218 },
219 TestCase {
221 name: "float_value_zstd",
222 inner_encoder: Box::new(ValueEncoder {}),
223 compression: CompressionConfig {
224 scheme: CompressionScheme::Zstd,
225 level: Some(3),
226 },
227 data: create_pattern_f64_block(1024, |i| i as f64 * 0.1),
228 expected_compressed: true,
229 min_compression_ratio: 0.9,
230 },
231 ]
232 }
233
234 fn create_repeated_i32_block(values: Vec<i32>) -> DataBlock {
235 let array = Int32Array::from(values);
236 DataBlock::from_array(array)
237 }
238
239 fn create_pattern_i32_block<F>(size: usize, pattern: F) -> DataBlock
240 where
241 F: Fn(usize) -> i32,
242 {
243 let values: Vec<i32> = (0..size).map(pattern).collect();
244 let array = Int32Array::from(values);
245 DataBlock::from_array(array)
246 }
247
248 fn create_pattern_f64_block<F>(size: usize, pattern: F) -> DataBlock
249 where
250 F: Fn(usize) -> f64,
251 {
252 let values: Vec<f64> = (0..size).map(pattern).collect();
253 let array = Float64Array::from(values);
254 DataBlock::from_array(array)
255 }
256
257 fn run_round_trip_test(test_case: TestCase) {
258 let compressor =
259 GeneralMiniBlockCompressor::new(test_case.inner_encoder, test_case.compression);
260
261 let (compressed, encoding) = compressor
263 .compress(miniblock_context(), test_case.data)
264 .unwrap();
265
266 match &encoding.compression {
268 Some(Compression::General(cm)) => {
269 assert!(
270 test_case.expected_compressed,
271 "{}: Expected compression to be applied",
272 test_case.name
273 );
274 assert_eq!(
275 CompressionScheme::try_from(cm.compression.as_ref().unwrap().scheme()).unwrap(),
276 test_case.compression.scheme
277 );
278 }
279 _ => {
280 if test_case.expected_compressed {
282 match &encoding.compression {
284 Some(Compression::Rle(_)) => {
285 }
287 Some(Compression::Flat(_)) => {
288 }
290 _ => {
291 panic!(
292 "{}: Expected GeneralMiniBlock but got {:?}",
293 test_case.name, encoding.compression
294 );
295 }
296 }
297 }
298 }
299 }
300
301 assert!(
303 !compressed.chunks.is_empty(),
304 "{}: No chunks created",
305 test_case.name
306 );
307
308 let decompressed_data = decompress_miniblock_chunks(&compressed, &encoding);
310
311 let bytes_per_value = if test_case.name.contains("float") {
315 8 } else {
317 4 };
319 let expected_bytes = compressed.num_values as usize * bytes_per_value;
320 assert_eq!(
321 expected_bytes,
322 decompressed_data.len(),
323 "{}: Data size mismatch",
324 test_case.name
325 );
326
327 if test_case.expected_compressed {
329 let compression_ratio = compressed.data[0].len() as f32 / expected_bytes as f32;
330 assert!(
331 compression_ratio <= test_case.min_compression_ratio,
332 "{}: Compression ratio {} > expected {}",
333 test_case.name,
334 compression_ratio,
335 test_case.min_compression_ratio
336 );
337 }
338 }
339
340 fn decompress_miniblock_chunks(
341 compressed: &MiniBlockCompressed,
342 encoding: &CompressiveEncoding,
343 ) -> Vec<u8> {
344 let mut decompressed_data = Vec::new();
345 let mut offsets = vec![0usize; compressed.data.len()]; let decompression_strategy = DefaultDecompressionStrategy::default();
347
348 for chunk in &compressed.chunks {
349 let chunk_values = if chunk.log_num_values > 0 {
350 1u64 << chunk.log_num_values
351 } else {
352 let decompressed_values =
354 decompressed_data.len() as u64 / get_bytes_per_value(compressed) as u64;
355 compressed.num_values.saturating_sub(decompressed_values)
356 };
357
358 let mut chunk_buffers = Vec::new();
360 for (i, &size) in chunk.buffer_sizes.iter().enumerate() {
361 if i < compressed.data.len() {
362 let buffer_data =
363 compressed.data[i].slice_with_length(offsets[i], size as usize);
364 chunk_buffers.push(buffer_data);
365 offsets[i] += size as usize;
366 }
367 }
368
369 let decompressor = decompression_strategy
371 .create_miniblock_decompressor(encoding, &decompression_strategy)
372 .unwrap();
373
374 let chunk_decompressed = decompressor
376 .decompress(chunk_buffers, chunk_values)
377 .unwrap();
378
379 match chunk_decompressed {
380 DataBlock::FixedWidth(ref block) => {
381 decompressed_data.extend_from_slice(block.data.as_ref());
382 }
383 _ => panic!("Expected FixedWidth block"),
384 }
385 }
386
387 decompressed_data
388 }
389
390 fn get_bytes_per_value(compressed: &MiniBlockCompressed) -> usize {
391 if compressed.num_values == 0 {
395 return 4; }
397
398 if compressed.num_values == 1024 {
400 return 8; }
402
403 4 }
405
406 #[test]
407 fn test_compressed_mini_block_table_driven() {
408 for test_case in create_test_cases() {
409 run_round_trip_test(test_case);
410 }
411 }
412
413 #[test]
414 fn test_compressed_mini_block_threshold() {
415 let small_test = TestCase {
417 name: "small_buffer_no_compression",
418 inner_encoder: Box::new(RleEncoder::new()),
419 compression: CompressionConfig {
420 scheme: CompressionScheme::Lz4,
421 level: None,
422 },
423 data: create_repeated_i32_block(vec![1, 1, 2, 2]),
424 expected_compressed: false,
425 min_compression_ratio: 1.0,
426 };
427 run_round_trip_test(small_test);
428 }
429
430 #[test]
431 fn test_compressed_mini_block_with_doubles() {
432 let test_case = TestCase {
436 name: "float_values_with_zstd",
437 inner_encoder: Box::new(ValueEncoder {}),
438 compression: CompressionConfig {
439 scheme: CompressionScheme::Zstd,
440 level: Some(3),
441 },
442 data: create_pattern_f64_block(1024, |i| (i / 10) as f64),
444 expected_compressed: true,
445 min_compression_ratio: 0.5, };
447
448 run_round_trip_test(test_case);
449 }
450
451 #[test]
452 fn test_compressed_mini_block_large_buffers() {
453 let values: Vec<i32> = (0..1024).collect();
456 let data = LanceBuffer::from_bytes(
457 bytemuck::cast_slice(&values).to_vec().into(),
458 std::mem::align_of::<i32>() as u64,
459 );
460 let block = DataBlock::FixedWidth(FixedWidthDataBlock {
461 bits_per_value: 32,
462 data,
463 num_values: 1024,
464 block_info: BlockInfo::new(),
465 });
466
467 let inner = Box::new(ValueEncoder {});
469 let compression = CompressionConfig {
470 scheme: CompressionScheme::Zstd,
471 level: Some(3),
472 };
473 let compressor = GeneralMiniBlockCompressor::new(inner, compression);
474
475 let (compressed, encoding) = compressor.compress(miniblock_context(), block).unwrap();
477
478 match &encoding.compression {
480 Some(Compression::General(cm)) => {
481 assert!(cm.values.is_some());
482 assert_eq!(
483 cm.compression.as_ref().unwrap().scheme(),
484 pb21::CompressionScheme::CompressionAlgorithmZstd
485 );
486 assert_eq!(cm.compression.as_ref().unwrap().level, Some(3));
487
488 match &cm.values.as_ref().unwrap().compression {
490 Some(Compression::Flat(flat)) => {
491 assert_eq!(flat.bits_per_value, 32);
492 }
493 _ => panic!("Expected Flat inner encoding"),
494 }
495 }
496 _ => panic!("Expected GeneralMiniBlock encoding"),
497 }
498
499 assert_eq!(compressed.num_values, 1024);
500 assert_eq!(compressed.data.len(), 1);
502 }
503
504 #[test]
507 fn test_compressed_mini_block_rle_multiple_buffers() {
508 let data = create_repeated_i32_block(vec![1; 100]);
510 let compressor = GeneralMiniBlockCompressor::new(
511 Box::new(RleEncoder::new()),
512 CompressionConfig {
513 scheme: CompressionScheme::Lz4,
514 level: None,
515 },
516 );
517
518 let (compressed, _) = compressor.compress(miniblock_context(), data).unwrap();
519 assert_eq!(compressed.data.len(), 2);
521 }
522
523 #[test]
524 fn test_rle_with_general_miniblock_wrapper() {
525 let test_32 = TestCase {
533 name: "rle_32bit_with_general_wrapper",
534 inner_encoder: Box::new(RleEncoder::new()),
535 compression: CompressionConfig {
536 scheme: CompressionScheme::Lz4,
537 level: None,
538 },
539 data: create_repeated_i32_block(vec![1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3]),
540 expected_compressed: false, min_compression_ratio: 1.0,
542 };
543
544 let compressor = GeneralMiniBlockCompressor::new(
547 Box::new(RleEncoder::new()),
548 CompressionConfig {
549 scheme: CompressionScheme::Lz4,
550 level: None,
551 },
552 );
553
554 let (_compressed, encoding) = compressor
555 .compress(miniblock_context(), test_32.data)
556 .unwrap();
557
558 match &encoding.compression {
560 Some(Compression::General(cm)) => {
561 match &cm.values.as_ref().unwrap().compression {
563 Some(Compression::Rle(rle)) => {
564 let Compression::Flat(values) =
565 rle.values.as_ref().unwrap().compression.as_ref().unwrap()
566 else {
567 panic!("Expected flat for RLE values")
568 };
569 let Compression::Flat(run_lengths) = rle
570 .run_lengths
571 .as_ref()
572 .unwrap()
573 .compression
574 .as_ref()
575 .unwrap()
576 else {
577 panic!("Expected flat for RLE run lengths")
578 };
579 assert_eq!(values.bits_per_value, 32);
580 assert_eq!(run_lengths.bits_per_value, 8);
581 }
582 _ => panic!("Expected RLE as inner encoding"),
583 }
584 assert_eq!(
586 cm.compression.as_ref().unwrap().scheme(),
587 pb21::CompressionScheme::CompressionAlgorithmLz4
588 );
589 }
590 Some(Compression::Rle(_)) => {
591 }
593 _ => panic!("Expected GeneralMiniBlock or Rle encoding"),
594 }
595
596 let values_64: Vec<i64> = vec![100i64; 50]
598 .into_iter()
599 .chain(vec![200i64; 50])
600 .chain(vec![300i64; 50])
601 .collect();
602 let array_64 = arrow_array::Int64Array::from(values_64);
603 let block_64 = DataBlock::from_array(array_64);
604
605 let compressor_64 = GeneralMiniBlockCompressor::new(
606 Box::new(RleEncoder::new()),
607 CompressionConfig {
608 scheme: CompressionScheme::Lz4,
609 level: None,
610 },
611 );
612
613 let (_compressed_64, encoding_64) = compressor_64
614 .compress(miniblock_context(), block_64)
615 .unwrap();
616
617 match &encoding_64.compression {
619 Some(Compression::General(cm)) => {
620 match &cm.values.as_ref().unwrap().compression {
622 Some(Compression::Rle(rle)) => {
623 let Compression::Flat(values) =
624 rle.values.as_ref().unwrap().compression.as_ref().unwrap()
625 else {
626 panic!("Expected flat for RLE values")
627 };
628 let Compression::Flat(run_lengths) = rle
629 .run_lengths
630 .as_ref()
631 .unwrap()
632 .compression
633 .as_ref()
634 .unwrap()
635 else {
636 panic!("Expected flat for RLE run lengths")
637 };
638 assert_eq!(values.bits_per_value, 64);
639 assert_eq!(run_lengths.bits_per_value, 8);
640 }
641 _ => panic!("Expected RLE as inner encoding for 64-bit"),
642 }
643 assert_eq!(
645 cm.compression.as_ref().unwrap().scheme(),
646 pb21::CompressionScheme::CompressionAlgorithmLz4
647 );
648 }
649 Some(Compression::Rle(_)) => {
650 }
652 _ => panic!("Expected GeneralMiniBlock or Rle encoding for 64-bit"),
653 }
654 }
655
656 #[test]
657 fn test_compressed_mini_block_empty_data() {
658 let empty_array = Int32Array::from(vec![] as Vec<i32>);
659 let empty_block = DataBlock::from_array(empty_array);
660
661 let compressor = GeneralMiniBlockCompressor::new(
662 Box::new(ValueEncoder {}),
663 CompressionConfig {
664 scheme: CompressionScheme::Lz4,
665 level: None,
666 },
667 );
668
669 let result = compressor.compress(miniblock_context(), empty_block);
670 match result {
671 Ok((compressed, _)) => {
672 assert_eq!(compressed.num_values, 0);
673 }
674 Err(_) => {
675 }
677 }
678 }
679}