Skip to main content

lance_encoding/encodings/physical/
block.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Encodings based on traditional block compression schemes
5//!
6//! Traditional compressors take in a buffer and return a smaller buffer.  All encoding
7//! description is shoved into the compressed buffer and the entire buffer is needed to
8//! decompress any of the data.
9//!
10//! These encodings are not transparent, which limits our ability to use them.  In addition
11//! they are often quite expensive in CPU terms.
12//!
13//! However, they are effective and useful for some cases.  For example, when working with large
14//! variable length values (e.g. source code files) they can be very effective.
15//!
16//! The module introduces the `[BufferCompressor]` trait which describes the interface for a
17//! traditional block compressor.  It is implemented for the most common compression schemes
18//! (zstd, lz4, etc).
19//!
20//! There is not yet a mini-block variant of this compressor (but could easily be one) and the
21//! full zip variant works by applying compression on a per-value basis (which allows it to be
22//! transparent).
23
24use arrow_buffer::ArrowNativeType;
25use lance_core::{Error, Result};
26
27use std::str::FromStr;
28
29use crate::compression::{BlockCompressor, BlockDecompressor};
30use crate::encodings::physical::binary::{BinaryBlockDecompressor, VariableEncoder};
31use crate::format::{
32    ProtobufUtils21,
33    pb21::{self, CompressiveEncoding},
34};
35use crate::{
36    buffer::LanceBuffer,
37    compression::VariablePerValueDecompressor,
38    data::{BlockInfo, DataBlock, VariableWidthBlock},
39    encodings::logical::primitive::fullzip::{PerValueCompressor, PerValueDataBlock},
40};
41
42#[derive(Debug, Clone, Copy, PartialEq)]
43pub struct CompressionConfig {
44    pub(crate) scheme: CompressionScheme,
45    pub(crate) level: Option<i32>,
46}
47
48impl CompressionConfig {
49    /// Create a compression configuration for an encoding mechanism.
50    pub fn new(scheme: CompressionScheme, level: Option<i32>) -> Self {
51        Self { scheme, level }
52    }
53
54    /// Return the selected compression scheme.
55    pub fn scheme(&self) -> CompressionScheme {
56        self.scheme
57    }
58
59    /// Return the optional compression level.
60    pub fn level(&self) -> Option<i32> {
61        self.level
62    }
63}
64
65impl Default for CompressionConfig {
66    fn default() -> Self {
67        Self {
68            scheme: CompressionScheme::Lz4,
69            level: Some(0),
70        }
71    }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq)]
75pub enum CompressionScheme {
76    None,
77    Fsst,
78    Zstd,
79    Lz4,
80}
81
82impl TryFrom<CompressionScheme> for pb21::CompressionScheme {
83    type Error = Error;
84
85    fn try_from(scheme: CompressionScheme) -> Result<Self> {
86        match scheme {
87            CompressionScheme::Lz4 => Ok(Self::CompressionAlgorithmLz4),
88            CompressionScheme::Zstd => Ok(Self::CompressionAlgorithmZstd),
89            _ => Err(Error::invalid_input(format!(
90                "Unsupported compression scheme: {:?}",
91                scheme
92            ))),
93        }
94    }
95}
96
97impl TryFrom<pb21::CompressionScheme> for CompressionScheme {
98    type Error = Error;
99
100    fn try_from(scheme: pb21::CompressionScheme) -> Result<Self> {
101        match scheme {
102            pb21::CompressionScheme::CompressionAlgorithmLz4 => Ok(Self::Lz4),
103            pb21::CompressionScheme::CompressionAlgorithmZstd => Ok(Self::Zstd),
104            _ => Err(Error::invalid_input(format!(
105                "Unsupported compression scheme: {:?}",
106                scheme
107            ))),
108        }
109    }
110}
111
112impl std::fmt::Display for CompressionScheme {
113    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
114        let scheme_str = match self {
115            Self::Fsst => "fsst",
116            Self::Zstd => "zstd",
117            Self::None => "none",
118            Self::Lz4 => "lz4",
119        };
120        write!(f, "{}", scheme_str)
121    }
122}
123
124impl FromStr for CompressionScheme {
125    type Err = Error;
126
127    fn from_str(s: &str) -> Result<Self> {
128        match s {
129            "none" => Ok(Self::None),
130            "fsst" => Ok(Self::Fsst),
131            "zstd" => Ok(Self::Zstd),
132            "lz4" => Ok(Self::Lz4),
133            _ => Err(Error::invalid_input(format!(
134                "Unknown compression scheme: {}",
135                s
136            ))),
137        }
138    }
139}
140
141pub trait BufferCompressor: std::fmt::Debug + Send + Sync {
142    fn compress(&self, input_buf: &[u8], output_buf: &mut Vec<u8>) -> Result<()>;
143    fn decompress(&self, input_buf: &[u8], output_buf: &mut Vec<u8>) -> Result<()>;
144    fn config(&self) -> CompressionConfig;
145}
146
147#[cfg(feature = "zstd")]
148mod zstd {
149    use std::io::{Cursor, Write};
150    use std::sync::{Mutex, OnceLock};
151
152    use super::*;
153
154    use ::zstd::bulk::{Compressor, decompress_to_buffer};
155    use ::zstd::stream::copy_decode;
156
157    /// A zstd buffer compressor that lazily creates and reuses compression contexts.
158    ///
159    /// The compression context is cached to enable reuse across chunks within a
160    /// page. It is lazily initialized to prevent it from getting initialized on
161    /// decode-only codepaths.
162    ///
163    /// Reuse is not implemented for decompression, only for compression:
164    /// * The single-threaded benefit of reuse was negligible when measured.
165    /// * Decompressors can get shared across threads, leading to mutex
166    ///   contention if the same strategy is used as for compression here. This
167    ///   should be mitigable with pooling but we can skip the complexity until a
168    ///   need is demonstrated. The multithreaded decode benchmark effectively
169    ///   demonstrates this scenario.
170    pub struct ZstdBufferCompressor {
171        compression_level: i32,
172        compressor: OnceLock<std::result::Result<Mutex<Compressor<'static>>, String>>,
173    }
174
175    impl std::fmt::Debug for ZstdBufferCompressor {
176        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177            f.debug_struct("ZstdBufferCompressor")
178                .field("compression_level", &self.compression_level)
179                .finish()
180        }
181    }
182
183    impl ZstdBufferCompressor {
184        pub fn new(compression_level: i32) -> Self {
185            Self {
186                compression_level,
187                compressor: OnceLock::new(),
188            }
189        }
190
191        fn get_compressor(&self) -> Result<&Mutex<Compressor<'static>>> {
192            self.compressor
193                .get_or_init(|| {
194                    Compressor::new(self.compression_level)
195                        .map(Mutex::new)
196                        .map_err(|e| e.to_string())
197                })
198                .as_ref()
199                .map_err(|e| Error::internal(format!("Failed to create zstd compressor: {}", e)))
200        }
201
202        // https://datatracker.ietf.org/doc/html/rfc8878
203        fn is_raw_stream_format(&self, input_buf: &[u8]) -> bool {
204            if input_buf.len() < 8 {
205                return true; // can't be length prefixed format if less than 8 bytes
206            }
207            // read the first 4 bytes as the magic number
208            let mut magic_buf = [0u8; 4];
209            magic_buf.copy_from_slice(&input_buf[..4]);
210            let magic = u32::from_le_bytes(magic_buf);
211
212            // see RFC 8878, section 3.1.1. Zstandard Frames, which defines the magic number
213            const ZSTD_MAGIC_NUMBER: u32 = 0xFD2FB528;
214            if magic == ZSTD_MAGIC_NUMBER {
215                // the compressed buffer starts like a Zstd frame.
216                // Per RFC 8878, the reserved bit (with Bit Number 3, the 4th bit) in the FHD (frame header descriptor) MUST be 0
217                // see section 3.1.1.1.1. 'Frame_Header_Descriptor' and section 3.1.1.1.1.4. 'Reserved Bit' for details
218                const FHD_BYTE_INDEX: usize = 4;
219                let fhd_byte = input_buf[FHD_BYTE_INDEX];
220                const FHD_RESERVED_BIT_MASK: u8 = 0b0001_0000;
221                let reserved_bit = fhd_byte & FHD_RESERVED_BIT_MASK;
222
223                if reserved_bit != 0 {
224                    // this bit is 1. This is NOT a valid zstd frame.
225                    // therefore, it must be length prefixed format where the length coincidentally
226                    // started with the magic number
227                    false
228                } else {
229                    // the reserved bit is 0. This is consistent with a valid Zstd frame.
230                    // treat it as raw stream format
231                    true
232                }
233            } else {
234                // doesn't start with the magic number, so it can't be the raw stream format
235                false
236            }
237        }
238
239        fn decompress_length_prefixed_zstd(
240            &self,
241            input_buf: &[u8],
242            output_buf: &mut Vec<u8>,
243        ) -> Result<()> {
244            const LENGTH_PREFIX_SIZE: usize = 8;
245            let mut len_buf = [0u8; LENGTH_PREFIX_SIZE];
246            len_buf.copy_from_slice(&input_buf[..LENGTH_PREFIX_SIZE]);
247
248            let uncompressed_len = u64::from_le_bytes(len_buf) as usize;
249
250            let start = output_buf.len();
251            output_buf.resize(start + uncompressed_len, 0);
252
253            let compressed_data = &input_buf[LENGTH_PREFIX_SIZE..];
254            decompress_to_buffer(compressed_data, &mut output_buf[start..])?;
255            Ok(())
256        }
257    }
258
259    impl BufferCompressor for ZstdBufferCompressor {
260        fn compress(&self, input_buf: &[u8], output_buf: &mut Vec<u8>) -> Result<()> {
261            output_buf.write_all(&(input_buf.len() as u64).to_le_bytes())?;
262
263            let max_compressed_size = ::zstd::zstd_safe::compress_bound(input_buf.len());
264            let start_pos = output_buf.len();
265            output_buf.resize(start_pos + max_compressed_size, 0);
266
267            let compressed_size = self
268                .get_compressor()?
269                .lock()
270                .unwrap()
271                .compress_to_buffer(input_buf, &mut output_buf[start_pos..])
272                .map_err(|e| Error::internal(format!("Zstd compression error: {}", e)))?;
273
274            output_buf.truncate(start_pos + compressed_size);
275            Ok(())
276        }
277
278        fn decompress(&self, input_buf: &[u8], output_buf: &mut Vec<u8>) -> Result<()> {
279            if input_buf.is_empty() {
280                return Ok(());
281            }
282
283            let is_raw_stream_format = self.is_raw_stream_format(input_buf);
284            if is_raw_stream_format {
285                copy_decode(Cursor::new(input_buf), output_buf)?;
286            } else {
287                self.decompress_length_prefixed_zstd(input_buf, output_buf)?;
288            }
289
290            Ok(())
291        }
292
293        fn config(&self) -> CompressionConfig {
294            CompressionConfig {
295                scheme: CompressionScheme::Zstd,
296                level: Some(self.compression_level),
297            }
298        }
299    }
300}
301
302#[cfg(feature = "lz4")]
303mod lz4 {
304    use super::*;
305
306    #[derive(Debug, Default)]
307    pub struct Lz4BufferCompressor {}
308
309    impl BufferCompressor for Lz4BufferCompressor {
310        fn compress(&self, input_buf: &[u8], output_buf: &mut Vec<u8>) -> Result<()> {
311            // Remember the starting position
312            let start_pos = output_buf.len();
313
314            // LZ4 needs space for the compressed data
315            let max_size = ::lz4::block::compress_bound(input_buf.len())?;
316            // Resize to ensure we have enough space (including 4 bytes for size header)
317            output_buf.resize(start_pos + max_size + 4, 0);
318
319            let compressed_size = ::lz4::block::compress_to_buffer(
320                input_buf,
321                None,
322                true,
323                &mut output_buf[start_pos..],
324            )
325            .map_err(|err| Error::internal(format!("LZ4 compression error: {}", err)))?;
326
327            // Truncate to actual size
328            output_buf.truncate(start_pos + compressed_size);
329            Ok(())
330        }
331
332        fn decompress(&self, input_buf: &[u8], output_buf: &mut Vec<u8>) -> Result<()> {
333            // When prepend_size is true, LZ4 stores the uncompressed size in the first 4 bytes
334            // We can read this to know exactly how much space we need
335            if input_buf.len() < 4 {
336                return Err(Error::internal("LZ4 compressed data too short".to_string()));
337            }
338
339            // Read the uncompressed size from the first 4 bytes (little-endian)
340            let uncompressed_size =
341                u32::from_le_bytes([input_buf[0], input_buf[1], input_buf[2], input_buf[3]])
342                    as usize;
343
344            // Remember the starting position
345            let start_pos = output_buf.len();
346
347            // Resize to ensure we have the exact space needed
348            output_buf.resize(start_pos + uncompressed_size, 0);
349
350            // Now decompress directly into the buffer slice
351            let decompressed_size =
352                ::lz4::block::decompress_to_buffer(input_buf, None, &mut output_buf[start_pos..])
353                    .map_err(|err| Error::internal(format!("LZ4 decompression error: {}", err)))?;
354
355            // Truncate to actual decompressed size (should be same as uncompressed_size)
356            output_buf.truncate(start_pos + decompressed_size);
357
358            Ok(())
359        }
360
361        fn config(&self) -> CompressionConfig {
362            CompressionConfig {
363                scheme: CompressionScheme::Lz4,
364                level: None,
365            }
366        }
367    }
368}
369
370#[derive(Debug, Default)]
371pub struct NoopBufferCompressor {}
372
373impl BufferCompressor for NoopBufferCompressor {
374    fn compress(&self, input_buf: &[u8], output_buf: &mut Vec<u8>) -> Result<()> {
375        output_buf.extend_from_slice(input_buf);
376        Ok(())
377    }
378
379    fn decompress(&self, input_buf: &[u8], output_buf: &mut Vec<u8>) -> Result<()> {
380        output_buf.extend_from_slice(input_buf);
381        Ok(())
382    }
383
384    fn config(&self) -> CompressionConfig {
385        CompressionConfig {
386            scheme: CompressionScheme::None,
387            level: None,
388        }
389    }
390}
391
392pub struct GeneralBufferCompressor {}
393
394impl GeneralBufferCompressor {
395    pub fn get_compressor(
396        compression_config: CompressionConfig,
397    ) -> Result<Box<dyn BufferCompressor>> {
398        match compression_config.scheme {
399            // FSST has its own compression path and isn't implemented as a generic buffer compressor
400            CompressionScheme::Fsst => Err(Error::invalid_input_source(
401                "fsst is not usable as a general buffer compressor".into(),
402            )),
403            CompressionScheme::Zstd => {
404                #[cfg(feature = "zstd")]
405                {
406                    Ok(Box::new(zstd::ZstdBufferCompressor::new(
407                        compression_config.level.unwrap_or(0),
408                    )))
409                }
410                #[cfg(not(feature = "zstd"))]
411                {
412                    Err(Error::invalid_input_source(
413                        "package was not built with zstd support".into(),
414                    ))
415                }
416            }
417            CompressionScheme::Lz4 => {
418                #[cfg(feature = "lz4")]
419                {
420                    Ok(Box::new(lz4::Lz4BufferCompressor::default()))
421                }
422                #[cfg(not(feature = "lz4"))]
423                {
424                    Err(Error::invalid_input_source(
425                        "package was not built with lz4 support".into(),
426                    ))
427                }
428            }
429            CompressionScheme::None => Ok(Box::new(NoopBufferCompressor {})),
430        }
431    }
432}
433
434/// A block decompressor that first applies general-purpose compression (LZ4/Zstd)
435/// before delegating to an inner block decompressor.
436#[derive(Debug)]
437pub struct GeneralBlockDecompressor {
438    inner: Box<dyn BlockDecompressor>,
439    compressor: Box<dyn BufferCompressor>,
440}
441
442impl GeneralBlockDecompressor {
443    pub fn try_new(
444        inner: Box<dyn BlockDecompressor>,
445        compression: CompressionConfig,
446    ) -> Result<Self> {
447        let compressor = GeneralBufferCompressor::get_compressor(compression)?;
448        Ok(Self { inner, compressor })
449    }
450}
451
452impl BlockDecompressor for GeneralBlockDecompressor {
453    fn decompress(&self, data: LanceBuffer, num_values: u64) -> Result<DataBlock> {
454        let mut decompressed = Vec::new();
455        self.compressor.decompress(&data, &mut decompressed)?;
456        self.inner
457            .decompress(LanceBuffer::from(decompressed), num_values)
458    }
459}
460
461// An encoder which uses generic compression, such as zstd/lz4 to encode buffers
462#[derive(Debug)]
463pub struct CompressedBufferEncoder {
464    pub(crate) compressor: Box<dyn BufferCompressor>,
465}
466
467impl Default for CompressedBufferEncoder {
468    fn default() -> Self {
469        // Pick zstd if available, otherwise lz4, otherwise none
470        #[cfg(feature = "zstd")]
471        let (scheme, level) = (CompressionScheme::Zstd, Some(0));
472        #[cfg(all(feature = "lz4", not(feature = "zstd")))]
473        let (scheme, level) = (CompressionScheme::Lz4, None);
474        #[cfg(not(any(feature = "zstd", feature = "lz4")))]
475        let (scheme, level) = (CompressionScheme::None, None);
476
477        let compressor =
478            GeneralBufferCompressor::get_compressor(CompressionConfig { scheme, level }).unwrap();
479        Self { compressor }
480    }
481}
482
483impl CompressedBufferEncoder {
484    pub fn try_new(compression_config: CompressionConfig) -> Result<Self> {
485        let compressor = GeneralBufferCompressor::get_compressor(compression_config)?;
486        Ok(Self { compressor })
487    }
488
489    pub fn from_scheme(scheme: pb21::CompressionScheme) -> Result<Self> {
490        let scheme = CompressionScheme::try_from(scheme)?;
491        Ok(Self {
492            compressor: GeneralBufferCompressor::get_compressor(CompressionConfig {
493                scheme,
494                level: Some(0),
495            })?,
496        })
497    }
498}
499
500impl CompressedBufferEncoder {
501    pub fn per_value_compress<T: ArrowNativeType>(
502        &self,
503        data: &[u8],
504        offsets: &[T],
505        compressed: &mut Vec<u8>,
506    ) -> Result<LanceBuffer> {
507        let mut new_offsets: Vec<T> = Vec::with_capacity(offsets.len());
508        new_offsets.push(T::from_usize(0).unwrap());
509
510        for off in offsets.windows(2) {
511            let start = off[0].as_usize();
512            let end = off[1].as_usize();
513            self.compressor.compress(&data[start..end], compressed)?;
514            new_offsets.push(T::from_usize(compressed.len()).unwrap());
515        }
516
517        Ok(LanceBuffer::reinterpret_vec(new_offsets))
518    }
519
520    pub fn per_value_decompress<T: ArrowNativeType>(
521        &self,
522        data: &[u8],
523        offsets: &[T],
524        decompressed: &mut Vec<u8>,
525    ) -> Result<LanceBuffer> {
526        let mut new_offsets: Vec<T> = Vec::with_capacity(offsets.len());
527        new_offsets.push(T::from_usize(0).unwrap());
528
529        for off in offsets.windows(2) {
530            let start = off[0].as_usize();
531            let end = off[1].as_usize();
532            self.compressor
533                .decompress(&data[start..end], decompressed)?;
534            new_offsets.push(T::from_usize(decompressed.len()).unwrap());
535        }
536
537        Ok(LanceBuffer::reinterpret_vec(new_offsets))
538    }
539}
540
541impl PerValueCompressor for CompressedBufferEncoder {
542    fn compress(&self, data: DataBlock) -> Result<(PerValueDataBlock, CompressiveEncoding)> {
543        let data_type = data.name();
544        let data = data.as_variable_width().ok_or(Error::internal(format!(
545            "Attempt to use CompressedBufferEncoder on data of type {}",
546            data_type
547        )))?;
548
549        let data_bytes = &data.data;
550        let mut compressed = Vec::with_capacity(data_bytes.len());
551
552        let new_offsets = match data.bits_per_offset {
553            32 => self.per_value_compress::<u32>(
554                data_bytes,
555                &data.offsets.borrow_to_typed_slice::<u32>(),
556                &mut compressed,
557            )?,
558            64 => self.per_value_compress::<u64>(
559                data_bytes,
560                &data.offsets.borrow_to_typed_slice::<u64>(),
561                &mut compressed,
562            )?,
563            _ => unreachable!(),
564        };
565
566        let compressed = PerValueDataBlock::Variable(VariableWidthBlock {
567            bits_per_offset: data.bits_per_offset,
568            data: LanceBuffer::from(compressed),
569            offsets: new_offsets,
570            num_values: data.num_values,
571            block_info: BlockInfo::new(),
572        });
573
574        // TODO: Support setting the level
575        // TODO: Support underlying compression of data (e.g. defer to binary encoding for offset bitpacking)
576        let encoding = ProtobufUtils21::wrapped(
577            self.compressor.config(),
578            ProtobufUtils21::variable(
579                ProtobufUtils21::flat(data.bits_per_offset as u64, None),
580                None,
581            ),
582        )?;
583
584        Ok((compressed, encoding))
585    }
586}
587
588impl VariablePerValueDecompressor for CompressedBufferEncoder {
589    fn decompress(&self, data: VariableWidthBlock) -> Result<DataBlock> {
590        let data_bytes = &data.data;
591        let mut decompressed = Vec::with_capacity(data_bytes.len() * 2);
592
593        let new_offsets = match data.bits_per_offset {
594            32 => self.per_value_decompress(
595                data_bytes,
596                &data.offsets.borrow_to_typed_slice::<u32>(),
597                &mut decompressed,
598            )?,
599            64 => self.per_value_decompress(
600                data_bytes,
601                &data.offsets.borrow_to_typed_slice::<u64>(),
602                &mut decompressed,
603            )?,
604            _ => unreachable!(),
605        };
606        Ok(DataBlock::VariableWidth(VariableWidthBlock {
607            bits_per_offset: data.bits_per_offset,
608            data: LanceBuffer::from(decompressed),
609            offsets: new_offsets,
610            num_values: data.num_values,
611            block_info: BlockInfo::new(),
612        }))
613    }
614}
615
616impl BlockCompressor for CompressedBufferEncoder {
617    fn compress(&self, data: DataBlock) -> Result<LanceBuffer> {
618        let encoded = match data {
619            DataBlock::FixedWidth(fixed_width) => fixed_width.data,
620            DataBlock::VariableWidth(variable_width) => {
621                // Wrap VariableEncoder to handle the encoding
622                let encoder = VariableEncoder::default();
623                BlockCompressor::compress(&encoder, DataBlock::VariableWidth(variable_width))?
624            }
625            _ => {
626                return Err(Error::invalid_input_source(
627                    "Unsupported data block type".into(),
628                ));
629            }
630        };
631
632        let mut compressed = Vec::new();
633        self.compressor.compress(&encoded, &mut compressed)?;
634        Ok(LanceBuffer::from(compressed))
635    }
636}
637
638impl BlockDecompressor for CompressedBufferEncoder {
639    fn decompress(&self, data: LanceBuffer, num_values: u64) -> Result<DataBlock> {
640        let mut decompressed = Vec::new();
641        self.compressor.decompress(&data, &mut decompressed)?;
642
643        // Delegate to BinaryBlockDecompressor which handles the inline metadata
644        let inner_decoder = BinaryBlockDecompressor::default();
645        inner_decoder.decompress(LanceBuffer::from(decompressed), num_values)
646    }
647}
648
649#[cfg(test)]
650mod tests {
651    use super::*;
652    use std::str::FromStr;
653
654    use crate::encodings::physical::block::zstd::ZstdBufferCompressor;
655
656    #[test]
657    fn test_compression_scheme_from_str() {
658        assert_eq!(
659            CompressionScheme::from_str("none").unwrap(),
660            CompressionScheme::None
661        );
662        assert_eq!(
663            CompressionScheme::from_str("zstd").unwrap(),
664            CompressionScheme::Zstd
665        );
666    }
667
668    #[test]
669    fn test_compression_scheme_from_str_invalid() {
670        assert!(CompressionScheme::from_str("invalid").is_err());
671    }
672
673    #[cfg(feature = "zstd")]
674    mod zstd {
675        use std::io::Write;
676
677        use super::*;
678
679        #[test]
680        fn test_compress_zstd_with_length_prefixed() {
681            let compressor = ZstdBufferCompressor::new(0);
682            let input_data = b"Hello, world!";
683            let mut compressed_data = Vec::new();
684
685            compressor
686                .compress(input_data, &mut compressed_data)
687                .unwrap();
688            let mut decompressed_data = Vec::new();
689            compressor
690                .decompress(&compressed_data, &mut decompressed_data)
691                .unwrap();
692            assert_eq!(input_data, decompressed_data.as_slice());
693        }
694
695        #[test]
696        fn test_zstd_compress_decompress_multiple_times() {
697            let compressor = ZstdBufferCompressor::new(0);
698            let (input_data_1, input_data_2) = (b"Hello ", b"World");
699            let mut compressed_data = Vec::new();
700
701            compressor
702                .compress(input_data_1, &mut compressed_data)
703                .unwrap();
704            let compressed_length_1 = compressed_data.len();
705
706            compressor
707                .compress(input_data_2, &mut compressed_data)
708                .unwrap();
709
710            let mut decompressed_data = Vec::new();
711            compressor
712                .decompress(
713                    &compressed_data[..compressed_length_1],
714                    &mut decompressed_data,
715                )
716                .unwrap();
717
718            compressor
719                .decompress(
720                    &compressed_data[compressed_length_1..],
721                    &mut decompressed_data,
722                )
723                .unwrap();
724
725            // the output should contain both input_data_1 and input_data_2
726            assert_eq!(
727                decompressed_data.len(),
728                input_data_1.len() + input_data_2.len()
729            );
730            assert_eq!(
731                &decompressed_data[..input_data_1.len()],
732                input_data_1,
733                "First part of decompressed data should match input_1"
734            );
735            assert_eq!(
736                &decompressed_data[input_data_1.len()..],
737                input_data_2,
738                "Second part of decompressed data should match input_2"
739            );
740        }
741
742        #[test]
743        fn test_compress_zstd_raw_stream_format_and_decompress_with_length_prefixed() {
744            let compressor = ZstdBufferCompressor::new(0);
745            let input_data = b"Hello, world!";
746            let mut compressed_data = Vec::new();
747
748            // compress using raw stream format
749            let mut encoder = ::zstd::Encoder::new(&mut compressed_data, 0).unwrap();
750            encoder.write_all(input_data).unwrap();
751            encoder.finish().expect("failed to encode data with zstd");
752
753            // decompress using length prefixed format
754            let mut decompressed_data = Vec::new();
755            compressor
756                .decompress(&compressed_data, &mut decompressed_data)
757                .unwrap();
758            assert_eq!(input_data, decompressed_data.as_slice());
759        }
760    }
761
762    #[cfg(feature = "lz4")]
763    mod lz4 {
764        use std::{collections::HashMap, sync::Arc};
765
766        use arrow_schema::{DataType, Field};
767        use lance_datagen::array::{binary_prefix_plus_counter, utf8_prefix_plus_counter};
768
769        use super::*;
770
771        use crate::constants::DICT_SIZE_RATIO_META_KEY;
772        use crate::{
773            constants::{
774                COMPRESSION_META_KEY, DICT_DIVISOR_META_KEY, STRUCTURAL_ENCODING_FULLZIP,
775                STRUCTURAL_ENCODING_META_KEY,
776            },
777            encodings::physical::block::lz4::Lz4BufferCompressor,
778            testing::{FnArrayGeneratorProvider, TestCases, check_round_trip_encoding_generated},
779        };
780
781        #[test]
782        fn test_lz4_compress_decompress() {
783            let compressor = Lz4BufferCompressor::default();
784            let input_data = b"Hello, world!";
785            let mut compressed_data = Vec::new();
786
787            compressor
788                .compress(input_data, &mut compressed_data)
789                .unwrap();
790            let mut decompressed_data = Vec::new();
791            compressor
792                .decompress(&compressed_data, &mut decompressed_data)
793                .unwrap();
794            assert_eq!(input_data, decompressed_data.as_slice());
795        }
796
797        #[test_log::test(tokio::test)]
798        async fn test_lz4_compress_round_trip() {
799            for data_type in &[
800                DataType::Utf8,
801                DataType::LargeUtf8,
802                DataType::Binary,
803                DataType::LargeBinary,
804            ] {
805                let field = Field::new("", data_type.clone(), false);
806                let mut field_meta = HashMap::new();
807                field_meta.insert(COMPRESSION_META_KEY.to_string(), "lz4".to_string());
808                // Some bad cardinality estimatation causes us to use dictionary encoding currently
809                // which causes the expected encoding check to fail.
810                field_meta.insert(DICT_DIVISOR_META_KEY.to_string(), "100000".to_string());
811                field_meta.insert(DICT_SIZE_RATIO_META_KEY.to_string(), "0.0001".to_string());
812                // Also disable size-based dictionary encoding
813                field_meta.insert(
814                    STRUCTURAL_ENCODING_META_KEY.to_string(),
815                    STRUCTURAL_ENCODING_FULLZIP.to_string(),
816                );
817                let field = field.with_metadata(field_meta);
818                let test_cases = TestCases::basic()
819                    // Need to use large pages as small pages might be too small to compress
820                    .with_page_sizes(vec![1024 * 1024])
821                    .with_expected_encoding("zstd")
822                    .with_structural_encodings();
823
824                // Can't use the default random provider because random data isn't compressible
825                // and we will fallback to uncompressed encoding
826                let datagen = Box::new(FnArrayGeneratorProvider::new(move || match data_type {
827                    DataType::Utf8 => utf8_prefix_plus_counter("compressme", false),
828                    DataType::Binary => {
829                        binary_prefix_plus_counter(Arc::from(b"compressme".to_owned()), false)
830                    }
831                    DataType::LargeUtf8 => utf8_prefix_plus_counter("compressme", true),
832                    DataType::LargeBinary => {
833                        binary_prefix_plus_counter(Arc::from(b"compressme".to_owned()), true)
834                    }
835                    _ => panic!("Unsupported data type: {:?}", data_type),
836                }));
837
838                check_round_trip_encoding_generated(field, datagen, test_cases).await;
839            }
840        }
841    }
842}