Skip to main content

lance_encoding/
compression.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Compression traits and definitions for Lance 2.1
5//!
6//! In 2.1 the first step of encoding is structural encoding, where we shred inputs into
7//! leaf arrays and take care of the validity / offsets structure.  Then we pick a structural
8//! encoding (mini-block or full-zip) and then we compress the data.
9//!
10//! This module defines the traits for the compression step.  Each structural encoding has its
11//! own compression strategy.
12//!
13//! Miniblock compression is a block based approach for small data.  Since we introduce some read
14//! amplification and decompress entire blocks we are able to use opaque compression.
15//!
16//! Fullzip compression is a per-value approach where we require that values are transparently
17//! compressed so that we can locate them later.
18
19#[cfg(feature = "bitpacking")]
20use crate::encodings::physical::bitpacking::{InlineBitpacking, OutOfLineBitpacking};
21use crate::{
22    buffer::LanceBuffer,
23    compression_config::{BssMode, CompressionFieldParams},
24    constants::{
25        BSS_META_KEY, COMPRESSION_LEVEL_META_KEY, COMPRESSION_META_KEY, RLE_THRESHOLD_META_KEY,
26    },
27    data::{DataBlock, FixedWidthDataBlock, VariableWidthBlock},
28    encodings::{
29        logical::primitive::{
30            fullzip::PerValueCompressor,
31            miniblock::{MAX_MINIBLOCK_VALUES, MiniBlockCompressor},
32        },
33        physical::{
34            binary::{
35                BinaryBlockDecompressor, BinaryMiniBlockDecompressor, BinaryMiniBlockEncoder,
36                VariableDecoder, VariableEncoder,
37            },
38            block::{
39                CompressedBufferEncoder, CompressionConfig, CompressionScheme,
40                GeneralBlockDecompressor,
41            },
42            byte_stream_split::{
43                ByteStreamSplitDecompressor, ByteStreamSplitEncoder, should_use_bss,
44            },
45            constant::ConstantDecompressor,
46            fsst::{
47                FsstMiniBlockDecompressor, FsstMiniBlockEncoder, FsstPerValueDecompressor,
48                FsstPerValueEncoder,
49            },
50            general::{GeneralMiniBlockCompressor, GeneralMiniBlockDecompressor},
51            packed::{
52                PackedStructFixedPerValueDecompressor, PackedStructFixedPerValueEncoder,
53                PackedStructFixedWidthMiniBlockDecompressor,
54                PackedStructFixedWidthMiniBlockEncoder, PackedStructVariablePerValueDecompressor,
55                PackedStructVariablePerValueEncoder, VariablePackedStructFieldDecoder,
56                VariablePackedStructFieldKind,
57            },
58            rle::{
59                RleChildDecompressor, RleDecompressor, RleEncoder, RunLengthWidth,
60                rle_encoded_size, select_run_length_width,
61            },
62            value::{ValueDecompressor, ValueEncoder},
63        },
64    },
65    format::{
66        ProtobufUtils21,
67        pb21::{CompressiveEncoding, compressive_encoding::Compression},
68    },
69    statistics::{GetStat, Stat},
70};
71
72use arrow_array::{cast::AsArray, types::UInt64Type};
73use arrow_schema::DataType;
74use fsst::fsst::{FSST_LEAST_INPUT_MAX_LENGTH, FSST_LEAST_INPUT_SIZE};
75use lance_core::{Error, Result, datatypes::Field, error::LanceOptionExt};
76use std::{str::FromStr, sync::Arc};
77
78/// Default threshold for RLE compression selection when the user explicitly provides a threshold.
79///
80/// If no threshold is provided, we use a size model instead of a fixed run ratio.
81/// This preserves existing behavior for users relying on the default, while making
82/// the default selection more type-aware.
83const DEFAULT_RLE_COMPRESSION_THRESHOLD: f64 = 0.5;
84
85// Minimum block size (32kb) to trigger general block compression
86const MIN_BLOCK_SIZE_FOR_GENERAL_COMPRESSION: u64 = 32 * 1024;
87const RLE_BLOCK_HEADER_BYTES: u128 = std::mem::size_of::<u64>() as u128;
88
89/// Trait for compression algorithms that compress an entire block of data into one opaque
90/// and self-described chunk.
91///
92/// This is actually a _third_ compression strategy used in a few corner cases today (TODO: remove?)
93///
94/// This is the most general type of compression.  There are no constraints on the method
95/// of compression it is assumed that the entire block of data will be present at decompression.
96///
97/// This is the least appropriate strategy for random access because we must load the entire
98/// block to access any single value.  This should only be used for cases where random access is never
99/// required (e.g. when encoding metadata buffers like a dictionary or for encoding rep/def
100/// mini-block chunks)
101pub trait BlockCompressor: std::fmt::Debug + Send + Sync {
102    /// Compress the data into a single buffer
103    ///
104    /// Also returns a description of the compression that can be used to decompress
105    /// when reading the data back
106    fn compress(&self, data: DataBlock) -> Result<LanceBuffer>;
107}
108
109/// A trait to pick which compression to use for given data
110///
111/// There are several different kinds of compression.
112///
113/// - Block compression is the most generic, but most difficult to use efficiently
114/// - Per-value compression results in either a fixed width data block or a variable
115///   width data block.  In other words, there is some number of bits per value.
116///   In addition, each value should be independently decompressible.
117/// - Mini-block compression results in a small block of opaque data for chunks
118///   of rows.  Each block is somewhere between 0 and 16KiB in size.  This is
119///   used for narrow data types (both fixed and variable length) where we can
120///   fit many values into an 16KiB block.
121pub trait CompressionStrategy: Send + Sync + std::fmt::Debug {
122    /// Create a block compressor for the given data
123    fn create_block_compressor(
124        &self,
125        field: &Field,
126        data: &DataBlock,
127    ) -> Result<(Box<dyn BlockCompressor>, CompressiveEncoding)>;
128
129    /// Create a per-value compressor for the given data
130    fn create_per_value(
131        &self,
132        field: &Field,
133        data: &DataBlock,
134    ) -> Result<Box<dyn PerValueCompressor>>;
135
136    /// Create a mini-block compressor for the given data
137    fn create_miniblock_compressor(
138        &self,
139        field: &Field,
140        data: &DataBlock,
141    ) -> Result<Box<dyn MiniBlockCompressor>>;
142}
143
144fn try_bss_for_mini_block(
145    data: &FixedWidthDataBlock,
146    params: &CompressionFieldParams,
147) -> Option<Box<dyn MiniBlockCompressor>> {
148    // BSS requires general compression to be effective
149    // If compression is not set or explicitly disabled, skip BSS
150    if params.compression.is_none() || params.compression.as_deref() == Some("none") {
151        return None;
152    }
153
154    let mode = params.bss.unwrap_or(BssMode::Auto);
155    // should_use_bss already checks for supported bit widths (32/64)
156    if should_use_bss(data, mode) {
157        return Some(Box::new(ByteStreamSplitEncoder::new(
158            data.bits_per_value as usize,
159        )));
160    }
161    None
162}
163
164fn rle_is_applicable(data: &FixedWidthDataBlock, params: &CompressionFieldParams) -> Option<u128> {
165    let bits = data.bits_per_value;
166    if !matches!(bits, 8 | 16 | 32 | 64) {
167        return None;
168    }
169
170    let type_size = bits / 8;
171    let run_count = data.expect_single_stat::<UInt64Type>(Stat::RunCount);
172    let threshold = params
173        .rle_threshold
174        .unwrap_or(DEFAULT_RLE_COMPRESSION_THRESHOLD);
175
176    // If the user explicitly provided a threshold then honor it as an additional guard.
177    // A lower threshold makes RLE harder to trigger and can be used to avoid CPU overhead.
178    let passes_threshold = match params.rle_threshold {
179        Some(_) => (run_count as f64) < (data.num_values as f64) * threshold,
180        None => true,
181    };
182
183    if !passes_threshold {
184        return None;
185    }
186
187    Some((data.num_values as u128) * (type_size as u128))
188}
189
190fn rle_beats_raw_and_bitpacking(
191    data: &FixedWidthDataBlock,
192    encoded_bytes: u128,
193    raw_bytes: u128,
194) -> bool {
195    if encoded_bytes >= raw_bytes {
196        return false;
197    }
198
199    #[cfg(feature = "bitpacking")]
200    {
201        if let Some(bitpack_bytes) = estimate_inline_bitpacking_bytes(data).map(u128::from)
202            && bitpack_bytes < encoded_bytes
203        {
204            return false;
205        }
206    }
207    true
208}
209
210fn try_fixed_u8_rle_for_mini_block(
211    data: &FixedWidthDataBlock,
212    params: &CompressionFieldParams,
213) -> Option<Box<dyn MiniBlockCompressor>> {
214    let raw_bytes = rle_is_applicable(data, params)?;
215    let rle_bytes = estimate_rle_size_for_width_from_data(
216        data,
217        Some(*MAX_MINIBLOCK_VALUES),
218        RunLengthWidth::U8,
219    )
220    .ok()?;
221    rle_beats_raw_and_bitpacking(data, rle_bytes, raw_bytes)
222        .then(|| Box::new(RleEncoder::with_run_length_width(RunLengthWidth::U8)) as _)
223}
224
225fn try_child_rle_for_mini_block(
226    data: &FixedWidthDataBlock,
227    params: &CompressionFieldParams,
228) -> Option<Box<dyn MiniBlockCompressor>> {
229    let raw_bytes = rle_is_applicable(data, params)?;
230    let (run_length_width, estimated_bytes) =
231        estimate_rle_width_and_size_from_data(data, Some(*MAX_MINIBLOCK_VALUES)).ok()?;
232    let child_compression = rle_child_compression_config(params);
233    let encoder = || {
234        RleEncoder::with_child_encoding(
235            run_length_width,
236            child_compression,
237            child_compression,
238            true,
239        )
240    };
241
242    #[cfg(feature = "bitpacking")]
243    let bitpack_bytes = estimate_inline_bitpacking_bytes(data).map(u128::from);
244    #[cfg(not(feature = "bitpacking"))]
245    let bitpack_bytes = None::<u128>;
246
247    let should_measure_children = (child_compression.is_some() || cfg!(feature = "bitpacking"))
248        && (estimated_bytes >= raw_bytes
249            || bitpack_bytes.is_some_and(|bytes| bytes < estimated_bytes));
250    let selected_bytes = if should_measure_children {
251        encoder().selected_payload_size(data).ok()?
252    } else {
253        estimated_bytes
254    };
255
256    rle_beats_raw_and_bitpacking(data, selected_bytes, raw_bytes).then(|| Box::new(encoder()) as _)
257}
258
259fn rle_child_compression_config(params: &CompressionFieldParams) -> Option<CompressionConfig> {
260    let raw = params.compression.as_deref()?;
261    if matches!(raw, "none" | "fsst") {
262        return None;
263    }
264    let scheme = CompressionScheme::from_str(raw).ok()?;
265    Some(CompressionConfig::new(scheme, params.compression_level))
266}
267
268fn try_rle_for_block_with_width(
269    data: &FixedWidthDataBlock,
270    params: &CompressionFieldParams,
271    run_length_width: RunLengthWidth,
272    rle_payload_bytes: u128,
273) -> Result<Option<(Box<dyn BlockCompressor>, CompressiveEncoding)>> {
274    let bits = data.bits_per_value;
275    if !matches!(bits, 8 | 16 | 32 | 64) {
276        return Ok(None);
277    }
278
279    let run_count = data.expect_single_stat::<UInt64Type>(Stat::RunCount);
280    let threshold = params
281        .rle_threshold
282        .unwrap_or(DEFAULT_RLE_COMPRESSION_THRESHOLD);
283
284    let passes_threshold = match params.rle_threshold {
285        Some(_) => (run_count as f64) < (data.num_values as f64) * threshold,
286        None => true,
287    };
288
289    if !passes_threshold {
290        return Ok(None);
291    }
292
293    let raw_bytes = (data.num_values as u128) * ((bits / 8) as u128);
294    let rle_bytes = rle_payload_bytes.saturating_add(RLE_BLOCK_HEADER_BYTES);
295
296    if rle_bytes >= raw_bytes {
297        return Ok(None);
298    }
299
300    #[cfg(feature = "bitpacking")]
301    {
302        if let Some(bitpack_bytes) = estimate_block_bitpacking_bytes(data)
303            && bitpack_bytes < rle_bytes
304        {
305            return Ok(None);
306        }
307    }
308
309    let compressor = Box::new(RleEncoder::with_run_length_width(run_length_width));
310    let encoding = ProtobufUtils21::rle(
311        ProtobufUtils21::flat(bits, None),
312        ProtobufUtils21::flat(run_length_width.bits_per_value(), None),
313    );
314    Ok(Some((compressor, encoding)))
315}
316
317fn try_fixed_u8_rle_for_block(
318    data: &FixedWidthDataBlock,
319    params: &CompressionFieldParams,
320) -> Result<Option<(Box<dyn BlockCompressor>, CompressiveEncoding)>> {
321    if !matches!(data.bits_per_value, 8 | 16 | 32 | 64) {
322        return Ok(None);
323    }
324    let encoded_bytes = estimate_rle_size_for_width_from_data(data, None, RunLengthWidth::U8)?;
325    try_rle_for_block_with_width(data, params, RunLengthWidth::U8, encoded_bytes)
326}
327
328fn try_variable_rle_for_block(
329    data: &FixedWidthDataBlock,
330    params: &CompressionFieldParams,
331) -> Result<Option<(Box<dyn BlockCompressor>, CompressiveEncoding)>> {
332    if !matches!(data.bits_per_value, 8 | 16 | 32 | 64) {
333        return Ok(None);
334    }
335    let (width, encoded_bytes) = estimate_rle_width_and_size_from_data(data, None)?;
336    try_rle_for_block_with_width(data, params, width, encoded_bytes)
337}
338
339fn estimate_rle_width_and_size_from_data(
340    data: &FixedWidthDataBlock,
341    max_segment_values: Option<u64>,
342) -> Result<(RunLengthWidth, u128)> {
343    select_run_length_width(
344        &data.data,
345        data.num_values,
346        data.bits_per_value,
347        max_segment_values,
348    )
349}
350
351fn estimate_rle_size_for_width_from_data(
352    data: &FixedWidthDataBlock,
353    max_segment_values: Option<u64>,
354    run_length_width: RunLengthWidth,
355) -> Result<u128> {
356    rle_encoded_size(
357        &data.data,
358        data.num_values,
359        data.bits_per_value,
360        max_segment_values,
361        run_length_width,
362    )
363}
364
365fn try_bitpack_for_mini_block(_data: &FixedWidthDataBlock) -> Option<Box<dyn MiniBlockCompressor>> {
366    #[cfg(feature = "bitpacking")]
367    {
368        let bits = _data.bits_per_value;
369        if estimate_inline_bitpacking_bytes(_data).is_some() {
370            return Some(Box::new(InlineBitpacking::new(bits)));
371        }
372        None
373    }
374    #[cfg(not(feature = "bitpacking"))]
375    {
376        None
377    }
378}
379
380#[cfg(feature = "bitpacking")]
381fn estimate_inline_bitpacking_bytes(data: &FixedWidthDataBlock) -> Option<u64> {
382    use arrow_array::cast::AsArray;
383
384    let bits = data.bits_per_value;
385    if !matches!(bits, 8 | 16 | 32 | 64) {
386        return None;
387    }
388    if data.num_values == 0 {
389        return None;
390    }
391
392    let bit_widths = data.expect_stat(Stat::BitWidth);
393    let widths = bit_widths.as_primitive::<UInt64Type>();
394
395    let words_per_chunk: u128 = 1;
396    let word_bytes: u128 = (bits / 8) as u128;
397    let mut total_words: u128 = 0;
398    for i in 0..widths.len() {
399        let bit_width = widths.value(i) as u128;
400        let packed_words = (1024u128 * bit_width) / (bits as u128);
401        total_words = total_words.saturating_add(words_per_chunk.saturating_add(packed_words));
402    }
403
404    let estimated_bytes = total_words.saturating_mul(word_bytes);
405    let raw_bytes = data.data_size() as u128;
406
407    if estimated_bytes >= raw_bytes {
408        return None;
409    }
410
411    u64::try_from(estimated_bytes).ok()
412}
413
414fn try_bitpack_for_block(
415    data: &FixedWidthDataBlock,
416) -> Option<(Box<dyn BlockCompressor>, CompressiveEncoding)> {
417    let bits = data.bits_per_value;
418    if !matches!(bits, 8 | 16 | 32 | 64) {
419        return None;
420    }
421
422    let bit_widths = data.expect_stat(Stat::BitWidth);
423    let widths = bit_widths.as_primitive::<UInt64Type>();
424    let max_bit_width = *widths.values().iter().max().unwrap();
425
426    let too_small =
427        widths.len() == 1 && InlineBitpacking::min_size_bytes(widths.value(0)) >= data.data_size();
428
429    if too_small {
430        return None;
431    }
432
433    if data.num_values <= 1024 {
434        let compressor = Box::new(InlineBitpacking::new(bits));
435        let encoding = ProtobufUtils21::inline_bitpacking(bits, None);
436        Some((compressor, encoding))
437    } else {
438        let compressor = Box::new(OutOfLineBitpacking::new(max_bit_width, bits));
439        let encoding = ProtobufUtils21::out_of_line_bitpacking(
440            bits,
441            ProtobufUtils21::flat(max_bit_width, None),
442        );
443        Some((compressor, encoding))
444    }
445}
446
447#[cfg(feature = "bitpacking")]
448fn estimate_block_bitpacking_bytes(data: &FixedWidthDataBlock) -> Option<u128> {
449    let bits = data.bits_per_value;
450    if !matches!(bits, 8 | 16 | 32 | 64) || data.num_values == 0 {
451        return None;
452    }
453
454    let bit_widths = data.expect_stat(Stat::BitWidth);
455    let widths = bit_widths.as_primitive::<UInt64Type>();
456    let max_bit_width = *widths.values().iter().max()?;
457    let word_bytes = (bits / 8) as u128;
458
459    let bitpacked_words = if data.num_values <= 1024 {
460        1 + (1024u128 * (max_bit_width as u128)) / (bits as u128)
461    } else {
462        estimate_out_of_line_bitpacking_words(data.num_values, max_bit_width, bits)?
463    };
464    let bitpacked_bytes = bitpacked_words.saturating_mul(word_bytes);
465    if bitpacked_bytes >= data.data_size() as u128 {
466        return None;
467    }
468
469    Some(bitpacked_bytes)
470}
471
472#[cfg(feature = "bitpacking")]
473fn estimate_out_of_line_bitpacking_words(
474    num_values: u64,
475    compressed_bits_per_value: u64,
476    bits_per_value: u64,
477) -> Option<u128> {
478    let num_values = usize::try_from(num_values).ok()?;
479    let compressed_bits_per_value = usize::try_from(compressed_bits_per_value).ok()?;
480    let bits_per_value = usize::try_from(bits_per_value).ok()?;
481    if compressed_bits_per_value >= bits_per_value {
482        return None;
483    }
484
485    let elems_per_chunk = 1024usize;
486    let num_chunks = num_values.div_ceil(elems_per_chunk);
487    let words_per_chunk = (elems_per_chunk * compressed_bits_per_value).div_ceil(bits_per_value);
488    let last_chunk_is_runt = !num_values.is_multiple_of(elems_per_chunk);
489
490    if !last_chunk_is_runt {
491        return Some((num_chunks * words_per_chunk) as u128);
492    }
493
494    let num_whole_chunks = num_chunks - 1;
495    let remaining_items = num_values - num_whole_chunks * elems_per_chunk;
496    let tail_bit_savings = bits_per_value - compressed_bits_per_value;
497    let padding_cost = compressed_bits_per_value * (elems_per_chunk - remaining_items);
498    let tail_pack_savings = tail_bit_savings * remaining_items;
499    let tail_words = if padding_cost < tail_pack_savings {
500        words_per_chunk
501    } else {
502        remaining_items
503    };
504
505    Some((num_whole_chunks * words_per_chunk + tail_words) as u128)
506}
507
508fn maybe_wrap_general_for_mini_block(
509    inner: Box<dyn MiniBlockCompressor>,
510    params: &CompressionFieldParams,
511) -> Result<Box<dyn MiniBlockCompressor>> {
512    match params.compression.as_deref() {
513        None | Some("none") | Some("fsst") => Ok(inner),
514        Some(raw) => {
515            let scheme = CompressionScheme::from_str(raw)
516                .map_err(|_| Error::invalid_input(format!("Unknown compression scheme: {raw}")))?;
517            let cfg = CompressionConfig::new(scheme, params.compression_level);
518            Ok(Box::new(GeneralMiniBlockCompressor::new(inner, cfg)))
519        }
520    }
521}
522
523fn try_general_compression(
524    field_params: &CompressionFieldParams,
525    data: &DataBlock,
526) -> Result<Option<(Box<dyn BlockCompressor>, CompressionConfig)>> {
527    // Explicitly disable general compression.
528    if field_params.compression.as_deref() == Some("none") {
529        return Ok(None);
530    }
531
532    // User-requested compression (unused today but perhaps still used
533    // in the future someday)
534    if let Some(compression_scheme) = &field_params.compression {
535        let scheme: CompressionScheme = compression_scheme.parse()?;
536        let config = CompressionConfig::new(scheme, field_params.compression_level);
537        let compressor = Box::new(CompressedBufferEncoder::try_new(config)?);
538        return Ok(Some((compressor, config)));
539    }
540
541    // Automatic compression for large blocks
542    if data.data_size() > MIN_BLOCK_SIZE_FOR_GENERAL_COMPRESSION {
543        let compressor = Box::new(CompressedBufferEncoder::default());
544        let config = compressor.compressor.config();
545        return Ok(Some((compressor, config)));
546    }
547
548    Ok(None)
549}
550
551/// Parse field-level compression metadata without applying format-specific constraints.
552pub fn field_metadata_params(field: &Field) -> CompressionFieldParams {
553    let mut params = CompressionFieldParams::default();
554
555    if let Some(compression) = field.metadata.get(COMPRESSION_META_KEY) {
556        params.compression = Some(compression.clone());
557    }
558    if let Some(level) = field.metadata.get(COMPRESSION_LEVEL_META_KEY) {
559        params.compression_level = level.parse().ok();
560    }
561    if let Some(threshold) = field.metadata.get(RLE_THRESHOLD_META_KEY) {
562        params.rle_threshold = threshold.parse().ok();
563    }
564    if let Some(bss_str) = field.metadata.get(BSS_META_KEY) {
565        match BssMode::parse(bss_str) {
566            Some(mode) => params.bss = Some(mode),
567            None => log::warn!("Invalid BSS mode '{}', using default", bss_str),
568        }
569    }
570    if let Some(minichunk_size_str) = field
571        .metadata
572        .get(super::constants::MINICHUNK_SIZE_META_KEY)
573    {
574        if let Ok(minichunk_size) = minichunk_size_str.parse::<i64>() {
575            params.minichunk_size = Some(minichunk_size);
576        } else {
577            log::warn!("Invalid minichunk_size '{}', skipping", minichunk_size_str);
578        }
579    }
580
581    params
582}
583
584/// Apply general-purpose compression requested for a fixed-width miniblock.
585pub fn finalize_miniblock_compressor(
586    data: &DataBlock,
587    compressor: Box<dyn MiniBlockCompressor>,
588    params: &CompressionFieldParams,
589) -> Result<Box<dyn MiniBlockCompressor>> {
590    if matches!(data, DataBlock::FixedWidth(_)) {
591        maybe_wrap_general_for_mini_block(compressor, params)
592    } else {
593        Ok(compressor)
594    }
595}
596
597/// Honor an explicit `compression = none` request for fixed-width miniblocks.
598pub fn try_uncompressed_fixed_width_miniblock(
599    data: &DataBlock,
600    params: &CompressionFieldParams,
601) -> Option<Box<dyn MiniBlockCompressor>> {
602    (matches!(data, DataBlock::FixedWidth(_)) && params.compression.as_deref() == Some("none"))
603        .then(|| Box::new(ValueEncoder::default()) as _)
604}
605
606/// Select byte-stream-split compression for an applicable fixed-width miniblock.
607pub fn try_byte_stream_split_miniblock(
608    data: &DataBlock,
609    params: &CompressionFieldParams,
610) -> Option<Box<dyn MiniBlockCompressor>> {
611    let DataBlock::FixedWidth(data) = data else {
612        return None;
613    };
614    try_bss_for_mini_block(data, params)
615}
616
617/// Select the original fixed-u8 RLE miniblock grammar.
618pub fn try_fixed_u8_rle_miniblock(
619    data: &DataBlock,
620    params: &CompressionFieldParams,
621) -> Option<Box<dyn MiniBlockCompressor>> {
622    let DataBlock::FixedWidth(data) = data else {
623        return None;
624    };
625    try_fixed_u8_rle_for_mini_block(data, params)
626}
627
628/// Select variable-width RLE with independently encoded children.
629pub fn try_child_rle_miniblock(
630    data: &DataBlock,
631    params: &CompressionFieldParams,
632) -> Option<Box<dyn MiniBlockCompressor>> {
633    let DataBlock::FixedWidth(data) = data else {
634        return None;
635    };
636    try_child_rle_for_mini_block(data, params)
637}
638
639/// Select inline bitpacking for applicable fixed-width miniblocks.
640pub fn try_bitpacking_miniblock(data: &DataBlock) -> Option<Box<dyn MiniBlockCompressor>> {
641    let DataBlock::FixedWidth(data) = data else {
642        return None;
643    };
644    try_bitpack_for_mini_block(data)
645}
646
647/// Store fixed-width miniblock values without a value codec.
648pub fn try_raw_fixed_width_miniblock(data: &DataBlock) -> Option<Box<dyn MiniBlockCompressor>> {
649    matches!(data, DataBlock::FixedWidth(_)).then(|| Box::new(ValueEncoder::default()) as _)
650}
651
652/// Encode variable-width miniblocks with binary or FSST encoding.
653pub fn try_variable_width_miniblock(
654    field: &Field,
655    data: &DataBlock,
656    params: &CompressionFieldParams,
657) -> Result<Option<Box<dyn MiniBlockCompressor>>> {
658    let DataBlock::VariableWidth(data) = data else {
659        return Ok(None);
660    };
661    if data.bits_per_offset != 32 && data.bits_per_offset != 64 {
662        return Err(Error::invalid_input(format!(
663            "Variable width compression not supported for {} bit offsets",
664            data.bits_per_offset
665        )));
666    }
667
668    let compression = params.compression.as_deref();
669    let data_size = data.expect_single_stat::<UInt64Type>(Stat::DataSize);
670    let max_len = data.expect_single_stat::<UInt64Type>(Stat::MaxLength);
671    if compression == Some("none") {
672        return Ok(Some(Box::new(BinaryMiniBlockEncoder::new(
673            params.minichunk_size,
674        ))));
675    }
676
677    let use_fsst = compression == Some("fsst")
678        || (compression.is_none()
679            && !matches!(field.data_type(), DataType::Binary | DataType::LargeBinary)
680            && max_len >= FSST_LEAST_INPUT_MAX_LENGTH
681            && data_size >= FSST_LEAST_INPUT_SIZE as u64);
682    let mut encoder: Box<dyn MiniBlockCompressor> = if use_fsst {
683        Box::new(FsstMiniBlockEncoder::new(params.minichunk_size))
684    } else {
685        Box::new(BinaryMiniBlockEncoder::new(params.minichunk_size))
686    };
687    if let Some(compression_scheme) = compression.filter(|scheme| *scheme != "fsst") {
688        let scheme: CompressionScheme = compression_scheme.parse()?;
689        let config = CompressionConfig::new(scheme, params.compression_level);
690        encoder = Box::new(GeneralMiniBlockCompressor::new(encoder, config));
691    }
692    Ok(Some(encoder))
693}
694
695/// Encode fixed-width packed structs as miniblocks.
696pub fn try_fixed_packed_struct_miniblock(
697    data: &DataBlock,
698) -> Result<Option<Box<dyn MiniBlockCompressor>>> {
699    let DataBlock::Struct(data) = data else {
700        return Ok(None);
701    };
702    if data.has_variable_width_child() {
703        return Err(Error::invalid_input(
704            "Packed struct mini-block encoding supports only fixed-width children",
705        ));
706    }
707    Ok(Some(Box::new(
708        PackedStructFixedWidthMiniBlockEncoder::default(),
709    )))
710}
711
712/// Store fixed-size-list miniblocks without a value codec.
713pub fn try_raw_fixed_size_list_miniblock(data: &DataBlock) -> Option<Box<dyn MiniBlockCompressor>> {
714    matches!(data, DataBlock::FixedSizeList(_)).then(|| Box::new(ValueEncoder::default()) as _)
715}
716
717/// Store fixed-width and fixed-size-list values directly in full-zip pages.
718pub fn try_raw_per_value(data: &DataBlock) -> Option<Box<dyn PerValueCompressor>> {
719    matches!(data, DataBlock::FixedWidth(_) | DataBlock::FixedSizeList(_))
720        .then(|| Box::new(ValueEncoder::default()) as _)
721}
722
723fn validate_packed_struct(field: &Field, data: &DataBlock) -> Result<Option<bool>> {
724    let DataBlock::Struct(data) = data else {
725        return Ok(None);
726    };
727    if field.children.len() != data.children.len() {
728        return Err(Error::invalid_input(
729            "Struct field metadata does not match data block children",
730        ));
731    }
732    Ok(Some(data.has_variable_width_child()))
733}
734
735/// Reject variable-width packed structs while preserving the fixed-width error.
736pub fn reject_packed_struct_per_value(
737    field: &Field,
738    data: &DataBlock,
739) -> Result<Option<Box<dyn PerValueCompressor>>> {
740    let Some(has_variable_child) = validate_packed_struct(field, data)? else {
741        return Ok(None);
742    };
743    if has_variable_child {
744        return Err(Error::not_supported_source(
745            "Variable packed struct encoding is not enabled by the selected file format".into(),
746        ));
747    }
748    Err(Error::invalid_input(
749        "Packed struct per-value compression should not be used for fixed-width-only structs",
750    ))
751}
752
753/// Encode variable-width packed structs with the exact strategy recursively.
754pub fn try_variable_packed_struct_per_value(
755    strategy: Arc<dyn CompressionStrategy>,
756    field: &Field,
757    data: &DataBlock,
758) -> Result<Option<Box<dyn PerValueCompressor>>> {
759    let Some(has_variable_child) = validate_packed_struct(field, data)? else {
760        return Ok(None);
761    };
762    if !has_variable_child {
763        return Err(Error::invalid_input(
764            "Packed struct per-value compression should not be used for fixed-width-only structs",
765        ));
766    }
767    Ok(Some(Box::new(PackedStructVariablePerValueEncoder::new(
768        strategy,
769        field.children.clone(),
770    ))))
771}
772
773/// Encode all packed structs with the exact strategy recursively.
774pub fn try_packed_struct_per_value(
775    strategy: Arc<dyn CompressionStrategy>,
776    field: &Field,
777    data: &DataBlock,
778) -> Result<Option<Box<dyn PerValueCompressor>>> {
779    let Some(has_variable_child) = validate_packed_struct(field, data)? else {
780        return Ok(None);
781    };
782    if has_variable_child {
783        return Ok(Some(Box::new(PackedStructVariablePerValueEncoder::new(
784            strategy,
785            field.children.clone(),
786        ))));
787    }
788
789    Ok(Some(Box::new(PackedStructFixedPerValueEncoder::new(
790        field.children.clone(),
791    ))))
792}
793
794/// Encode variable-width values directly, with FSST or per-value compression
795/// when applicable.
796pub fn try_variable_width_per_value(
797    field: &Field,
798    data: &DataBlock,
799    params: &CompressionFieldParams,
800) -> Result<Option<Box<dyn PerValueCompressor>>> {
801    let DataBlock::VariableWidth(data) = data else {
802        return Ok(None);
803    };
804    let compression = params.compression.as_deref();
805    if compression == Some("none") {
806        return Ok(Some(Box::new(VariableEncoder::default())));
807    }
808
809    let max_len = data.expect_single_stat::<UInt64Type>(Stat::MaxLength);
810    let data_size = data.expect_single_stat::<UInt64Type>(Stat::DataSize);
811    let per_value_requested = compression.is_some_and(|compression| compression != "fsst");
812    if (max_len > 32 * 1024 || per_value_requested) && data_size >= FSST_LEAST_INPUT_SIZE as u64 {
813        if compression == Some("zstd") {
814            let config = CompressionConfig::new(CompressionScheme::Zstd, params.compression_level);
815            return Ok(Some(Box::new(CompressedBufferEncoder::try_new(config)?)));
816        }
817        return Ok(Some(Box::new(CompressedBufferEncoder::default())));
818    }
819
820    if data.bits_per_offset != 32 && data.bits_per_offset != 64 {
821        return Err(Error::invalid_input(format!(
822            "Per-value compression does not support variable-width data with {}-bit offsets",
823            data.bits_per_offset
824        )));
825    }
826    let encoder = Box::new(VariableEncoder::default());
827    let use_fsst = compression == Some("fsst")
828        || (compression.is_none()
829            && !matches!(field.data_type(), DataType::Binary | DataType::LargeBinary)
830            && max_len >= FSST_LEAST_INPUT_MAX_LENGTH
831            && data_size >= FSST_LEAST_INPUT_SIZE as u64);
832    Ok(Some(if use_fsst {
833        Box::new(FsstPerValueEncoder::new(encoder))
834    } else {
835        encoder
836    }))
837}
838
839/// Select fixed-u8 RLE for block compression.
840pub fn try_fixed_u8_rle_block(
841    data: &DataBlock,
842    params: &CompressionFieldParams,
843) -> Result<Option<(Box<dyn BlockCompressor>, CompressiveEncoding)>> {
844    let DataBlock::FixedWidth(data) = data else {
845        return Ok(None);
846    };
847    try_fixed_u8_rle_for_block(data, params)
848}
849
850/// Select variable-width RLE for block compression.
851pub fn try_variable_rle_block(
852    data: &DataBlock,
853    params: &CompressionFieldParams,
854) -> Result<Option<(Box<dyn BlockCompressor>, CompressiveEncoding)>> {
855    let DataBlock::FixedWidth(data) = data else {
856        return Ok(None);
857    };
858    try_variable_rle_for_block(data, params)
859}
860
861/// Select block bitpacking for applicable fixed-width values.
862pub fn try_bitpacking_block(
863    data: &DataBlock,
864) -> Option<(Box<dyn BlockCompressor>, CompressiveEncoding)> {
865    let DataBlock::FixedWidth(data) = data else {
866        return None;
867    };
868    try_bitpack_for_block(data)
869}
870
871/// Select explicitly requested or automatic general-purpose block compression.
872pub fn try_general_block(
873    data: &DataBlock,
874    params: &CompressionFieldParams,
875) -> Result<Option<(Box<dyn BlockCompressor>, CompressiveEncoding)>> {
876    let Some((compressor, config)) = try_general_compression(params, data)? else {
877        return Ok(None);
878    };
879    let inner = match data {
880        DataBlock::FixedWidth(data) => ProtobufUtils21::flat(data.bits_per_value, None),
881        DataBlock::VariableWidth(data) => ProtobufUtils21::variable(
882            ProtobufUtils21::flat(data.bits_per_offset as u64, None),
883            None,
884        ),
885        _ => return Ok(None),
886    };
887    Ok(Some((compressor, ProtobufUtils21::wrapped(config, inner)?)))
888}
889
890/// Store fixed- and variable-width block values without block compression.
891pub fn try_raw_block(data: &DataBlock) -> Option<(Box<dyn BlockCompressor>, CompressiveEncoding)> {
892    match data {
893        DataBlock::FixedWidth(data) => Some((
894            Box::new(ValueEncoder::default()) as Box<dyn BlockCompressor>,
895            ProtobufUtils21::flat(data.bits_per_value, None),
896        )),
897        DataBlock::VariableWidth(data) => Some((
898            Box::new(VariableEncoder::default()) as Box<dyn BlockCompressor>,
899            ProtobufUtils21::variable(
900                ProtobufUtils21::flat(data.bits_per_offset as u64, None),
901                None,
902            ),
903        )),
904        _ => None,
905    }
906}
907
908pub trait MiniBlockDecompressor: std::fmt::Debug + Send + Sync {
909    fn decompress(&self, data: Vec<LanceBuffer>, num_values: u64) -> Result<DataBlock>;
910
911    /// Returns the exact aggregate decoded size when it is determined solely by the value count.
912    ///
913    /// Implementations should only return `Some` when this aggregate estimate can be used by
914    /// [`DataBlockBuilder`](crate::data::DataBlockBuilder) to preallocate the decoded output
915    /// exactly. Outputs with multiple buffers or whose layout-dependent allocation cannot be
916    /// represented by one aggregate estimate should return `None`.
917    fn decoded_size_bytes(&self, _num_values: u64) -> Option<u64> {
918        None
919    }
920}
921
922pub trait FixedPerValueDecompressor: std::fmt::Debug + Send + Sync {
923    /// Decompress one or more values
924    fn decompress(&self, data: FixedWidthDataBlock, num_values: u64) -> Result<DataBlock>;
925    /// The number of bits in each value
926    ///
927    /// Currently (and probably long term) this must be a multiple of 8
928    fn bits_per_value(&self) -> u64;
929
930    /// Returns the exact aggregate decoded size when it is determined solely by the value count.
931    ///
932    /// Implementations should only return `Some` when this aggregate estimate can be used by
933    /// [`DataBlockBuilder`](crate::data::DataBlockBuilder) to preallocate the decoded output
934    /// exactly. Outputs with multiple buffers or whose layout-dependent allocation cannot be
935    /// represented by one aggregate estimate should return `None`.
936    fn decoded_size_bytes(&self, _num_values: u64) -> Option<u64> {
937        None
938    }
939}
940
941pub trait VariablePerValueDecompressor: std::fmt::Debug + Send + Sync {
942    /// Decompress one or more values
943    fn decompress(&self, data: VariableWidthBlock) -> Result<DataBlock>;
944}
945
946pub trait BlockDecompressor: std::fmt::Debug + Send + Sync {
947    fn decompress(&self, data: LanceBuffer, num_values: u64) -> Result<DataBlock>;
948}
949
950pub trait DecompressionStrategy: std::fmt::Debug + Send + Sync {
951    fn create_miniblock_decompressor(
952        &self,
953        description: &CompressiveEncoding,
954        decompression_strategy: &dyn DecompressionStrategy,
955    ) -> Result<Box<dyn MiniBlockDecompressor>>;
956
957    fn create_fixed_per_value_decompressor(
958        &self,
959        description: &CompressiveEncoding,
960    ) -> Result<Box<dyn FixedPerValueDecompressor>>;
961
962    fn create_variable_per_value_decompressor(
963        &self,
964        description: &CompressiveEncoding,
965    ) -> Result<Box<dyn VariablePerValueDecompressor>>;
966
967    fn create_block_decompressor(
968        &self,
969        description: &CompressiveEncoding,
970    ) -> Result<Box<dyn BlockDecompressor>>;
971}
972
973#[derive(Debug, Default)]
974pub struct DefaultDecompressionStrategy {}
975
976impl DecompressionStrategy for DefaultDecompressionStrategy {
977    fn create_miniblock_decompressor(
978        &self,
979        description: &CompressiveEncoding,
980        decompression_strategy: &dyn DecompressionStrategy,
981    ) -> Result<Box<dyn MiniBlockDecompressor>> {
982        match description.compression.as_ref().unwrap() {
983            Compression::Flat(flat) => Ok(Box::new(ValueDecompressor::from_flat(flat))),
984            #[cfg(feature = "bitpacking")]
985            Compression::InlineBitpacking(description) => {
986                Ok(Box::new(InlineBitpacking::from_description(description)))
987            }
988            #[cfg(not(feature = "bitpacking"))]
989            Compression::InlineBitpacking(_) => Err(Error::not_supported_source(
990                "this runtime was not built with bitpacking support".into(),
991            )),
992            Compression::Variable(variable) => {
993                let Compression::Flat(offsets) = variable
994                    .offsets
995                    .as_ref()
996                    .unwrap()
997                    .compression
998                    .as_ref()
999                    .unwrap()
1000                else {
1001                    panic!("Variable compression only supports flat offsets")
1002                };
1003                Ok(Box::new(BinaryMiniBlockDecompressor::new(
1004                    offsets.bits_per_value as u8,
1005                )))
1006            }
1007            Compression::Fsst(description) => {
1008                let inner_decompressor = decompression_strategy.create_miniblock_decompressor(
1009                    description.values.as_ref().unwrap(),
1010                    decompression_strategy,
1011                )?;
1012                Ok(Box::new(FsstMiniBlockDecompressor::new(
1013                    description,
1014                    inner_decompressor,
1015                )))
1016            }
1017            Compression::PackedStruct(description) => Ok(Box::new(
1018                PackedStructFixedWidthMiniBlockDecompressor::new(description),
1019            )),
1020            Compression::VariablePackedStruct(_) => Err(Error::not_supported_source(
1021                "variable packed struct decoding is not yet implemented".into(),
1022            )),
1023            Compression::FixedSizeList(fsl) => {
1024                // In the future, we might need to do something more complex here if FSL supports
1025                // compression.
1026                Ok(Box::new(ValueDecompressor::from_fsl(fsl)))
1027            }
1028            Compression::Rle(rle) => Ok(Box::new(create_rle_decompressor(
1029                rle,
1030                decompression_strategy,
1031            )?)),
1032            Compression::ByteStreamSplit(bss) => {
1033                let Compression::Flat(values) =
1034                    bss.values.as_ref().unwrap().compression.as_ref().unwrap()
1035                else {
1036                    panic!("ByteStreamSplit compression only supports flat values")
1037                };
1038                Ok(Box::new(ByteStreamSplitDecompressor::new(
1039                    values.bits_per_value as usize,
1040                )))
1041            }
1042            Compression::General(general) => {
1043                // Create inner decompressor
1044                let inner_decompressor = self.create_miniblock_decompressor(
1045                    general.values.as_ref().ok_or_else(|| {
1046                        Error::invalid_input("GeneralMiniBlock missing inner encoding")
1047                    })?,
1048                    decompression_strategy,
1049                )?;
1050
1051                // Parse compression config
1052                let compression = general.compression.as_ref().ok_or_else(|| {
1053                    Error::invalid_input("GeneralMiniBlock missing compression config")
1054                })?;
1055
1056                let scheme = compression.scheme().try_into()?;
1057
1058                let compression_config = CompressionConfig::new(scheme, compression.level);
1059
1060                Ok(Box::new(GeneralMiniBlockDecompressor::new(
1061                    inner_decompressor,
1062                    compression_config,
1063                )))
1064            }
1065            _ => todo!(),
1066        }
1067    }
1068
1069    fn create_fixed_per_value_decompressor(
1070        &self,
1071        description: &CompressiveEncoding,
1072    ) -> Result<Box<dyn FixedPerValueDecompressor>> {
1073        match description.compression.as_ref().unwrap() {
1074            Compression::Constant(constant) => Ok(Box::new(ConstantDecompressor::new(
1075                constant
1076                    .value
1077                    .as_ref()
1078                    .map(|v| LanceBuffer::from_bytes(v.clone(), 1)),
1079            ))),
1080            Compression::Flat(flat) => Ok(Box::new(ValueDecompressor::from_flat(flat))),
1081            Compression::FixedSizeList(fsl) => Ok(Box::new(ValueDecompressor::from_fsl(fsl))),
1082            Compression::PackedStruct(description) => Ok(Box::new(
1083                PackedStructFixedPerValueDecompressor::new(description)?,
1084            )),
1085            _ => todo!("fixed-per-value decompressor for {:?}", description),
1086        }
1087    }
1088
1089    fn create_variable_per_value_decompressor(
1090        &self,
1091        description: &CompressiveEncoding,
1092    ) -> Result<Box<dyn VariablePerValueDecompressor>> {
1093        match description.compression.as_ref().unwrap() {
1094            Compression::Variable(variable) => {
1095                let Compression::Flat(offsets) = variable
1096                    .offsets
1097                    .as_ref()
1098                    .unwrap()
1099                    .compression
1100                    .as_ref()
1101                    .unwrap()
1102                else {
1103                    panic!("Variable compression only supports flat offsets")
1104                };
1105                assert!(offsets.bits_per_value < u8::MAX as u64);
1106                Ok(Box::new(VariableDecoder::default()))
1107            }
1108            Compression::Fsst(fsst) => Ok(Box::new(FsstPerValueDecompressor::new(
1109                LanceBuffer::from_bytes(fsst.symbol_table.clone(), 1),
1110                Box::new(VariableDecoder::default()),
1111            ))),
1112            Compression::General(general) => Ok(Box::new(CompressedBufferEncoder::from_scheme(
1113                general.compression.as_ref().expect_ok()?.scheme(),
1114            )?)),
1115            Compression::VariablePackedStruct(description) => {
1116                let mut fields = Vec::with_capacity(description.fields.len());
1117                for field in &description.fields {
1118                    let value_encoding = field.value.as_ref().ok_or_else(|| {
1119                        Error::invalid_input("VariablePackedStruct field is missing value encoding")
1120                    })?;
1121                    let decoder = match field.layout.as_ref().ok_or_else(|| {
1122                        Error::invalid_input("VariablePackedStruct field is missing layout details")
1123                    })? {
1124                        crate::format::pb21::variable_packed_struct::field_encoding::Layout::BitsPerValue(
1125                            bits_per_value,
1126                        ) => {
1127                            let decompressor =
1128                                self.create_fixed_per_value_decompressor(value_encoding)?;
1129                            VariablePackedStructFieldDecoder {
1130                                kind: VariablePackedStructFieldKind::Fixed {
1131                                    bits_per_value: *bits_per_value,
1132                                    decompressor: Arc::from(decompressor),
1133                                },
1134                            }
1135                        }
1136                        crate::format::pb21::variable_packed_struct::field_encoding::Layout::BitsPerLength(
1137                            bits_per_length,
1138                        ) => {
1139                            let decompressor =
1140                                self.create_variable_per_value_decompressor(value_encoding)?;
1141                            VariablePackedStructFieldDecoder {
1142                                kind: VariablePackedStructFieldKind::Variable {
1143                                    bits_per_length: *bits_per_length,
1144                                    decompressor: Arc::from(decompressor),
1145                                },
1146                            }
1147                        }
1148                    };
1149                    fields.push(decoder);
1150                }
1151                Ok(Box::new(PackedStructVariablePerValueDecompressor::new(
1152                    fields,
1153                )))
1154            }
1155            _ => todo!("variable-per-value decompressor for {:?}", description),
1156        }
1157    }
1158
1159    fn create_block_decompressor(
1160        &self,
1161        description: &CompressiveEncoding,
1162    ) -> Result<Box<dyn BlockDecompressor>> {
1163        match description.compression.as_ref().unwrap() {
1164            Compression::InlineBitpacking(inline_bitpacking) => Ok(Box::new(
1165                InlineBitpacking::from_description(inline_bitpacking),
1166            )),
1167            Compression::Flat(flat) => Ok(Box::new(ValueDecompressor::from_flat(flat))),
1168            Compression::Constant(constant) => {
1169                let scalar = constant
1170                    .value
1171                    .as_ref()
1172                    .map(|v| LanceBuffer::from_bytes(v.clone(), 1));
1173                Ok(Box::new(ConstantDecompressor::new(scalar)))
1174            }
1175            Compression::Variable(_) => Ok(Box::new(BinaryBlockDecompressor::default())),
1176            Compression::FixedSizeList(fsl) => {
1177                Ok(Box::new(ValueDecompressor::from_fsl(fsl.as_ref())))
1178            }
1179            Compression::OutOfLineBitpacking(out_of_line) => {
1180                // Extract the compressed bit width from the values encoding
1181                let compressed_bit_width = match out_of_line
1182                    .values
1183                    .as_ref()
1184                    .unwrap()
1185                    .compression
1186                    .as_ref()
1187                    .unwrap()
1188                {
1189                    Compression::Flat(flat) => flat.bits_per_value,
1190                    _ => {
1191                        return Err(Error::invalid_input_source(
1192                            "OutOfLineBitpacking values must use Flat encoding".into(),
1193                        ));
1194                    }
1195                };
1196                Ok(Box::new(OutOfLineBitpacking::new(
1197                    compressed_bit_width,
1198                    out_of_line.uncompressed_bits_per_value,
1199                )))
1200            }
1201            Compression::General(general) => {
1202                let inner_desc = general
1203                    .values
1204                    .as_ref()
1205                    .ok_or_else(|| {
1206                        Error::invalid_input("General compression missing inner encoding")
1207                    })?
1208                    .as_ref();
1209                let inner_decompressor = self.create_block_decompressor(inner_desc)?;
1210
1211                let compression = general.compression.as_ref().ok_or_else(|| {
1212                    Error::invalid_input("General compression missing compression config")
1213                })?;
1214                let scheme = compression.scheme().try_into()?;
1215                let config = CompressionConfig::new(scheme, compression.level);
1216                let general_decompressor =
1217                    GeneralBlockDecompressor::try_new(inner_decompressor, config)?;
1218
1219                Ok(Box::new(general_decompressor))
1220            }
1221            Compression::Rle(rle) => Ok(Box::new(create_rle_decompressor(rle, self)?)),
1222            _ => todo!(),
1223        }
1224    }
1225}
1226pub(crate) fn create_rle_decompressor(
1227    rle: &crate::format::pb21::Rle,
1228    decompression_strategy: &dyn DecompressionStrategy,
1229) -> Result<RleDecompressor> {
1230    let values = rle
1231        .values
1232        .as_ref()
1233        .ok_or_else(|| Error::invalid_input("RLE compression missing values encoding"))?;
1234    let run_lengths = rle
1235        .run_lengths
1236        .as_ref()
1237        .ok_or_else(|| Error::invalid_input("RLE compression missing run lengths encoding"))?;
1238
1239    let values = create_rle_child_decompressor(values, "values", decompression_strategy)?;
1240    let run_lengths =
1241        create_rle_child_decompressor(run_lengths, "run lengths", decompression_strategy)?;
1242
1243    if !matches!(values.bits_per_value(), 8 | 16 | 32 | 64) {
1244        return Err(Error::invalid_input(format!(
1245            "RLE compression only supports 8, 16, 32, or 64-bit values, got {}",
1246            values.bits_per_value()
1247        )));
1248    }
1249
1250    let run_length_width =
1251        RunLengthWidth::from_bits(run_lengths.bits_per_value()).ok_or_else(|| {
1252            Error::invalid_input(format!(
1253                "RLE compression only supports 8, 16, or 32-bit run lengths, got {}",
1254                run_lengths.bits_per_value()
1255            ))
1256        })?;
1257
1258    if values.requires_num_values() && run_lengths.requires_num_values() {
1259        return Err(Error::invalid_input(
1260            "RLE values and run lengths child encodings cannot both require the run count",
1261        ));
1262    }
1263
1264    if values.is_identity() && run_lengths.is_identity() {
1265        return Ok(RleDecompressor::with_run_length_width(
1266            values.bits_per_value(),
1267            run_length_width,
1268        ));
1269    }
1270
1271    Ok(RleDecompressor::with_child_decompressors(
1272        values.bits_per_value(),
1273        run_length_width,
1274        values,
1275        run_lengths,
1276    ))
1277}
1278
1279fn create_rle_child_decompressor(
1280    encoding: &CompressiveEncoding,
1281    role: &str,
1282    decompression_strategy: &dyn DecompressionStrategy,
1283) -> Result<RleChildDecompressor> {
1284    let compression = encoding
1285        .compression
1286        .as_ref()
1287        .ok_or_else(|| Error::invalid_input(format!("RLE {role} missing child compression")))?;
1288    let (bits_per_value, requires_num_values, needs_decompressor) =
1289        validate_rle_child_compression(compression, role)?;
1290
1291    if needs_decompressor {
1292        Ok(RleChildDecompressor::block(
1293            bits_per_value,
1294            decompression_strategy.create_block_decompressor(encoding)?,
1295            requires_num_values,
1296        ))
1297    } else {
1298        Ok(RleChildDecompressor::flat(bits_per_value))
1299    }
1300}
1301
1302fn validate_rle_child_compression(
1303    compression: &Compression,
1304    role: &str,
1305) -> Result<(u64, bool, bool)> {
1306    match compression {
1307        Compression::Flat(flat) => Ok((flat.bits_per_value, false, false)),
1308        Compression::General(general) => {
1309            general.compression.as_ref().ok_or_else(|| {
1310                Error::invalid_input(format!(
1311                    "RLE {role} general child missing compression config"
1312                ))
1313            })?;
1314            let values = general.values.as_ref().ok_or_else(|| {
1315                Error::invalid_input(format!("RLE {role} general child missing inner encoding"))
1316            })?;
1317            let inner = values.compression.as_ref().ok_or_else(|| {
1318                Error::invalid_input(format!(
1319                    "RLE {role} general child missing inner compression"
1320                ))
1321            })?;
1322            let (bits_per_value, requires_num_values) =
1323                validate_rle_block_child_inner(inner, role)?;
1324            Ok((bits_per_value, requires_num_values, true))
1325        }
1326        Compression::OutOfLineBitpacking(out_of_line) => {
1327            let values = out_of_line.values.as_ref().ok_or_else(|| {
1328                Error::invalid_input(format!(
1329                    "RLE {role} bitpacking child missing values encoding"
1330                ))
1331            })?;
1332            let Compression::Flat(_) = values.compression.as_ref().ok_or_else(|| {
1333                Error::invalid_input(format!(
1334                    "RLE {role} bitpacking child missing values compression"
1335                ))
1336            })?
1337            else {
1338                return Err(Error::invalid_input(format!(
1339                    "RLE {role} bitpacking child only supports flat values"
1340                )));
1341            };
1342            Ok((out_of_line.uncompressed_bits_per_value, true, true))
1343        }
1344        other => Err(Error::invalid_input(format!(
1345            "RLE {role} only supports flat, general, or out-of-line bitpacking child encodings, got {}",
1346            compression_name(other)
1347        ))),
1348    }
1349}
1350
1351fn validate_rle_block_child_inner(compression: &Compression, role: &str) -> Result<(u64, bool)> {
1352    match compression {
1353        Compression::Flat(flat) => Ok((flat.bits_per_value, false)),
1354        Compression::OutOfLineBitpacking(out_of_line) => {
1355            let values = out_of_line.values.as_ref().ok_or_else(|| {
1356                Error::invalid_input(format!(
1357                    "RLE {role} bitpacking child missing values encoding"
1358                ))
1359            })?;
1360            let Compression::Flat(_) = values.compression.as_ref().ok_or_else(|| {
1361                Error::invalid_input(format!(
1362                    "RLE {role} bitpacking child missing values compression"
1363                ))
1364            })?
1365            else {
1366                return Err(Error::invalid_input(format!(
1367                    "RLE {role} bitpacking child only supports flat values"
1368                )));
1369            };
1370            Ok((out_of_line.uncompressed_bits_per_value, true))
1371        }
1372        other => Err(Error::invalid_input(format!(
1373            "RLE {role} general child only supports flat or out-of-line bitpacking inner encodings, got {}",
1374            compression_name(other)
1375        ))),
1376    }
1377}
1378
1379fn compression_name(compression: &Compression) -> &'static str {
1380    match compression {
1381        Compression::Flat(_) => "flat",
1382        Compression::Variable(_) => "variable",
1383        Compression::Fsst(_) => "fsst",
1384        Compression::OutOfLineBitpacking(_) => "out-of-line bitpacking",
1385        Compression::InlineBitpacking(_) => "inline bitpacking",
1386        Compression::General(_) => "general",
1387        Compression::Constant(_) => "constant",
1388        Compression::Dictionary(_) => "dictionary",
1389        Compression::ByteStreamSplit(_) => "byte stream split",
1390        Compression::PackedStruct(_) => "packed struct",
1391        Compression::FixedSizeList(_) => "fixed-size list",
1392        Compression::VariablePackedStruct(_) => "variable packed struct",
1393        Compression::Rle(_) => "rle",
1394    }
1395}
1396
1397#[cfg(test)]
1398mod tests {
1399    use super::*;
1400    use crate::buffer::LanceBuffer;
1401    use crate::compression_config::CompressionParams;
1402    use crate::data::{BlockInfo, DataBlock, FixedWidthDataBlock};
1403    use crate::encodings::logical::primitive::miniblock::MiniBlockCompressionContext;
1404    use crate::statistics::ComputeStat;
1405    use crate::testing::{TestEncoding, extract_array_encoding_chain, test_compression_strategy};
1406    use arrow_schema::{DataType, Field as ArrowField};
1407    use std::collections::HashMap;
1408
1409    fn strategy(encoding: TestEncoding, params: CompressionParams) -> Arc<dyn CompressionStrategy> {
1410        test_compression_strategy(encoding, params)
1411    }
1412
1413    fn baseline_strategy(params: CompressionParams) -> Arc<dyn CompressionStrategy> {
1414        strategy(TestEncoding::StructuralU16, params)
1415    }
1416
1417    fn miniblock_context() -> MiniBlockCompressionContext {
1418        MiniBlockCompressionContext::new(0, true, true)
1419    }
1420
1421    fn create_test_field(name: &str, data_type: DataType) -> Field {
1422        let arrow_field = ArrowField::new(name, data_type, true);
1423        let mut field = Field::try_from(&arrow_field).unwrap();
1424        field.id = -1;
1425        field
1426    }
1427
1428    fn create_fixed_width_block_with_stats(
1429        bits_per_value: u64,
1430        num_values: u64,
1431        run_count: u64,
1432    ) -> DataBlock {
1433        // Create varied data to avoid low entropy
1434        let bytes_per_value = (bits_per_value / 8) as usize;
1435        let total_bytes = bytes_per_value * num_values as usize;
1436        let mut data = vec![0u8; total_bytes];
1437
1438        // Create data with specified run count
1439        let values_per_run = (num_values / run_count).max(1);
1440        let mut run_value = 0u8;
1441
1442        for i in 0..num_values as usize {
1443            if i % values_per_run as usize == 0 {
1444                run_value = run_value.wrapping_add(17); // Use prime to get varied values
1445            }
1446            // Fill all bytes of the value to create high entropy
1447            for j in 0..bytes_per_value {
1448                let byte_offset = i * bytes_per_value + j;
1449                if byte_offset < data.len() {
1450                    data[byte_offset] = run_value.wrapping_add(j as u8);
1451                }
1452            }
1453        }
1454
1455        let mut block = FixedWidthDataBlock {
1456            bits_per_value,
1457            data: LanceBuffer::reinterpret_vec(data),
1458            num_values,
1459            block_info: BlockInfo::default(),
1460        };
1461
1462        // Compute all statistics including BytePositionEntropy
1463        use crate::statistics::ComputeStat;
1464        block.compute_stat();
1465
1466        DataBlock::FixedWidth(block)
1467    }
1468
1469    fn create_fixed_width_block(bits_per_value: u64, num_values: u64) -> DataBlock {
1470        // Create data with some variety to avoid always triggering BSS
1471        let bytes_per_value = (bits_per_value / 8) as usize;
1472        let total_bytes = bytes_per_value * num_values as usize;
1473        let mut data = vec![0u8; total_bytes];
1474
1475        // Add some variation to the data to make it more realistic
1476        for i in 0..num_values as usize {
1477            let byte_offset = i * bytes_per_value;
1478            if byte_offset < data.len() {
1479                data[byte_offset] = (i % 256) as u8;
1480            }
1481        }
1482
1483        let mut block = FixedWidthDataBlock {
1484            bits_per_value,
1485            data: LanceBuffer::reinterpret_vec(data),
1486            num_values,
1487            block_info: BlockInfo::default(),
1488        };
1489
1490        // Compute all statistics including BytePositionEntropy
1491        use crate::statistics::ComputeStat;
1492        block.compute_stat();
1493
1494        DataBlock::FixedWidth(block)
1495    }
1496
1497    fn rle_run_length_bits(encoding: &CompressiveEncoding) -> u64 {
1498        let Compression::Rle(rle) = encoding.compression.as_ref().unwrap() else {
1499            panic!("expected RLE encoding");
1500        };
1501        let Compression::Flat(run_lengths) = rle
1502            .run_lengths
1503            .as_ref()
1504            .unwrap()
1505            .compression
1506            .as_ref()
1507            .unwrap()
1508        else {
1509            panic!("expected flat run lengths");
1510        };
1511        run_lengths.bits_per_value
1512    }
1513
1514    fn expect_rle_encoding(encoding: &CompressiveEncoding) -> &crate::format::pb21::Rle {
1515        match encoding.compression.as_ref().unwrap() {
1516            Compression::Rle(rle) => rle,
1517            Compression::General(general) => {
1518                let inner = general.values.as_ref().unwrap();
1519                let Compression::Rle(rle) = inner.compression.as_ref().unwrap() else {
1520                    panic!("expected wrapped RLE encoding");
1521                };
1522                rle
1523            }
1524            other => panic!("expected RLE encoding, got {}", compression_name(other)),
1525        }
1526    }
1527
1528    fn create_variable_width_block(
1529        bits_per_offset: u8,
1530        num_values: u64,
1531        avg_value_size: usize,
1532    ) -> DataBlock {
1533        use crate::statistics::ComputeStat;
1534
1535        // Create offsets buffer (num_values + 1 offsets)
1536        let mut offsets = Vec::with_capacity((num_values + 1) as usize);
1537        let mut current_offset = 0i64;
1538        offsets.push(current_offset);
1539
1540        // Generate offsets with varying value sizes
1541        for i in 0..num_values {
1542            let value_size = if avg_value_size == 0 {
1543                1
1544            } else {
1545                ((avg_value_size as i64 + (i as i64 % 8) - 4).max(1) as usize)
1546                    .min(avg_value_size * 2)
1547            };
1548            current_offset += value_size as i64;
1549            offsets.push(current_offset);
1550        }
1551
1552        // Create data buffer with realistic content
1553        let total_data_size = current_offset as usize;
1554        let mut data = vec![0u8; total_data_size];
1555
1556        // Fill data with varied content
1557        for i in 0..num_values {
1558            let start_offset = offsets[i as usize] as usize;
1559            let end_offset = offsets[(i + 1) as usize] as usize;
1560
1561            let content = (i % 256) as u8;
1562            for j in 0..end_offset - start_offset {
1563                data[start_offset + j] = content.wrapping_add(j as u8);
1564            }
1565        }
1566
1567        // Convert offsets to appropriate lance buffer
1568        let offsets_buffer = match bits_per_offset {
1569            32 => {
1570                let offsets_32: Vec<i32> = offsets.iter().map(|&o| o as i32).collect();
1571                LanceBuffer::reinterpret_vec(offsets_32)
1572            }
1573            64 => LanceBuffer::reinterpret_vec(offsets),
1574            _ => panic!("Unsupported bits_per_offset: {}", bits_per_offset),
1575        };
1576
1577        let mut block = VariableWidthBlock {
1578            data: LanceBuffer::from(data),
1579            offsets: offsets_buffer,
1580            bits_per_offset,
1581            num_values,
1582            block_info: BlockInfo::default(),
1583        };
1584
1585        block.compute_stat();
1586        DataBlock::VariableWidth(block)
1587    }
1588
1589    fn create_fsst_candidate_variable_width_block() -> DataBlock {
1590        create_variable_width_block(32, 4096, FSST_LEAST_INPUT_MAX_LENGTH as usize + 16)
1591    }
1592
1593    #[test]
1594    fn test_parameter_based_compression() {
1595        let mut params = CompressionParams::new();
1596
1597        // Configure RLE for ID columns with BSS explicitly disabled
1598        params.columns.insert(
1599            "*_id".to_string(),
1600            CompressionFieldParams {
1601                rle_threshold: Some(0.3),
1602                compression: Some("lz4".to_string()),
1603                compression_level: None,
1604                bss: Some(BssMode::Off), // Explicitly disable BSS to test RLE
1605                minichunk_size: None,
1606            },
1607        );
1608
1609        let strategy = baseline_strategy(params);
1610        let field = create_test_field("user_id", DataType::Int32);
1611
1612        // Create data with low run count for RLE
1613        // Use create_fixed_width_block_with_stats which properly sets run count
1614        let data = create_fixed_width_block_with_stats(32, 1000, 100); // 100 runs out of 1000 values
1615
1616        let compressor = strategy.create_miniblock_compressor(&field, &data).unwrap();
1617        // Should use RLE due to low threshold (0.3) and low run count (100/1000 = 0.1)
1618        let debug_str = format!("{:?}", compressor);
1619
1620        // The compressor should be RLE wrapped in general compression
1621        assert!(debug_str.contains("GeneralMiniBlockCompressor"));
1622        assert!(debug_str.contains("RleEncoder"));
1623    }
1624
1625    #[test]
1626    fn test_type_level_parameters() {
1627        let mut params = CompressionParams::new();
1628
1629        // Configure all Int32 to use specific settings
1630        params.types.insert(
1631            "Int32".to_string(),
1632            CompressionFieldParams {
1633                rle_threshold: Some(0.1), // Very low threshold
1634                compression: Some("zstd".to_string()),
1635                compression_level: Some(3),
1636                bss: Some(BssMode::Off), // Disable BSS to test RLE
1637                minichunk_size: None,
1638            },
1639        );
1640
1641        let strategy = baseline_strategy(params);
1642        let field = create_test_field("some_column", DataType::Int32);
1643        // Create data with very low run count (50 runs for 1000 values = 0.05 ratio)
1644        let data = create_fixed_width_block_with_stats(32, 1000, 50);
1645
1646        let compressor = strategy.create_miniblock_compressor(&field, &data).unwrap();
1647        // Should use RLE due to very low threshold
1648        assert!(format!("{:?}", compressor).contains("RleEncoder"));
1649    }
1650
1651    // Regression for #6626: an all-zero stat segment (e.g. rep/def for a long
1652    // run of empty lists) used to disable block bitpacking entirely.
1653    #[test]
1654    #[cfg(feature = "bitpacking")]
1655    fn test_block_bitpacks_with_zero_segment() {
1656        let strategy = baseline_strategy(CompressionParams::default());
1657        let field = create_test_field("levels", DataType::UInt16);
1658
1659        // First 1024 zeros, then 1024 ones; max bit width is 1.
1660        let mut values: Vec<u16> = vec![0; 1024];
1661        values.extend(std::iter::repeat_n(1u16, 1024));
1662        let mut block = FixedWidthDataBlock {
1663            bits_per_value: 16,
1664            data: LanceBuffer::reinterpret_vec(values),
1665            num_values: 2048,
1666            block_info: BlockInfo::default(),
1667        };
1668        block.compute_stat();
1669        let data = DataBlock::FixedWidth(block);
1670
1671        let (compressor, _encoding) = strategy.create_block_compressor(&field, &data).unwrap();
1672        let debug_str = format!("{:?}", compressor);
1673        assert!(
1674            debug_str.contains("OutOfLineBitpacking"),
1675            "expected OutOfLineBitpacking, got: {debug_str}"
1676        );
1677    }
1678
1679    #[test]
1680    fn test_rle_block_accounts_for_header_before_selecting() {
1681        let strategy = strategy(TestEncoding::StructuralSparse, CompressionParams::default());
1682        let field = create_test_field("small_constant", DataType::Int32);
1683        let values = vec![42i32; 2];
1684        let mut block = FixedWidthDataBlock {
1685            bits_per_value: 32,
1686            data: LanceBuffer::reinterpret_vec(values),
1687            num_values: 2,
1688            block_info: BlockInfo::default(),
1689        };
1690        block.compute_stat();
1691        let data = DataBlock::FixedWidth(block);
1692
1693        let (compressor, encoding) = strategy.create_block_compressor(&field, &data).unwrap();
1694
1695        assert!(format!("{compressor:?}").contains("ValueEncoder"));
1696        assert!(matches!(
1697            encoding.compression.as_ref(),
1698            Some(Compression::Flat(_))
1699        ));
1700    }
1701
1702    #[test]
1703    #[cfg(feature = "bitpacking")]
1704    fn test_rle_block_prefers_bitpacking_when_smaller() {
1705        let strategy = strategy(TestEncoding::StructuralSparse, CompressionParams::default());
1706        let field = create_test_field("levels", DataType::UInt16);
1707
1708        let mut values = Vec::with_capacity(2048);
1709        for run_idx in 0..1024 {
1710            values.extend(std::iter::repeat_n((run_idx % 2) as u16, 2));
1711        }
1712        let mut block = FixedWidthDataBlock {
1713            bits_per_value: 16,
1714            data: LanceBuffer::reinterpret_vec(values),
1715            num_values: 2048,
1716            block_info: BlockInfo::default(),
1717        };
1718        block.compute_stat();
1719        let data = DataBlock::FixedWidth(block);
1720
1721        let (compressor, encoding) = strategy.create_block_compressor(&field, &data).unwrap();
1722        let debug_str = format!("{compressor:?}");
1723        assert!(
1724            debug_str.contains("OutOfLineBitpacking"),
1725            "expected OutOfLineBitpacking, got: {debug_str}"
1726        );
1727        assert!(matches!(
1728            encoding.compression.as_ref(),
1729            Some(Compression::OutOfLineBitpacking(_))
1730        ));
1731    }
1732
1733    #[test]
1734    #[cfg(feature = "bitpacking")]
1735    fn test_low_cardinality_prefers_bitpacking_over_rle() {
1736        let strategy = baseline_strategy(CompressionParams::default());
1737        let field = create_test_field("int_score", DataType::Int64);
1738
1739        // Low cardinality values (3/4/5) but with moderate run count:
1740        // RLE compresses vs raw, yet bitpacking should be smaller.
1741        let mut values: Vec<u64> = Vec::with_capacity(256);
1742        for run_idx in 0..64 {
1743            let value = match run_idx % 3 {
1744                0 => 3u64,
1745                1 => 4u64,
1746                _ => 5u64,
1747            };
1748            values.extend(std::iter::repeat_n(value, 4));
1749        }
1750
1751        let mut block = FixedWidthDataBlock {
1752            bits_per_value: 64,
1753            data: LanceBuffer::reinterpret_vec(values),
1754            num_values: 256,
1755            block_info: BlockInfo::default(),
1756        };
1757
1758        use crate::statistics::ComputeStat;
1759        block.compute_stat();
1760
1761        let data = DataBlock::FixedWidth(block);
1762        let compressor = strategy.create_miniblock_compressor(&field, &data).unwrap();
1763        let debug_str = format!("{:?}", compressor);
1764        assert!(
1765            debug_str.contains("InlineBitpacking"),
1766            "expected InlineBitpacking, got: {debug_str}"
1767        );
1768        assert!(
1769            !debug_str.contains("RleEncoder"),
1770            "expected RLE to be skipped when bitpacking is smaller, got: {debug_str}"
1771        );
1772    }
1773
1774    fn check_uncompressed_encoding(encoding: &CompressiveEncoding, variable: bool) {
1775        let chain = extract_array_encoding_chain(encoding);
1776        if variable {
1777            assert_eq!(chain.len(), 2);
1778            assert_eq!(chain.first().unwrap().as_str(), "variable");
1779            assert_eq!(chain.get(1).unwrap().as_str(), "flat");
1780        } else {
1781            assert_eq!(chain.len(), 1);
1782            assert_eq!(chain.first().unwrap().as_str(), "flat");
1783        }
1784    }
1785
1786    #[test]
1787    fn test_none_compression() {
1788        let mut params = CompressionParams::new();
1789
1790        // Disable compression for embeddings
1791        params.columns.insert(
1792            "embeddings".to_string(),
1793            CompressionFieldParams {
1794                compression: Some("none".to_string()),
1795                ..Default::default()
1796            },
1797        );
1798
1799        let strategy = baseline_strategy(params);
1800        let field = create_test_field("embeddings", DataType::Float32);
1801        let fixed_data = create_fixed_width_block(32, 1000);
1802        let variable_data = create_variable_width_block(32, 10, 32 * 1024);
1803
1804        // Test miniblock
1805        let compressor = strategy
1806            .create_miniblock_compressor(&field, &fixed_data)
1807            .unwrap();
1808        let (_block, encoding) = compressor
1809            .compress(miniblock_context(), fixed_data.clone())
1810            .unwrap();
1811        check_uncompressed_encoding(&encoding, false);
1812        let compressor = strategy
1813            .create_miniblock_compressor(&field, &variable_data)
1814            .unwrap();
1815        let (_block, encoding) = compressor
1816            .compress(miniblock_context(), variable_data.clone())
1817            .unwrap();
1818        check_uncompressed_encoding(&encoding, true);
1819
1820        // Test pervalue
1821        let compressor = strategy.create_per_value(&field, &fixed_data).unwrap();
1822        let (_block, encoding) = compressor.compress(fixed_data).unwrap();
1823        check_uncompressed_encoding(&encoding, false);
1824        let compressor = strategy.create_per_value(&field, &variable_data).unwrap();
1825        let (_block, encoding) = compressor.compress(variable_data).unwrap();
1826        check_uncompressed_encoding(&encoding, true);
1827    }
1828
1829    #[test]
1830    fn test_field_metadata_none_compression() {
1831        // Prepare field with metadata for none compression
1832        let mut arrow_field = ArrowField::new("simple_col", DataType::Binary, true);
1833        let mut metadata = HashMap::new();
1834        metadata.insert(COMPRESSION_META_KEY.to_string(), "none".to_string());
1835        arrow_field = arrow_field.with_metadata(metadata);
1836        let field = Field::try_from(&arrow_field).unwrap();
1837
1838        let strategy = baseline_strategy(CompressionParams::new());
1839
1840        // Test miniblock
1841        let fixed_data = create_fixed_width_block(32, 1000);
1842        let variable_data = create_variable_width_block(32, 10, 32 * 1024);
1843
1844        let compressor = strategy
1845            .create_miniblock_compressor(&field, &fixed_data)
1846            .unwrap();
1847        let (_block, encoding) = compressor
1848            .compress(miniblock_context(), fixed_data.clone())
1849            .unwrap();
1850        check_uncompressed_encoding(&encoding, false);
1851
1852        let compressor = strategy
1853            .create_miniblock_compressor(&field, &variable_data)
1854            .unwrap();
1855        let (_block, encoding) = compressor
1856            .compress(miniblock_context(), variable_data.clone())
1857            .unwrap();
1858        check_uncompressed_encoding(&encoding, true);
1859
1860        // Test pervalue
1861        let compressor = strategy.create_per_value(&field, &fixed_data).unwrap();
1862        let (_block, encoding) = compressor.compress(fixed_data).unwrap();
1863        check_uncompressed_encoding(&encoding, false);
1864
1865        let compressor = strategy.create_per_value(&field, &variable_data).unwrap();
1866        let (_block, encoding) = compressor.compress(variable_data).unwrap();
1867        check_uncompressed_encoding(&encoding, true);
1868    }
1869
1870    #[test]
1871    fn test_auto_fsst_disabled_for_binary_fields() {
1872        let strategy = baseline_strategy(CompressionParams::default());
1873        let field = create_test_field("bytes", DataType::Binary);
1874        let variable_data = create_fsst_candidate_variable_width_block();
1875
1876        let miniblock = strategy
1877            .create_miniblock_compressor(&field, &variable_data)
1878            .unwrap();
1879        let miniblock_debug = format!("{:?}", miniblock);
1880        assert!(
1881            miniblock_debug.contains("BinaryMiniBlockEncoder"),
1882            "expected BinaryMiniBlockEncoder, got: {miniblock_debug}"
1883        );
1884        assert!(
1885            !miniblock_debug.contains("FsstMiniBlockEncoder"),
1886            "did not expect FsstMiniBlockEncoder, got: {miniblock_debug}"
1887        );
1888
1889        let per_value = strategy.create_per_value(&field, &variable_data).unwrap();
1890        let per_value_debug = format!("{:?}", per_value);
1891        assert!(
1892            per_value_debug.contains("VariableEncoder"),
1893            "expected VariableEncoder, got: {per_value_debug}"
1894        );
1895        assert!(
1896            !per_value_debug.contains("FsstPerValueEncoder"),
1897            "did not expect FsstPerValueEncoder, got: {per_value_debug}"
1898        );
1899    }
1900
1901    #[test]
1902    fn test_auto_fsst_still_enabled_for_utf8_fields() {
1903        let strategy = baseline_strategy(CompressionParams::default());
1904        let field = create_test_field("text", DataType::Utf8);
1905        let variable_data = create_fsst_candidate_variable_width_block();
1906
1907        let miniblock = strategy
1908            .create_miniblock_compressor(&field, &variable_data)
1909            .unwrap();
1910        let miniblock_debug = format!("{:?}", miniblock);
1911        assert!(
1912            miniblock_debug.contains("FsstMiniBlockEncoder"),
1913            "expected FsstMiniBlockEncoder, got: {miniblock_debug}"
1914        );
1915
1916        let per_value = strategy.create_per_value(&field, &variable_data).unwrap();
1917        let per_value_debug = format!("{:?}", per_value);
1918        assert!(
1919            per_value_debug.contains("FsstPerValueEncoder"),
1920            "expected FsstPerValueEncoder, got: {per_value_debug}"
1921        );
1922    }
1923
1924    #[test]
1925    fn test_explicit_fsst_still_supported_for_binary_fields() {
1926        let mut params = CompressionParams::new();
1927        params.columns.insert(
1928            "bytes".to_string(),
1929            CompressionFieldParams {
1930                compression: Some("fsst".to_string()),
1931                ..Default::default()
1932            },
1933        );
1934
1935        let strategy = baseline_strategy(params);
1936        let field = create_test_field("bytes", DataType::Binary);
1937        let variable_data = create_fsst_candidate_variable_width_block();
1938
1939        let miniblock = strategy
1940            .create_miniblock_compressor(&field, &variable_data)
1941            .unwrap();
1942        let miniblock_debug = format!("{:?}", miniblock);
1943        assert!(
1944            miniblock_debug.contains("FsstMiniBlockEncoder"),
1945            "expected FsstMiniBlockEncoder, got: {miniblock_debug}"
1946        );
1947
1948        let per_value = strategy.create_per_value(&field, &variable_data).unwrap();
1949        let per_value_debug = format!("{:?}", per_value);
1950        assert!(
1951            per_value_debug.contains("FsstPerValueEncoder"),
1952            "expected FsstPerValueEncoder, got: {per_value_debug}"
1953        );
1954    }
1955
1956    #[test]
1957    #[cfg(feature = "zstd")]
1958    fn test_compression_level_honored_for_large_per_value() {
1959        let mut params = CompressionParams::new();
1960        params.columns.insert(
1961            "html".to_string(),
1962            CompressionFieldParams {
1963                compression: Some("zstd".to_string()),
1964                compression_level: Some(19),
1965                ..Default::default()
1966            },
1967        );
1968        let strategy = baseline_strategy(params);
1969        let field = create_test_field("html", DataType::Utf8);
1970        let large = create_variable_width_block(32, 64, 40 * 1024);
1971
1972        let per_value = strategy.create_per_value(&field, &large).unwrap();
1973        let debug = format!("{per_value:?}");
1974        assert!(
1975            debug.contains("ZstdBufferCompressor") && debug.contains("compression_level: 19"),
1976            "expected zstd level 19 to reach the per-value compressor, got: {debug}"
1977        );
1978    }
1979
1980    #[test]
1981    fn test_parameter_merge_priority() {
1982        let mut params = CompressionParams::new();
1983
1984        // Set type-level
1985        params.types.insert(
1986            "Int32".to_string(),
1987            CompressionFieldParams {
1988                rle_threshold: Some(0.5),
1989                compression: Some("lz4".to_string()),
1990                ..Default::default()
1991            },
1992        );
1993
1994        // Set column-level (highest priority)
1995        params.columns.insert(
1996            "user_id".to_string(),
1997            CompressionFieldParams {
1998                rle_threshold: Some(0.2),
1999                compression: Some("zstd".to_string()),
2000                compression_level: Some(6),
2001                bss: None,
2002                minichunk_size: None,
2003            },
2004        );
2005
2006        // Get merged params
2007        let merged = params.get_field_params("user_id", &DataType::Int32);
2008
2009        // Column params should override type params
2010        assert_eq!(merged.rle_threshold, Some(0.2));
2011        assert_eq!(merged.compression, Some("zstd".to_string()));
2012        assert_eq!(merged.compression_level, Some(6));
2013
2014        // Test field with only type params
2015        let merged = params.get_field_params("other_field", &DataType::Int32);
2016        assert_eq!(merged.rle_threshold, Some(0.5));
2017        assert_eq!(merged.compression, Some("lz4".to_string()));
2018        assert_eq!(merged.compression_level, None);
2019    }
2020
2021    #[test]
2022    fn test_pattern_matching() {
2023        let mut params = CompressionParams::new();
2024
2025        // Configure pattern for log files
2026        params.columns.insert(
2027            "log_*".to_string(),
2028            CompressionFieldParams {
2029                compression: Some("zstd".to_string()),
2030                compression_level: Some(6),
2031                ..Default::default()
2032            },
2033        );
2034
2035        // Should match pattern
2036        let merged = params.get_field_params("log_messages", &DataType::Utf8);
2037        assert_eq!(merged.compression, Some("zstd".to_string()));
2038        assert_eq!(merged.compression_level, Some(6));
2039
2040        // Should not match
2041        let merged = params.get_field_params("messages_log", &DataType::Utf8);
2042        assert_eq!(merged.compression, None);
2043    }
2044
2045    #[test]
2046    fn test_legacy_metadata_support() {
2047        let params = CompressionParams::new();
2048        let strategy = baseline_strategy(params);
2049
2050        // Test field with "none" compression metadata
2051        let mut metadata = HashMap::new();
2052        metadata.insert(COMPRESSION_META_KEY.to_string(), "none".to_string());
2053        let mut field = create_test_field("some_column", DataType::Int32);
2054        field.metadata = metadata;
2055
2056        let data = create_fixed_width_block(32, 1000);
2057        let compressor = strategy.create_miniblock_compressor(&field, &data).unwrap();
2058
2059        // Should respect metadata and use ValueEncoder
2060        assert!(format!("{:?}", compressor).contains("ValueEncoder"));
2061    }
2062
2063    #[test]
2064    fn test_default_behavior() {
2065        // Empty params should fall back to default behavior
2066        let params = CompressionParams::new();
2067        let strategy = baseline_strategy(params);
2068
2069        let field = create_test_field("random_column", DataType::Int32);
2070        // Create data with high run count that won't trigger RLE (600 runs for 1000 values = 0.6 ratio)
2071        let data = create_fixed_width_block_with_stats(32, 1000, 600);
2072
2073        let compressor = strategy.create_miniblock_compressor(&field, &data).unwrap();
2074        // Should use default strategy's decision
2075        let debug_str = format!("{:?}", compressor);
2076        assert!(debug_str.contains("ValueEncoder") || debug_str.contains("InlineBitpacking"));
2077    }
2078
2079    #[test]
2080    fn test_field_metadata_compression() {
2081        let params = CompressionParams::new();
2082        let strategy = baseline_strategy(params);
2083
2084        // Test field with compression metadata
2085        let mut metadata = HashMap::new();
2086        metadata.insert(COMPRESSION_META_KEY.to_string(), "zstd".to_string());
2087        metadata.insert(COMPRESSION_LEVEL_META_KEY.to_string(), "6".to_string());
2088        let mut field = create_test_field("test_column", DataType::Int32);
2089        field.metadata = metadata;
2090
2091        let data = create_fixed_width_block(32, 1000);
2092        let compressor = strategy.create_miniblock_compressor(&field, &data).unwrap();
2093
2094        // Should use zstd with level 6
2095        let debug_str = format!("{:?}", compressor);
2096        assert!(debug_str.contains("GeneralMiniBlockCompressor"));
2097    }
2098
2099    #[test]
2100    fn test_field_metadata_rle_threshold() {
2101        let params = CompressionParams::new();
2102        let strategy = baseline_strategy(params);
2103
2104        // Test field with RLE threshold metadata
2105        let mut metadata = HashMap::new();
2106        metadata.insert(RLE_THRESHOLD_META_KEY.to_string(), "0.8".to_string());
2107        metadata.insert(BSS_META_KEY.to_string(), "off".to_string()); // Disable BSS to test RLE
2108        let mut field = create_test_field("test_column", DataType::Int32);
2109        field.metadata = metadata;
2110
2111        // Create data with low run count (e.g., 100 runs for 1000 values = 0.1 ratio)
2112        // This ensures run_count (100) < num_values * threshold (1000 * 0.8 = 800)
2113        let data = create_fixed_width_block_with_stats(32, 1000, 100);
2114
2115        let compressor = strategy.create_miniblock_compressor(&field, &data).unwrap();
2116
2117        // Should use RLE because run_count (100) < num_values * threshold (800)
2118        let debug_str = format!("{:?}", compressor);
2119        assert!(debug_str.contains("RleEncoder"));
2120    }
2121
2122    #[test]
2123    fn test_rle_v2_miniblock_selects_u16_run_lengths() {
2124        let mut metadata = HashMap::new();
2125        metadata.insert(RLE_THRESHOLD_META_KEY.to_string(), "1.0".to_string());
2126        metadata.insert(BSS_META_KEY.to_string(), "off".to_string());
2127        let mut field = create_test_field("test_column", DataType::Int32);
2128        field.metadata = metadata;
2129
2130        let values = vec![7i32; 1000];
2131        let mut data = FixedWidthDataBlock {
2132            bits_per_value: 32,
2133            data: LanceBuffer::reinterpret_vec(values),
2134            num_values: 1000,
2135            block_info: BlockInfo::default(),
2136        };
2137        data.compute_stat();
2138        let data = DataBlock::FixedWidth(data);
2139
2140        let strategy = strategy(TestEncoding::StructuralSparse, CompressionParams::default());
2141        let compressor = strategy.create_miniblock_compressor(&field, &data).unwrap();
2142        let (_compressed, encoding) = compressor.compress(miniblock_context(), data).unwrap();
2143        assert_eq!(rle_run_length_bits(&encoding), 16);
2144    }
2145
2146    #[test]
2147    fn test_rle_v2_miniblock_keeps_u8_run_lengths_before_v2_3() {
2148        for version in [TestEncoding::StructuralU16, TestEncoding::StructuralU32] {
2149            let mut metadata = HashMap::new();
2150            metadata.insert(RLE_THRESHOLD_META_KEY.to_string(), "1.0".to_string());
2151            metadata.insert(BSS_META_KEY.to_string(), "off".to_string());
2152            let mut field = create_test_field("test_column", DataType::Int32);
2153            field.metadata = metadata;
2154
2155            let values = vec![7i32; 1000];
2156            let mut data = FixedWidthDataBlock {
2157                bits_per_value: 32,
2158                data: LanceBuffer::reinterpret_vec(values),
2159                num_values: 1000,
2160                block_info: BlockInfo::default(),
2161            };
2162            data.compute_stat();
2163            let data = DataBlock::FixedWidth(data);
2164
2165            let strategy = strategy(version, CompressionParams::default());
2166            let compressor = strategy.create_miniblock_compressor(&field, &data).unwrap();
2167            let (_compressed, encoding) = compressor.compress(miniblock_context(), data).unwrap();
2168            assert_eq!(rle_run_length_bits(&encoding), 8, "version={version}");
2169        }
2170    }
2171
2172    #[test]
2173    fn test_rle_v2_uses_selected_width_cost_before_bitpacking() {
2174        let mut metadata = HashMap::new();
2175        metadata.insert(RLE_THRESHOLD_META_KEY.to_string(), "1.0".to_string());
2176        metadata.insert(BSS_META_KEY.to_string(), "off".to_string());
2177        let mut field = create_test_field("test_column", DataType::Int32);
2178        field.metadata = metadata;
2179
2180        let values = vec![0i32; 4096];
2181        let mut data = FixedWidthDataBlock {
2182            bits_per_value: 32,
2183            data: LanceBuffer::reinterpret_vec(values),
2184            num_values: 4096,
2185            block_info: BlockInfo::default(),
2186        };
2187        data.compute_stat();
2188        let data = DataBlock::FixedWidth(data);
2189
2190        let strategy = strategy(TestEncoding::StructuralSparse, CompressionParams::default());
2191        let compressor = strategy.create_miniblock_compressor(&field, &data).unwrap();
2192        let debug_str = format!("{compressor:?}");
2193        assert!(debug_str.contains("RleEncoder"));
2194
2195        let (_compressed, encoding) = compressor.compress(miniblock_context(), data).unwrap();
2196        assert_eq!(rle_run_length_bits(&encoding), 16);
2197    }
2198
2199    #[test]
2200    fn test_rle_v2_sorted_dictionary_indices_select_u16_run_lengths() {
2201        let field = create_test_field("dict_indices", DataType::Int32);
2202
2203        let mut values = Vec::with_capacity(1_200);
2204        for value in 0..4 {
2205            values.extend(std::iter::repeat_n(value, 300));
2206        }
2207        let mut data = FixedWidthDataBlock {
2208            bits_per_value: 32,
2209            data: LanceBuffer::reinterpret_vec(values),
2210            num_values: 1_200,
2211            block_info: BlockInfo::default(),
2212        };
2213        data.compute_stat();
2214        let data = DataBlock::FixedWidth(data);
2215
2216        let strategy = strategy(TestEncoding::StructuralSparse, CompressionParams::default());
2217        let compressor = strategy.create_miniblock_compressor(&field, &data).unwrap();
2218        let (_compressed, encoding) = compressor.compress(miniblock_context(), data).unwrap();
2219        assert_eq!(rle_run_length_bits(&encoding), 16);
2220    }
2221
2222    #[test]
2223    fn test_rle_v2_short_runs_keep_u8_run_lengths() {
2224        let field = create_test_field("dict_indices", DataType::Int32);
2225
2226        let mut values = Vec::with_capacity(1_280);
2227        for value in 0..10 {
2228            values.extend(std::iter::repeat_n(value, 128));
2229        }
2230        let mut data = FixedWidthDataBlock {
2231            bits_per_value: 32,
2232            data: LanceBuffer::reinterpret_vec(values),
2233            num_values: 1_280,
2234            block_info: BlockInfo::default(),
2235        };
2236        data.compute_stat();
2237        let data = DataBlock::FixedWidth(data);
2238
2239        let strategy = strategy(TestEncoding::StructuralSparse, CompressionParams::default());
2240        let compressor = strategy.create_miniblock_compressor(&field, &data).unwrap();
2241        let (_compressed, encoding) = compressor.compress(miniblock_context(), data).unwrap();
2242        assert_eq!(rle_run_length_bits(&encoding), 8);
2243    }
2244
2245    #[test]
2246    #[cfg(any(feature = "lz4", feature = "zstd"))]
2247    fn test_rle_miniblock_released_versions_keep_flat_children_when_compression_requested() {
2248        for version in [TestEncoding::StructuralU16, TestEncoding::StructuralU32] {
2249            let mut params = CompressionParams::new();
2250            params.columns.insert(
2251                "dict_indices".to_string(),
2252                CompressionFieldParams {
2253                    compression: Some(
2254                        if cfg!(feature = "lz4") { "lz4" } else { "zstd" }.to_string(),
2255                    ),
2256                    rle_threshold: Some(1.0),
2257                    bss: Some(BssMode::Off),
2258                    ..Default::default()
2259                },
2260            );
2261            let strategy = strategy(version, params);
2262            let field = create_test_field("dict_indices", DataType::UInt32);
2263
2264            let mut values = Vec::with_capacity(8192 * 4);
2265            for value in 0..8192u32 {
2266                values.extend(std::iter::repeat_n(value, 4));
2267            }
2268            let mut data = FixedWidthDataBlock {
2269                bits_per_value: 32,
2270                data: LanceBuffer::reinterpret_vec(values),
2271                num_values: 8192 * 4,
2272                block_info: BlockInfo::default(),
2273            };
2274            data.compute_stat();
2275            let data = DataBlock::FixedWidth(data);
2276
2277            let compressor = strategy.create_miniblock_compressor(&field, &data).unwrap();
2278            let (_compressed, encoding) = compressor.compress(miniblock_context(), data).unwrap();
2279            let rle = expect_rle_encoding(&encoding);
2280
2281            assert!(
2282                matches!(
2283                    rle.values.as_ref().unwrap().compression.as_ref().unwrap(),
2284                    Compression::Flat(_)
2285                ),
2286                "version={version}"
2287            );
2288            assert!(
2289                matches!(
2290                    rle.run_lengths
2291                        .as_ref()
2292                        .unwrap()
2293                        .compression
2294                        .as_ref()
2295                        .unwrap(),
2296                    Compression::Flat(_)
2297                ),
2298                "version={version}"
2299            );
2300        }
2301    }
2302
2303    #[test]
2304    #[cfg(feature = "bitpacking")]
2305    fn test_rle_miniblock_strategy_bitpacks_child_values_when_smaller() {
2306        let field = create_test_field("dict_indices", DataType::Int32);
2307
2308        let mut values = Vec::with_capacity(8192 * 4);
2309        for value in 0..8192 {
2310            values.extend(std::iter::repeat_n(value, 4));
2311        }
2312        let mut data = FixedWidthDataBlock {
2313            bits_per_value: 32,
2314            data: LanceBuffer::reinterpret_vec(values),
2315            num_values: 8192 * 4,
2316            block_info: BlockInfo::default(),
2317        };
2318        data.compute_stat();
2319        let data = DataBlock::FixedWidth(data);
2320
2321        let strategy = strategy(TestEncoding::StructuralSparse, CompressionParams::default());
2322        let compressor = strategy.create_miniblock_compressor(&field, &data).unwrap();
2323        let debug_str = format!("{compressor:?}");
2324        assert!(debug_str.contains("RleEncoder"));
2325
2326        let (_compressed, encoding) = compressor.compress(miniblock_context(), data).unwrap();
2327        let Compression::Rle(rle) = encoding.compression.as_ref().unwrap() else {
2328            panic!("expected RLE encoding");
2329        };
2330        assert!(matches!(
2331            rle.values.as_ref().unwrap().compression.as_ref().unwrap(),
2332            Compression::OutOfLineBitpacking(_)
2333        ));
2334        assert!(matches!(
2335            rle.run_lengths
2336                .as_ref()
2337                .unwrap()
2338                .compression
2339                .as_ref()
2340                .unwrap(),
2341            Compression::Flat(_)
2342        ));
2343    }
2344
2345    #[test]
2346    #[cfg(feature = "bitpacking")]
2347    fn test_rle_miniblock_keeps_child_bitpacked_rle_when_smaller_than_inline_bitpacking() {
2348        let field = create_test_field("int_score", DataType::UInt64);
2349
2350        let mut values = Vec::with_capacity(8192 * 8);
2351        for run_idx in 0..8192 {
2352            let value = match run_idx % 3 {
2353                0 => 3u64,
2354                1 => 4u64,
2355                _ => 5u64,
2356            };
2357            values.extend(std::iter::repeat_n(value, 8));
2358        }
2359        let mut data = FixedWidthDataBlock {
2360            bits_per_value: 64,
2361            data: LanceBuffer::reinterpret_vec(values),
2362            num_values: 8192 * 8,
2363            block_info: BlockInfo::default(),
2364        };
2365        data.compute_stat();
2366        let data = DataBlock::FixedWidth(data);
2367
2368        let strategy = strategy(TestEncoding::StructuralSparse, CompressionParams::default());
2369        let compressor = strategy.create_miniblock_compressor(&field, &data).unwrap();
2370        let debug_str = format!("{compressor:?}");
2371        assert!(
2372            debug_str.contains("RleEncoder"),
2373            "expected RLE to beat inline bitpacking after child selection, got: {debug_str}"
2374        );
2375
2376        let (_compressed, encoding) = compressor.compress(miniblock_context(), data).unwrap();
2377        let rle = expect_rle_encoding(&encoding);
2378        assert!(matches!(
2379            rle.values.as_ref().unwrap().compression.as_ref().unwrap(),
2380            Compression::OutOfLineBitpacking(_)
2381        ));
2382        assert!(matches!(
2383            rle.run_lengths
2384                .as_ref()
2385                .unwrap()
2386                .compression
2387                .as_ref()
2388                .unwrap(),
2389            Compression::Flat(_)
2390        ));
2391    }
2392
2393    #[test]
2394    fn test_field_metadata_override_params() {
2395        // Set up params with one configuration
2396        let mut params = CompressionParams::new();
2397        params.columns.insert(
2398            "test_column".to_string(),
2399            CompressionFieldParams {
2400                rle_threshold: Some(0.3),
2401                compression: Some("lz4".to_string()),
2402                compression_level: None,
2403                bss: None,
2404                minichunk_size: None,
2405            },
2406        );
2407
2408        let strategy = baseline_strategy(params);
2409
2410        // Field metadata should override params
2411        let mut metadata = HashMap::new();
2412        metadata.insert(COMPRESSION_META_KEY.to_string(), "none".to_string());
2413        let mut field = create_test_field("test_column", DataType::Int32);
2414        field.metadata = metadata;
2415
2416        let data = create_fixed_width_block(32, 1000);
2417        let compressor = strategy.create_miniblock_compressor(&field, &data).unwrap();
2418
2419        // Should use none compression (from metadata) instead of lz4 (from params)
2420        assert!(format!("{:?}", compressor).contains("ValueEncoder"));
2421    }
2422
2423    #[test]
2424    fn test_field_metadata_mixed_configuration() {
2425        // Configure type-level params
2426        let mut params = CompressionParams::new();
2427        params.types.insert(
2428            "Int32".to_string(),
2429            CompressionFieldParams {
2430                rle_threshold: Some(0.5),
2431                compression: Some("lz4".to_string()),
2432                ..Default::default()
2433            },
2434        );
2435
2436        let strategy = baseline_strategy(params);
2437
2438        // Field metadata provides partial override
2439        let mut metadata = HashMap::new();
2440        metadata.insert(COMPRESSION_LEVEL_META_KEY.to_string(), "3".to_string());
2441        let mut field = create_test_field("test_column", DataType::Int32);
2442        field.metadata = metadata;
2443
2444        let data = create_fixed_width_block(32, 1000);
2445        let compressor = strategy.create_miniblock_compressor(&field, &data).unwrap();
2446
2447        // Should use lz4 (from type params) with level 3 (from metadata)
2448        let debug_str = format!("{:?}", compressor);
2449        assert!(debug_str.contains("GeneralMiniBlockCompressor"));
2450    }
2451
2452    #[test]
2453    fn test_bss_field_metadata() {
2454        let params = CompressionParams::new();
2455        let strategy = baseline_strategy(params);
2456
2457        // Test BSS "on" mode with compression enabled (BSS requires compression to be effective)
2458        let mut metadata = HashMap::new();
2459        metadata.insert(BSS_META_KEY.to_string(), "on".to_string());
2460        metadata.insert(COMPRESSION_META_KEY.to_string(), "lz4".to_string());
2461        let arrow_field =
2462            ArrowField::new("temperature", DataType::Float32, false).with_metadata(metadata);
2463        let field = Field::try_from(&arrow_field).unwrap();
2464
2465        // Create float data
2466        let data = create_fixed_width_block(32, 100);
2467
2468        let compressor = strategy.create_miniblock_compressor(&field, &data).unwrap();
2469        let debug_str = format!("{:?}", compressor);
2470        assert!(debug_str.contains("ByteStreamSplitEncoder"));
2471    }
2472
2473    #[test]
2474    fn test_bss_with_compression() {
2475        let params = CompressionParams::new();
2476        let strategy = baseline_strategy(params);
2477
2478        // Test BSS with LZ4 compression
2479        let mut metadata = HashMap::new();
2480        metadata.insert(BSS_META_KEY.to_string(), "on".to_string());
2481        metadata.insert(COMPRESSION_META_KEY.to_string(), "lz4".to_string());
2482        let arrow_field =
2483            ArrowField::new("sensor_data", DataType::Float64, false).with_metadata(metadata);
2484        let field = Field::try_from(&arrow_field).unwrap();
2485
2486        // Create double data
2487        let data = create_fixed_width_block(64, 100);
2488
2489        let compressor = strategy.create_miniblock_compressor(&field, &data).unwrap();
2490        let debug_str = format!("{:?}", compressor);
2491        // Should have BSS wrapped in general compression
2492        assert!(debug_str.contains("GeneralMiniBlockCompressor"));
2493        assert!(debug_str.contains("ByteStreamSplitEncoder"));
2494    }
2495
2496    #[test]
2497    #[cfg(any(feature = "lz4", feature = "zstd"))]
2498    fn test_general_block_decompression_fixed_width_v2_2() {
2499        // Request general compression via the write path (2.2 requirement) and ensure the read path mirrors it.
2500        let mut params = CompressionParams::new();
2501        params.columns.insert(
2502            "dict_values".to_string(),
2503            CompressionFieldParams {
2504                compression: Some(if cfg!(feature = "lz4") { "lz4" } else { "zstd" }.to_string()),
2505                ..Default::default()
2506            },
2507        );
2508
2509        let strategy = strategy(TestEncoding::StructuralU32, params);
2510
2511        let field = create_test_field("dict_values", DataType::FixedSizeBinary(3));
2512        let data = create_fixed_width_block(24, 1024);
2513        let DataBlock::FixedWidth(expected_block) = &data else {
2514            panic!("expected fixed width block");
2515        };
2516        let expected_bits = expected_block.bits_per_value;
2517        let expected_num_values = expected_block.num_values;
2518        let num_values = expected_num_values;
2519
2520        let (compressor, encoding) = strategy
2521            .create_block_compressor(&field, &data)
2522            .expect("general compression should be selected");
2523        match encoding.compression.as_ref() {
2524            Some(Compression::General(_)) => {}
2525            other => panic!("expected general compression, got {:?}", other),
2526        }
2527
2528        let compressed_buffer = compressor
2529            .compress(data.clone())
2530            .expect("write path general compression should succeed");
2531
2532        let decompressor = DefaultDecompressionStrategy::default()
2533            .create_block_decompressor(&encoding)
2534            .expect("general block decompressor should be created");
2535
2536        let decoded = decompressor
2537            .decompress(compressed_buffer, num_values)
2538            .expect("decompression should succeed");
2539
2540        match decoded {
2541            DataBlock::FixedWidth(block) => {
2542                assert_eq!(block.bits_per_value, expected_bits);
2543                assert_eq!(block.num_values, expected_num_values);
2544                assert_eq!(block.data.as_ref(), expected_block.data.as_ref());
2545            }
2546            _ => panic!("expected fixed width block"),
2547        }
2548    }
2549
2550    #[test]
2551    #[cfg(any(feature = "lz4", feature = "zstd"))]
2552    fn test_general_compression_not_selected_for_v2_1_even_if_requested() {
2553        let mut params = CompressionParams::new();
2554        params.columns.insert(
2555            "dict_values".to_string(),
2556            CompressionFieldParams {
2557                compression: Some(if cfg!(feature = "lz4") { "lz4" } else { "zstd" }.to_string()),
2558                ..Default::default()
2559            },
2560        );
2561
2562        let strategy = strategy(TestEncoding::StructuralU16, params);
2563        let field = create_test_field("dict_values", DataType::FixedSizeBinary(3));
2564        let data = create_fixed_width_block(24, 1024);
2565
2566        let (_compressor, encoding) = strategy
2567            .create_block_compressor(&field, &data)
2568            .expect("block compressor selection should succeed");
2569
2570        assert!(
2571            !matches!(encoding.compression.as_ref(), Some(Compression::General(_))),
2572            "general compression should not be selected for V2.1"
2573        );
2574    }
2575
2576    #[test]
2577    fn test_none_compression_disables_auto_general_block_compression() {
2578        let mut params = CompressionParams::new();
2579        params.columns.insert(
2580            "dict_values".to_string(),
2581            CompressionFieldParams {
2582                compression: Some("none".to_string()),
2583                ..Default::default()
2584            },
2585        );
2586
2587        let strategy = strategy(TestEncoding::StructuralU32, params);
2588        let field = create_test_field("dict_values", DataType::FixedSizeBinary(3));
2589        let data = create_fixed_width_block(24, 20_000);
2590
2591        assert!(
2592            data.data_size() > MIN_BLOCK_SIZE_FOR_GENERAL_COMPRESSION,
2593            "test requires block size above automatic general compression threshold"
2594        );
2595
2596        let (_compressor, encoding) = strategy
2597            .create_block_compressor(&field, &data)
2598            .expect("block compressor selection should succeed");
2599
2600        assert!(
2601            !matches!(encoding.compression.as_ref(), Some(Compression::General(_))),
2602            "compression=none should disable automatic block general compression"
2603        );
2604    }
2605
2606    #[test]
2607    fn test_rle_v2_block_selects_u32_run_lengths() {
2608        let field = create_test_field("dict_indices", DataType::Int32);
2609        let expected_values = vec![42i32; 70_000];
2610        let mut block = FixedWidthDataBlock {
2611            bits_per_value: 32,
2612            data: LanceBuffer::reinterpret_vec(expected_values.clone()),
2613            num_values: expected_values.len() as u64,
2614            block_info: BlockInfo::default(),
2615        };
2616        block.compute_stat();
2617        let data = DataBlock::FixedWidth(block);
2618
2619        let strategy = strategy(TestEncoding::StructuralSparse, CompressionParams::new());
2620        let (compressor, encoding) = strategy.create_block_compressor(&field, &data).unwrap();
2621        assert_eq!(rle_run_length_bits(&encoding), 32);
2622
2623        let compressed = compressor.compress(data).unwrap();
2624        let decompressor = DefaultDecompressionStrategy::default()
2625            .create_block_decompressor(&encoding)
2626            .unwrap();
2627        let decoded = decompressor
2628            .decompress(compressed, expected_values.len() as u64)
2629            .unwrap();
2630
2631        match decoded {
2632            DataBlock::FixedWidth(block) => {
2633                let values = block.data.borrow_to_typed_slice::<i32>();
2634                assert_eq!(values.as_ref(), expected_values);
2635            }
2636            _ => panic!("expected fixed-width block"),
2637        }
2638    }
2639
2640    #[test]
2641    fn test_rle_v2_block_keeps_u8_run_lengths_for_v2_2() {
2642        let field = create_test_field("dict_indices", DataType::Int32);
2643        let values = vec![42i32; 70_000];
2644        let mut block = FixedWidthDataBlock {
2645            bits_per_value: 32,
2646            data: LanceBuffer::reinterpret_vec(values),
2647            num_values: 70_000,
2648            block_info: BlockInfo::default(),
2649        };
2650        block.compute_stat();
2651        let data = DataBlock::FixedWidth(block);
2652
2653        let strategy = strategy(TestEncoding::StructuralU32, CompressionParams::new());
2654        let (_compressor, encoding) = strategy.create_block_compressor(&field, &data).unwrap();
2655        assert_eq!(rle_run_length_bits(&encoding), 8);
2656    }
2657
2658    #[test]
2659    fn test_rle_block_used_for_version_v2_2() {
2660        let field = create_test_field("test_repdef", DataType::UInt16);
2661
2662        // Create highly repetitive data
2663        let num_values = 1000u64;
2664        let mut data = Vec::with_capacity(num_values as usize);
2665        for i in 0..10 {
2666            for _ in 0..100 {
2667                data.push(i as u16);
2668            }
2669        }
2670
2671        let mut block = FixedWidthDataBlock {
2672            bits_per_value: 16,
2673            data: LanceBuffer::reinterpret_vec(data),
2674            num_values,
2675            block_info: BlockInfo::default(),
2676        };
2677
2678        block.compute_stat();
2679
2680        let data_block = DataBlock::FixedWidth(block);
2681
2682        let strategy = strategy(TestEncoding::StructuralU32, CompressionParams::new());
2683
2684        let (compressor, _) = strategy
2685            .create_block_compressor(&field, &data_block)
2686            .unwrap();
2687
2688        let debug_str = format!("{:?}", compressor);
2689        assert!(debug_str.contains("RleEncoder"));
2690    }
2691
2692    #[test]
2693    fn test_rle_block_not_used_for_version_v2_1() {
2694        let field = create_test_field("test_repdef", DataType::UInt16);
2695
2696        // Create highly repetitive data
2697        let num_values = 1000u64;
2698        let mut data = Vec::with_capacity(num_values as usize);
2699        for i in 0..10 {
2700            for _ in 0..100 {
2701                data.push(i as u16);
2702            }
2703        }
2704
2705        let mut block = FixedWidthDataBlock {
2706            bits_per_value: 16,
2707            data: LanceBuffer::reinterpret_vec(data),
2708            num_values,
2709            block_info: BlockInfo::default(),
2710        };
2711
2712        block.compute_stat();
2713
2714        let data_block = DataBlock::FixedWidth(block);
2715
2716        let strategy = strategy(TestEncoding::StructuralU16, CompressionParams::new());
2717
2718        let (compressor, _) = strategy
2719            .create_block_compressor(&field, &data_block)
2720            .unwrap();
2721
2722        let debug_str = format!("{:?}", compressor);
2723        assert!(
2724            !debug_str.contains("RleEncoder"),
2725            "RLE should not be used for V2.1"
2726        );
2727    }
2728}