Skip to main content

lance_encoding/encodings/physical/
value.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use arrow_buffer::{BooleanBufferBuilder, bit_util};
5
6use crate::buffer::LanceBuffer;
7use crate::compression::{
8    BlockCompressor, BlockDecompressor, FixedPerValueDecompressor, MiniBlockDecompressor,
9};
10use crate::data::{
11    BlockInfo, DataBlock, FixedSizeListBlock, FixedWidthDataBlock, NullableDataBlock,
12};
13use crate::encodings::logical::primitive::fullzip::{PerValueCompressor, PerValueDataBlock};
14use crate::encodings::logical::primitive::miniblock::{
15    MAX_MINIBLOCK_BYTES, MAX_MINIBLOCK_VALUES, MiniBlockChunk, MiniBlockCompressed,
16    MiniBlockCompressor,
17};
18use crate::format::ProtobufUtils21;
19use crate::format::pb21::compressive_encoding::Compression;
20use crate::format::pb21::{self, CompressiveEncoding};
21
22use lance_core::{Error, Result};
23
24/// A compression strategy that writes fixed-width data as-is (no compression)
25#[derive(Debug, Default)]
26pub struct ValueEncoder {}
27
28impl ValueEncoder {
29    /// Use the largest chunk we can smaller than 4KiB
30    fn find_log_vals_per_chunk(bytes_per_word: u64, values_per_word: u64) -> Result<(u64, u64)> {
31        let mut size_bytes = 2 * bytes_per_word;
32        let (mut log_num_vals, mut num_vals) = match values_per_word {
33            1 => (1, 2),
34            8 => (3, 8),
35            _ => unreachable!(),
36        };
37
38        if size_bytes >= MAX_MINIBLOCK_BYTES {
39            let num_values = 2 * values_per_word;
40            return Err(Error::invalid_input(format!(
41                "Value is too wide for miniblock encoding: {} values require {} bytes but a \
42                 miniblock chunk is limited to {} bytes.",
43                num_values, size_bytes, MAX_MINIBLOCK_BYTES
44            )));
45        }
46
47        while 2 * size_bytes < MAX_MINIBLOCK_BYTES && 2 * num_vals <= *MAX_MINIBLOCK_VALUES {
48            log_num_vals += 1;
49            size_bytes *= 2;
50            num_vals *= 2;
51        }
52
53        Ok((log_num_vals, num_vals))
54    }
55
56    fn chunk_data(data: FixedWidthDataBlock) -> Result<MiniBlockCompressed> {
57        // Usually there are X bytes per value.  However, when working with boolean
58        // or FSL<boolean> we might have some number of bits per value that isn't
59        // divisible by 8.  In this case, to avoid chunking in the middle of a byte
60        // we calculate how many 8-value words we can fit in a chunk.
61        let (bytes_per_word, values_per_word) = if data.bits_per_value.is_multiple_of(8) {
62            (data.bits_per_value / 8, 1)
63        } else {
64            (data.bits_per_value, 8)
65        };
66
67        // Aim for 4KiB chunks
68        let (log_vals_per_chunk, vals_per_chunk) =
69            Self::find_log_vals_per_chunk(bytes_per_word, values_per_word)?;
70        let num_chunks = bit_util::ceil(data.num_values as usize, vals_per_chunk as usize);
71        debug_assert_eq!(vals_per_chunk % values_per_word, 0);
72        let bytes_per_chunk = bytes_per_word * (vals_per_chunk / values_per_word);
73        let bytes_per_chunk = u32::try_from(bytes_per_chunk).unwrap();
74        debug_assert!(bytes_per_chunk > 0);
75
76        let data_buffer = data.data;
77
78        let mut row_offset = 0;
79        let mut chunks = Vec::with_capacity(num_chunks);
80
81        let mut bytes_counter = 0;
82        loop {
83            if row_offset + vals_per_chunk <= data.num_values {
84                // We can make a full chunk
85                chunks.push(MiniBlockChunk {
86                    log_num_values: log_vals_per_chunk as u8,
87                    buffer_sizes: vec![bytes_per_chunk],
88                });
89                row_offset += vals_per_chunk;
90                bytes_counter += bytes_per_chunk as u64;
91            } else if row_offset < data.num_values {
92                // Final chunk, special values
93                let num_bytes = data_buffer.len() as u64 - bytes_counter;
94                let num_bytes = u32::try_from(num_bytes).unwrap();
95                chunks.push(MiniBlockChunk {
96                    log_num_values: 0,
97                    buffer_sizes: vec![num_bytes],
98                });
99                break;
100            } else {
101                // If we get here then all chunks were full chunks and we have no remainder chunk
102                break;
103            }
104        }
105
106        debug_assert_eq!(chunks.len(), num_chunks);
107
108        Ok(MiniBlockCompressed {
109            chunks,
110            data: vec![data_buffer],
111            num_values: data.num_values,
112        })
113    }
114}
115
116#[derive(Debug)]
117struct MiniblockFslLayer {
118    validity: Option<LanceBuffer>,
119    dimension: u64,
120}
121
122/// This impl deals with encoding FSL<FSL<...<FSL<FixedWidth>>>> data as a mini-block compressor.
123/// The tricky part of FSL data is that we want to include inner validity buffers (we don't want these
124/// to be part of the rep-def because that usually ends up being more expensive).
125///
126/// The resulting mini-block will, instead of having a single buffer, have X + 1 buffers where X is
127/// the number of FSL layers that contain validity.
128///
129/// In the simple case where there is no validity inside the FSL layers, all we are doing here is flattening
130/// the FSL layers into a single buffer.
131///
132/// Also: We don't allow a row to be broken across chunks.  This typically isn't too big of a deal since we
133/// are usually dealing with relatively small vectors if we are using mini-block.
134///
135/// Note: when we do have validity we have to make copies of the validity buffers because they are bit buffers
136/// and we need to bit slice them which requires copies or offsets.  Paying the price at write time to make
137/// the copies is better than paying the price at read time to do the bit slicing.
138impl ValueEncoder {
139    fn make_fsl_encoding(layers: &[MiniblockFslLayer], bits_per_value: u64) -> CompressiveEncoding {
140        let mut encoding = ProtobufUtils21::flat(bits_per_value, None);
141        for layer in layers.iter().rev() {
142            let has_validity = layer.validity.is_some();
143            let dimension = layer.dimension;
144            encoding = ProtobufUtils21::fsl(dimension, has_validity, encoding);
145        }
146        encoding
147    }
148
149    fn extract_fsl_chunk(
150        data: &FixedWidthDataBlock,
151        layers: &[MiniblockFslLayer],
152        row_offset: usize,
153        num_rows: usize,
154        validity_buffers: &mut [Vec<u8>],
155    ) -> Vec<u32> {
156        let mut row_offset = row_offset;
157        let mut num_values = num_rows;
158        let mut buffer_counter = 0;
159        let mut buffer_sizes = Vec::with_capacity(validity_buffers.len() + 1);
160        for layer in layers {
161            row_offset *= layer.dimension as usize;
162            num_values *= layer.dimension as usize;
163            if let Some(validity) = &layer.validity {
164                let validity_slice = validity
165                    .clone()
166                    .bit_slice_le_with_length(row_offset, num_values);
167                validity_buffers[buffer_counter].extend_from_slice(&validity_slice);
168                buffer_sizes.push(validity_slice.len() as u32);
169                buffer_counter += 1;
170            }
171        }
172
173        let bits_in_chunk = data.bits_per_value * num_values as u64;
174        let bytes_in_chunk = bits_in_chunk.div_ceil(8);
175        let bytes_in_chunk = u32::try_from(bytes_in_chunk).unwrap();
176        debug_assert!(bytes_in_chunk > 0);
177        buffer_sizes.push(bytes_in_chunk);
178
179        buffer_sizes
180    }
181
182    fn chunk_fsl(
183        data: FixedWidthDataBlock,
184        layers: Vec<MiniblockFslLayer>,
185        num_rows: u64,
186    ) -> Result<(MiniBlockCompressed, CompressiveEncoding)> {
187        // Count size to calculate rows per chunk
188        let mut ceil_bytes_validity = 0;
189        let mut cum_dim = 1;
190        let mut num_validity_buffers = 0;
191        for layer in &layers {
192            cum_dim *= layer.dimension;
193            if layer.validity.is_some() {
194                ceil_bytes_validity += cum_dim.div_ceil(8);
195                num_validity_buffers += 1;
196            }
197        }
198        // It's an estimate because validity buffers may have some padding bits
199        let cum_bits_per_value = data.bits_per_value * cum_dim;
200        let (cum_bytes_per_word, vals_per_word) = if cum_bits_per_value.is_multiple_of(8) {
201            (cum_bits_per_value / 8, 1)
202        } else {
203            (cum_bits_per_value, 8)
204        };
205        let est_bytes_per_word = (ceil_bytes_validity * vals_per_word) + cum_bytes_per_word;
206        let (log_rows_per_chunk, rows_per_chunk) =
207            Self::find_log_vals_per_chunk(est_bytes_per_word, vals_per_word)?;
208
209        let num_chunks = num_rows.div_ceil(rows_per_chunk) as usize;
210
211        // Allocate buffers for validity, these will be slightly bigger than the input validity buffers
212        let mut chunks = Vec::with_capacity(num_chunks);
213        let mut validity_buffers: Vec<Vec<u8>> = Vec::with_capacity(num_validity_buffers);
214        cum_dim = 1;
215        for layer in &layers {
216            cum_dim *= layer.dimension;
217            if let Some(validity) = &layer.validity {
218                let layer_bytes_validity = cum_dim.div_ceil(8);
219                let validity_with_padding =
220                    layer_bytes_validity as usize * num_chunks * rows_per_chunk as usize;
221                debug_assert!(validity_with_padding >= validity.len());
222                validity_buffers.push(Vec::with_capacity(
223                    layer_bytes_validity as usize * num_chunks,
224                ));
225            }
226        }
227
228        // Now go through and extract validity buffers
229        let mut row_offset = 0;
230        while row_offset + rows_per_chunk <= num_rows {
231            let buffer_sizes = Self::extract_fsl_chunk(
232                &data,
233                &layers,
234                row_offset as usize,
235                rows_per_chunk as usize,
236                &mut validity_buffers,
237            );
238            row_offset += rows_per_chunk;
239            chunks.push(MiniBlockChunk {
240                log_num_values: log_rows_per_chunk as u8,
241                buffer_sizes,
242            })
243        }
244        let rows_in_chunk = num_rows - row_offset;
245        if rows_in_chunk > 0 {
246            let buffer_sizes = Self::extract_fsl_chunk(
247                &data,
248                &layers,
249                row_offset as usize,
250                rows_in_chunk as usize,
251                &mut validity_buffers,
252            );
253            chunks.push(MiniBlockChunk {
254                log_num_values: 0,
255                buffer_sizes,
256            });
257        }
258
259        let encoding = Self::make_fsl_encoding(&layers, data.bits_per_value);
260        // Finally, add the data buffer
261        let buffers = validity_buffers
262            .into_iter()
263            .map(LanceBuffer::from)
264            .chain(std::iter::once(data.data))
265            .collect::<Vec<_>>();
266
267        Ok((
268            MiniBlockCompressed {
269                chunks,
270                data: buffers,
271                num_values: num_rows,
272            },
273            encoding,
274        ))
275    }
276
277    fn miniblock_fsl(data: DataBlock) -> Result<(MiniBlockCompressed, CompressiveEncoding)> {
278        let num_rows = data.num_values();
279        let fsl = data.as_fixed_size_list().unwrap();
280        let mut layers = Vec::new();
281        let mut child = *fsl.child;
282        let mut cur_layer = MiniblockFslLayer {
283            validity: None,
284            dimension: fsl.dimension,
285        };
286        loop {
287            if let DataBlock::Nullable(nullable) = child {
288                cur_layer.validity = Some(nullable.nulls);
289                child = *nullable.data;
290            }
291            match child {
292                DataBlock::FixedSizeList(inner) => {
293                    layers.push(cur_layer);
294                    cur_layer = MiniblockFslLayer {
295                        validity: None,
296                        dimension: inner.dimension,
297                    };
298                    child = *inner.child;
299                }
300                DataBlock::FixedWidth(inner) => {
301                    layers.push(cur_layer);
302                    return Self::chunk_fsl(inner, layers, num_rows);
303                }
304                _ => unreachable!("Unexpected data block type in value encoder's miniblock_fsl"),
305            }
306        }
307    }
308}
309
310struct PerValueFslValidityIter {
311    buffer: LanceBuffer,
312    bits_per_row: usize,
313    offset: usize,
314}
315
316/// In this section we deal with per-value encoding of FSL<FSL<...<FSL<FixedWidth>>>> data.
317///
318/// It's easier than mini-block.  All we need to do is flatten the FSL layers into a single buffer.
319/// This includes any validity buffers we encounter on the way.
320impl ValueEncoder {
321    fn fsl_to_encoding(fsl: &FixedSizeListBlock) -> CompressiveEncoding {
322        let mut inner = fsl.child.as_ref();
323        let mut has_validity = false;
324        inner = match inner {
325            DataBlock::Nullable(nullable) => {
326                has_validity = true;
327                nullable.data.as_ref()
328            }
329            DataBlock::AllNull(_) => {
330                return ProtobufUtils21::constant(None);
331            }
332            _ => inner,
333        };
334        let inner_encoding = match inner {
335            DataBlock::FixedWidth(fixed_width) => {
336                ProtobufUtils21::flat(fixed_width.bits_per_value, None)
337            }
338            DataBlock::FixedSizeList(inner) => Self::fsl_to_encoding(inner),
339            _ => unreachable!(
340                "Unexpected data block type in value encoder's fsl_to_encoding: {}",
341                inner.name()
342            ),
343        };
344        ProtobufUtils21::fsl(fsl.dimension, has_validity, inner_encoding)
345    }
346
347    fn simple_per_value_fsl(fsl: FixedSizeListBlock) -> (PerValueDataBlock, CompressiveEncoding) {
348        // The simple case is zero-copy, we just return the flattened inner buffer
349        let encoding = Self::fsl_to_encoding(&fsl);
350        let num_values = fsl.num_values();
351        let mut child = *fsl.child;
352        let mut cum_dim = 1;
353        loop {
354            cum_dim *= fsl.dimension;
355            match child {
356                DataBlock::Nullable(nullable) => {
357                    child = *nullable.data;
358                }
359                DataBlock::FixedSizeList(inner) => {
360                    child = *inner.child;
361                }
362                DataBlock::FixedWidth(inner) => {
363                    let data = FixedWidthDataBlock {
364                        bits_per_value: inner.bits_per_value * cum_dim,
365                        num_values,
366                        data: inner.data,
367                        block_info: BlockInfo::new(),
368                    };
369                    return (PerValueDataBlock::Fixed(data), encoding);
370                }
371                _ => unreachable!(
372                    "Unexpected data block type in value encoder's simple_per_value_fsl"
373                ),
374            }
375        }
376    }
377
378    fn nullable_per_value_fsl(fsl: FixedSizeListBlock) -> (PerValueDataBlock, CompressiveEncoding) {
379        // If there are nullable inner values then we need to zip the validity with the values
380        let encoding = Self::fsl_to_encoding(&fsl);
381        let num_values = fsl.num_values();
382        let mut bytes_per_row = 0;
383        let mut cum_dim = 1;
384        let mut current = fsl;
385        let mut validity_iters: Vec<PerValueFslValidityIter> = Vec::new();
386        let data_bytes_per_row: usize;
387        let data_buffer: LanceBuffer;
388        loop {
389            cum_dim *= current.dimension;
390            let mut child = *current.child;
391            if let DataBlock::Nullable(nullable) = child {
392                // Each item will need this many bytes of validity prepended to it
393                bytes_per_row += cum_dim.div_ceil(8) as usize;
394                validity_iters.push(PerValueFslValidityIter {
395                    buffer: nullable.nulls,
396                    bits_per_row: cum_dim as usize,
397                    offset: 0,
398                });
399                child = *nullable.data;
400            };
401            match child {
402                DataBlock::FixedSizeList(inner) => {
403                    current = inner;
404                }
405                DataBlock::FixedWidth(fixed_width) => {
406                    data_bytes_per_row =
407                        (fixed_width.bits_per_value.div_ceil(8) * cum_dim) as usize;
408                    bytes_per_row += data_bytes_per_row;
409                    data_buffer = fixed_width.data;
410                    break;
411                }
412                DataBlock::AllNull(_) => {
413                    data_bytes_per_row = 0;
414                    data_buffer = LanceBuffer::empty();
415                    break;
416                }
417                _ => unreachable!(
418                    "Unexpected data block type in value encoder's nullable_per_value_fsl: {:?}",
419                    child
420                ),
421            }
422        }
423
424        let bytes_needed = bytes_per_row * num_values as usize;
425        let mut zipped = Vec::with_capacity(bytes_needed);
426        let data_slice = &data_buffer;
427        // Hopefully values are pretty large so we don't iterate this loop _too_ many times
428        for i in 0..num_values as usize {
429            for validity in validity_iters.iter_mut() {
430                let validity_slice = validity
431                    .buffer
432                    .bit_slice_le_with_length(validity.offset, validity.bits_per_row);
433                zipped.extend_from_slice(&validity_slice);
434                validity.offset += validity.bits_per_row;
435            }
436            let start = i * data_bytes_per_row;
437            let end = start + data_bytes_per_row;
438            zipped.extend_from_slice(&data_slice[start..end]);
439        }
440
441        let zipped = LanceBuffer::from(zipped);
442        let data = PerValueDataBlock::Fixed(FixedWidthDataBlock {
443            bits_per_value: bytes_per_row as u64 * 8,
444            num_values,
445            data: zipped,
446            block_info: BlockInfo::new(),
447        });
448        (data, encoding)
449    }
450
451    fn per_value_fsl(fsl: FixedSizeListBlock) -> (PerValueDataBlock, CompressiveEncoding) {
452        if !fsl.child.is_nullable() {
453            Self::simple_per_value_fsl(fsl)
454        } else {
455            Self::nullable_per_value_fsl(fsl)
456        }
457    }
458}
459
460impl BlockCompressor for ValueEncoder {
461    fn compress(&self, data: DataBlock) -> Result<LanceBuffer> {
462        let data = match data {
463            DataBlock::FixedWidth(fixed_width) => fixed_width.data,
464            _ => unimplemented!(
465                "Cannot compress block of type {} with ValueEncoder",
466                data.name()
467            ),
468        };
469        Ok(data)
470    }
471}
472
473impl MiniBlockCompressor for ValueEncoder {
474    fn compress(&self, chunk: DataBlock) -> Result<(MiniBlockCompressed, CompressiveEncoding)> {
475        match chunk {
476            DataBlock::FixedWidth(fixed_width) => {
477                let encoding = ProtobufUtils21::flat(fixed_width.bits_per_value, None);
478                Ok((Self::chunk_data(fixed_width)?, encoding))
479            }
480            DataBlock::FixedSizeList(_) => Self::miniblock_fsl(chunk),
481            _ => Err(Error::invalid_input_source(
482                format!(
483                    "Cannot compress a data block of type {} with ValueEncoder",
484                    chunk.name()
485                )
486                .into(),
487            )),
488        }
489    }
490}
491
492#[derive(Debug)]
493struct ValueFslDesc {
494    dimension: u64,
495    has_validity: bool,
496}
497
498/// A decompressor for fixed-width data that has
499/// been written, as-is, to disk in single contiguous array
500#[derive(Debug)]
501pub struct ValueDecompressor {
502    /// How many bits are in each inner-most item (e.g. FSL<Int32, 100> would be 32)
503    bits_per_item: u64,
504    /// How many bits are in each value (e.g. FSL<Int32, 100> would be 3200)
505    ///
506    /// This number is a little trickier to compute because we also have to include bytes
507    /// of any inner validity
508    bits_per_value: u64,
509    /// How many items are in each value (e.g. FSL<Int32, 100> would be 100)
510    items_per_value: u64,
511    layers: Vec<ValueFslDesc>,
512}
513
514impl ValueDecompressor {
515    pub fn from_flat(description: &pb21::Flat) -> Self {
516        Self {
517            bits_per_item: description.bits_per_value,
518            bits_per_value: description.bits_per_value,
519            items_per_value: 1,
520            layers: Vec::default(),
521        }
522    }
523
524    pub fn from_fsl(mut description: &pb21::FixedSizeList) -> Self {
525        let mut layers = Vec::new();
526        let mut cum_dim = 1;
527        let mut bytes_per_value = 0;
528        loop {
529            layers.push(ValueFslDesc {
530                has_validity: description.has_validity,
531                dimension: description.items_per_value,
532            });
533            cum_dim *= description.items_per_value;
534            if description.has_validity {
535                bytes_per_value += cum_dim.div_ceil(8);
536            }
537            match description
538                .values
539                .as_ref()
540                .unwrap()
541                .compression
542                .as_ref()
543                .unwrap()
544            {
545                Compression::FixedSizeList(inner) => {
546                    description = inner;
547                }
548                Compression::Flat(flat) => {
549                    let mut bits_per_value = bytes_per_value * 8;
550                    bits_per_value += flat.bits_per_value * cum_dim;
551                    return Self {
552                        bits_per_item: flat.bits_per_value,
553                        bits_per_value,
554                        items_per_value: cum_dim,
555                        layers,
556                    };
557                }
558                _ => unreachable!(),
559            }
560        }
561    }
562
563    fn buffer_to_block(&self, data: LanceBuffer, num_values: u64) -> DataBlock {
564        DataBlock::FixedWidth(FixedWidthDataBlock {
565            bits_per_value: self.bits_per_item,
566            num_values,
567            data,
568            block_info: BlockInfo::new(),
569        })
570    }
571}
572
573impl BlockDecompressor for ValueDecompressor {
574    fn decompress(&self, data: LanceBuffer, num_values: u64) -> Result<DataBlock> {
575        let block = self.buffer_to_block(data, num_values);
576        assert_eq!(block.num_values(), num_values);
577        Ok(block)
578    }
579}
580
581impl MiniBlockDecompressor for ValueDecompressor {
582    fn decompress(&self, data: Vec<LanceBuffer>, num_values: u64) -> Result<DataBlock> {
583        let num_items = num_values * self.items_per_value;
584        let mut buffer_iter = data.into_iter().rev();
585
586        // Always at least 1 buffer
587        let data_buf = buffer_iter.next().unwrap();
588        let items = self.buffer_to_block(data_buf, num_items);
589        let mut lists = items;
590
591        for layer in self.layers.iter().rev() {
592            if layer.has_validity {
593                let validity_buf = buffer_iter.next().unwrap();
594                lists = DataBlock::Nullable(NullableDataBlock {
595                    data: Box::new(lists),
596                    nulls: validity_buf,
597                    block_info: BlockInfo::default(),
598                });
599            }
600            lists = DataBlock::FixedSizeList(FixedSizeListBlock {
601                child: Box::new(lists),
602                dimension: layer.dimension,
603            })
604        }
605
606        assert_eq!(lists.num_values(), num_values);
607        Ok(lists)
608    }
609}
610
611struct FslDecompressorValidityBuilder {
612    buffer: BooleanBufferBuilder,
613    bits_per_row: usize,
614    bytes_per_row: usize,
615}
616
617// Helper methods for per-value decompression
618impl ValueDecompressor {
619    fn has_validity(&self) -> bool {
620        self.layers.iter().any(|layer| layer.has_validity)
621    }
622
623    // If there is no validity then decompression is zero-copy, we just need to restore any FSL layers
624    fn simple_decompress(&self, data: FixedWidthDataBlock, num_rows: u64) -> DataBlock {
625        let mut cum_dim = 1;
626        for layer in &self.layers {
627            cum_dim *= layer.dimension;
628        }
629        debug_assert_eq!(self.bits_per_item, data.bits_per_value / cum_dim);
630        let mut block = DataBlock::FixedWidth(FixedWidthDataBlock {
631            bits_per_value: self.bits_per_item,
632            num_values: num_rows * cum_dim,
633            data: data.data,
634            block_info: BlockInfo::new(),
635        });
636        for layer in self.layers.iter().rev() {
637            block = DataBlock::FixedSizeList(FixedSizeListBlock {
638                child: Box::new(block),
639                dimension: layer.dimension,
640            });
641        }
642        debug_assert_eq!(num_rows, block.num_values());
643        block
644    }
645
646    // If there is validity then it has been zipped in with the values and we must unzip it
647    fn unzip_decompress(&self, data: FixedWidthDataBlock, num_rows: usize) -> DataBlock {
648        // No support for full-zip on per-value encodings
649        assert_eq!(self.bits_per_item % 8, 0);
650        let bytes_per_item = self.bits_per_item / 8;
651        let mut buffer_builders = Vec::with_capacity(self.layers.len());
652        let mut cum_dim = 1;
653        let mut total_size_bytes = 0;
654        // First, go through the layers, setup our builders, allocate space
655        for layer in &self.layers {
656            cum_dim *= layer.dimension as usize;
657            if layer.has_validity {
658                let validity_size_bits = cum_dim;
659                let validity_size_bytes = validity_size_bits.div_ceil(8);
660                total_size_bytes += num_rows * validity_size_bytes;
661                buffer_builders.push(FslDecompressorValidityBuilder {
662                    buffer: BooleanBufferBuilder::new(validity_size_bits * num_rows),
663                    bits_per_row: cum_dim,
664                    bytes_per_row: validity_size_bytes,
665                })
666            }
667        }
668        let num_items = num_rows * cum_dim;
669        let data_size = num_items * bytes_per_item as usize;
670        total_size_bytes += data_size;
671        let mut data_buffer = Vec::with_capacity(data_size);
672
673        assert_eq!(data.data.len(), total_size_bytes);
674
675        let bytes_per_value = bytes_per_item as usize;
676        let data_bytes_per_row = bytes_per_value * cum_dim;
677
678        // Next, unzip
679        let mut data_offset = 0;
680        while data_offset < total_size_bytes {
681            for builder in buffer_builders.iter_mut() {
682                let start = data_offset * 8;
683                let end = start + builder.bits_per_row;
684                builder.buffer.append_packed_range(start..end, &data.data);
685                data_offset += builder.bytes_per_row;
686            }
687            let end = data_offset + data_bytes_per_row;
688            data_buffer.extend_from_slice(&data.data[data_offset..end]);
689            data_offset += data_bytes_per_row;
690        }
691
692        // Finally, restore the structure
693        let mut block = DataBlock::FixedWidth(FixedWidthDataBlock {
694            bits_per_value: self.bits_per_item,
695            num_values: num_items as u64,
696            data: LanceBuffer::from(data_buffer),
697            block_info: BlockInfo::new(),
698        });
699
700        let mut validity_bufs = buffer_builders
701            .into_iter()
702            .rev()
703            .map(|mut b| LanceBuffer::from(b.buffer.finish().into_inner()));
704        for layer in self.layers.iter().rev() {
705            if layer.has_validity {
706                let nullable = NullableDataBlock {
707                    data: Box::new(block),
708                    nulls: validity_bufs.next().unwrap(),
709                    block_info: BlockInfo::new(),
710                };
711                block = DataBlock::Nullable(nullable);
712            }
713            block = DataBlock::FixedSizeList(FixedSizeListBlock {
714                child: Box::new(block),
715                dimension: layer.dimension,
716            });
717        }
718
719        assert_eq!(num_rows, block.num_values() as usize);
720
721        block
722    }
723}
724
725impl FixedPerValueDecompressor for ValueDecompressor {
726    fn decompress(&self, data: FixedWidthDataBlock, num_rows: u64) -> Result<DataBlock> {
727        if self.has_validity() {
728            Ok(self.unzip_decompress(data, num_rows as usize))
729        } else {
730            Ok(self.simple_decompress(data, num_rows))
731        }
732    }
733
734    fn bits_per_value(&self) -> u64 {
735        self.bits_per_value
736    }
737}
738
739impl PerValueCompressor for ValueEncoder {
740    fn compress(&self, data: DataBlock) -> Result<(PerValueDataBlock, CompressiveEncoding)> {
741        let (data, encoding) = match data {
742            DataBlock::FixedWidth(fixed_width) => {
743                let encoding = ProtobufUtils21::flat(fixed_width.bits_per_value, None);
744                (PerValueDataBlock::Fixed(fixed_width), encoding)
745            }
746            DataBlock::FixedSizeList(fixed_size_list) => Self::per_value_fsl(fixed_size_list),
747            _ => unimplemented!(
748                "Cannot compress block of type {} with ValueEncoder",
749                data.name()
750            ),
751        };
752        Ok((data, encoding))
753    }
754}
755
756// public tests module because we share the PRIMITIVE_TYPES constant with fixed_size_list
757#[cfg(test)]
758mod tests {
759    use std::{
760        collections::HashMap,
761        sync::{Arc, LazyLock},
762    };
763
764    use arrow_array::{
765        Array, ArrayRef, Decimal128Array, FixedSizeListArray, Int32Array, ListArray, UInt8Array,
766        make_array, new_null_array, types::UInt32Type,
767    };
768    use arrow_buffer::{BooleanBuffer, NullBuffer, OffsetBuffer, ScalarBuffer};
769    use arrow_schema::{DataType, Field, TimeUnit};
770    use lance_datagen::{ArrayGeneratorExt, Dimension, RowCount, array, gen_batch};
771
772    use crate::{
773        compression::{FixedPerValueDecompressor, MiniBlockDecompressor},
774        data::DataBlock,
775        encodings::{
776            logical::primitive::{
777                fullzip::{PerValueCompressor, PerValueDataBlock},
778                miniblock::MiniBlockCompressor,
779            },
780            physical::value::ValueDecompressor,
781        },
782        format::pb21::compressive_encoding::Compression,
783        testing::{
784            FnArrayGeneratorProvider, TestCases, check_basic_random,
785            check_round_trip_encoding_generated, check_round_trip_encoding_of_data,
786        },
787        version::LanceFileVersion,
788    };
789
790    use super::ValueEncoder;
791
792    const PRIMITIVE_TYPES: &[DataType] = &[
793        DataType::Null,
794        DataType::FixedSizeBinary(2),
795        DataType::Date32,
796        DataType::Date64,
797        DataType::Int8,
798        DataType::Int16,
799        DataType::Int32,
800        DataType::Int64,
801        DataType::UInt8,
802        DataType::UInt16,
803        DataType::UInt32,
804        DataType::UInt64,
805        DataType::Float16,
806        DataType::Float32,
807        DataType::Float64,
808        DataType::Decimal128(10, 10),
809        DataType::Decimal256(10, 10),
810        DataType::Timestamp(TimeUnit::Nanosecond, None),
811        DataType::Time32(TimeUnit::Second),
812        DataType::Time64(TimeUnit::Nanosecond),
813        DataType::Duration(TimeUnit::Second),
814        // The Interval type is supported by the reader but the writer works with Lance schema
815        // at the moment and Lance schema can't parse interval
816        // DataType::Interval(IntervalUnit::DayTime),
817    ];
818
819    #[test_log::test(tokio::test)]
820    async fn test_simple_value() {
821        let items = Arc::new(Int32Array::from(vec![
822            Some(0),
823            None,
824            Some(2),
825            Some(3),
826            Some(4),
827            Some(5),
828        ]));
829
830        let test_cases = TestCases::default()
831            .with_range(0..3)
832            .with_range(0..2)
833            .with_range(1..3)
834            .with_indices(vec![0, 1, 2])
835            .with_indices(vec![1])
836            .with_indices(vec![2])
837            .with_min_file_version(LanceFileVersion::V2_1);
838
839        check_round_trip_encoding_of_data(vec![items], &test_cases, HashMap::default()).await;
840    }
841
842    #[test_log::test(tokio::test)]
843    async fn test_simple_range() {
844        let items = Arc::new(Int32Array::from_iter(
845            (0..5000).map(|i| if i % 2 == 0 { Some(i) } else { None }),
846        ));
847
848        let test_cases = TestCases::default().with_min_file_version(LanceFileVersion::V2_1);
849
850        check_round_trip_encoding_of_data(vec![items], &test_cases, HashMap::default()).await;
851    }
852
853    #[test_log::test(tokio::test)]
854    async fn test_value_primitive() {
855        for data_type in PRIMITIVE_TYPES {
856            log::info!("Testing encoding for {:?}", data_type);
857            let field = Field::new("", data_type.clone(), false);
858            check_basic_random(field).await;
859        }
860    }
861
862    static LARGE_TYPES: LazyLock<Vec<DataType>> = LazyLock::new(|| {
863        vec![DataType::FixedSizeList(
864            Arc::new(Field::new("", DataType::Int32, false)),
865            128,
866        )]
867    });
868
869    #[test_log::test(tokio::test)]
870    async fn test_large_primitive() {
871        for data_type in LARGE_TYPES.iter() {
872            log::info!("Testing encoding for {:?}", data_type);
873            let field = Field::new("", data_type.clone(), false);
874            check_basic_random(field).await;
875        }
876    }
877
878    #[test_log::test(tokio::test)]
879    async fn test_decimal128_dictionary_encoding() {
880        let test_cases = TestCases::default().with_min_file_version(LanceFileVersion::V2_1);
881        let decimals: Vec<i32> = (0..100).collect();
882        let repeated_strings: Vec<_> = decimals
883            .iter()
884            .cycle()
885            .take(decimals.len() * 10000)
886            .map(|&v| Some(v as i128))
887            .collect();
888        let decimal_array = Arc::new(Decimal128Array::from(repeated_strings)) as ArrayRef;
889        check_round_trip_encoding_of_data(vec![decimal_array], &test_cases, HashMap::new()).await;
890    }
891
892    #[test_log::test(tokio::test)]
893    async fn test_miniblock_stress() {
894        // Tests for strange page sizes and batch sizes and validity scenarios for miniblock
895
896        // 10K integers, 100 per array, all valid
897        let data1 = (0..100)
898            .map(|_| Arc::new(Int32Array::from_iter_values(0..100)) as Arc<dyn Array>)
899            .collect::<Vec<_>>();
900
901        // Same as above but with mixed validity
902        let data2 = (0..100)
903            .map(|_| {
904                Arc::new(Int32Array::from_iter(
905                    (0..100).map(|i| if i % 2 == 0 { Some(i) } else { None }),
906                )) as Arc<dyn Array>
907            })
908            .collect::<Vec<_>>();
909
910        // Same as above but with all null for first half then all valid
911        // TODO: Re-enable once the all-null path is complete
912        let _data3 = (0..100)
913            .map(|chunk_idx| {
914                Arc::new(Int32Array::from_iter(
915                    (0..100).map(|i| if chunk_idx < 50 { None } else { Some(i) }),
916                )) as Arc<dyn Array>
917            })
918            .collect::<Vec<_>>();
919
920        for data in [data1, data2 /*data3*/] {
921            for batch_size in [10, 100, 1500, 15000] {
922                // 40000 bytes of data
923                let test_cases = TestCases::default()
924                    .with_page_sizes(vec![1000, 2000, 3000, 60000])
925                    .with_batch_size(batch_size)
926                    .with_min_file_version(LanceFileVersion::V2_1);
927
928                check_round_trip_encoding_of_data(data.clone(), &test_cases, HashMap::new()).await;
929            }
930        }
931    }
932
933    fn create_simple_fsl() -> FixedSizeListArray {
934        // [[0, 1], NULL], [NULL, NULL], [[8, 9], [NULL, 11]]
935        let items = Arc::new(Int32Array::from(vec![
936            Some(0),
937            Some(1),
938            Some(2),
939            Some(3),
940            None,
941            None,
942            None,
943            None,
944            Some(8),
945            Some(9),
946            None,
947            Some(11),
948        ]));
949        let items_field = Arc::new(Field::new("item", DataType::Int32, true));
950        let inner_list_nulls = BooleanBuffer::from(vec![true, false, false, false, true, true]);
951        let inner_list = Arc::new(FixedSizeListArray::new(
952            items_field.clone(),
953            2,
954            items,
955            Some(NullBuffer::new(inner_list_nulls)),
956        ));
957        let inner_list_field = Arc::new(Field::new(
958            "item",
959            DataType::FixedSizeList(items_field, 2),
960            true,
961        ));
962        FixedSizeListArray::new(inner_list_field, 2, inner_list, None)
963    }
964
965    #[test]
966    fn test_fsl_value_compression_miniblock() {
967        let sample_list = create_simple_fsl();
968
969        let starting_data = DataBlock::from_array(sample_list.clone());
970
971        let encoder = ValueEncoder::default();
972        let (data, compression) = MiniBlockCompressor::compress(&encoder, starting_data).unwrap();
973
974        assert_eq!(data.num_values, 3);
975        assert_eq!(data.data.len(), 3);
976        assert_eq!(data.chunks.len(), 1);
977        assert_eq!(data.chunks[0].buffer_sizes, vec![1, 2, 48]);
978        assert_eq!(data.chunks[0].log_num_values, 0);
979
980        let Compression::FixedSizeList(fsl) = compression.compression.unwrap() else {
981            panic!()
982        };
983
984        let decompressor = ValueDecompressor::from_fsl(fsl.as_ref());
985
986        let decompressed =
987            MiniBlockDecompressor::decompress(&decompressor, data.data, data.num_values).unwrap();
988
989        let decompressed = make_array(
990            decompressed
991                .into_arrow(sample_list.data_type().clone(), true)
992                .unwrap(),
993        );
994
995        assert_eq!(decompressed.as_ref(), &sample_list);
996    }
997
998    fn wide_fixed_size_binary() -> ArrayRef {
999        let wide_value = vec![0xABu8; 5000];
1000        Arc::new(
1001            arrow_array::FixedSizeBinaryArray::try_from_sparse_iter_with_size(
1002                std::iter::repeat_n(Some(wide_value.as_slice()), 4),
1003                5000,
1004            )
1005            .unwrap(),
1006        )
1007    }
1008
1009    fn wide_fixed_size_list_bool() -> ArrayRef {
1010        // A wide FSL<Boolean> is sub-byte, so it chunks eight values per word and the
1011        // smallest unit is 16 values rather than 2.
1012        let dimension = 4095;
1013        let values = arrow_array::BooleanArray::from(vec![false; dimension * 2]);
1014        let field = Arc::new(Field::new("item", DataType::Boolean, true));
1015        Arc::new(FixedSizeListArray::new(
1016            field,
1017            dimension as i32,
1018            Arc::new(values),
1019            None,
1020        ))
1021    }
1022
1023    #[rstest::rstest]
1024    #[case::fixed_size_binary(wide_fixed_size_binary(), 2)]
1025    #[case::fixed_size_list_bool(wide_fixed_size_list_bool(), 16)]
1026    fn test_wide_value_miniblock_returns_error(
1027        #[case] array: ArrayRef,
1028        #[case] expected_min_values: u64,
1029    ) {
1030        let starting_data = DataBlock::from_array(array);
1031
1032        let encoder = ValueEncoder::default();
1033        let result = MiniBlockCompressor::compress(&encoder, starting_data);
1034
1035        let err = result.expect_err("wide values should not be encodable as miniblock");
1036        assert!(
1037            matches!(err, lance_core::Error::InvalidInput { .. }),
1038            "expected InvalidInput, got {err:?}"
1039        );
1040        let msg = err.to_string();
1041        assert!(
1042            msg.contains("too wide for miniblock encoding"),
1043            "unexpected error message: {msg}"
1044        );
1045        assert!(
1046            msg.contains(&format!("{expected_min_values} values require")),
1047            "unexpected error message: {msg}"
1048        );
1049    }
1050
1051    #[test]
1052    fn test_fsl_value_compression_per_value() {
1053        let sample_list = create_simple_fsl();
1054
1055        let starting_data = DataBlock::from_array(sample_list.clone());
1056
1057        let encoder = ValueEncoder::default();
1058        let (data, compression) = PerValueCompressor::compress(&encoder, starting_data).unwrap();
1059
1060        let PerValueDataBlock::Fixed(data) = data else {
1061            panic!()
1062        };
1063
1064        assert_eq!(data.bits_per_value, 144);
1065        assert_eq!(data.num_values, 3);
1066        assert_eq!(data.data.len(), 18 * 3);
1067
1068        let Compression::FixedSizeList(fsl) = compression.compression.unwrap() else {
1069            panic!()
1070        };
1071
1072        let decompressor = ValueDecompressor::from_fsl(fsl.as_ref());
1073
1074        let num_values = data.num_values;
1075        let decompressed =
1076            FixedPerValueDecompressor::decompress(&decompressor, data, num_values).unwrap();
1077
1078        let decompressed = make_array(
1079            decompressed
1080                .into_arrow(sample_list.data_type().clone(), true)
1081                .unwrap(),
1082        );
1083
1084        assert_eq!(decompressed.as_ref(), &sample_list);
1085    }
1086
1087    #[test_log::test(tokio::test)]
1088    async fn test_fsl_all_null() {
1089        let items = new_null_array(&DataType::Int32, 12);
1090        let items_field = Arc::new(Field::new("item", DataType::Int32, true));
1091        let list_nulls = BooleanBuffer::from(vec![true, false, false, false, true, true]);
1092        let list_array =
1093            FixedSizeListArray::new(items_field, 2, items, Some(NullBuffer::new(list_nulls)));
1094
1095        let test_cases = TestCases::default().with_min_file_version(LanceFileVersion::V2_1);
1096
1097        check_round_trip_encoding_of_data(vec![Arc::new(list_array)], &test_cases, HashMap::new())
1098            .await;
1099    }
1100
1101    #[test_log::test(tokio::test)]
1102    async fn regress_list_fsl() {
1103        // This regresses a case where rows are large lists that span multiple
1104        // mini-block chunks which gives us some all-premable mini-block chunks.
1105        let offsets = ScalarBuffer::<i32>::from(vec![0, 393, 755, 1156, 1536]);
1106        let data = UInt8Array::from(vec![0; 1536 * 16]);
1107        let fsl_field = Arc::new(Field::new("item", DataType::UInt8, true));
1108        let fsl = FixedSizeListArray::new(fsl_field, 16, Arc::new(data), None);
1109        let list_field = Arc::new(Field::new("item", fsl.data_type().clone(), false));
1110        let list_arr = ListArray::new(list_field, OffsetBuffer::new(offsets), Arc::new(fsl), None);
1111
1112        let test_cases = TestCases::default()
1113            .with_min_file_version(LanceFileVersion::V2_1)
1114            .with_batch_size(1);
1115
1116        check_round_trip_encoding_of_data(vec![Arc::new(list_arr)], &test_cases, HashMap::new())
1117            .await;
1118    }
1119
1120    fn create_random_fsl() -> Arc<dyn Array> {
1121        // Several levels of def and multiple pages
1122        let inner = array::rand_type(&DataType::Int32).with_random_nulls(0.1);
1123        let list_one = array::cycle_vec(inner, Dimension::from(4)).with_random_nulls(0.1);
1124        let list_two = array::cycle_vec(list_one, Dimension::from(4)).with_random_nulls(0.1);
1125        let list_three = array::cycle_vec(list_two, Dimension::from(2));
1126
1127        // Should be 256Ki rows ~ 1MiB of data
1128        let batch = gen_batch()
1129            .anon_col(list_three)
1130            .into_batch_rows(RowCount::from(8 * 1024))
1131            .unwrap();
1132        batch.column(0).clone()
1133    }
1134
1135    #[test]
1136    fn fsl_value_miniblock_stress() {
1137        let sample_array = create_random_fsl();
1138
1139        let starting_data = DataBlock::from_arrays(
1140            std::slice::from_ref(&sample_array),
1141            sample_array.len() as u64,
1142        );
1143
1144        let encoder = ValueEncoder::default();
1145        let (data, compression) = MiniBlockCompressor::compress(&encoder, starting_data).unwrap();
1146
1147        let Compression::FixedSizeList(fsl) = compression.compression.unwrap() else {
1148            panic!()
1149        };
1150
1151        let decompressor = ValueDecompressor::from_fsl(fsl.as_ref());
1152
1153        let decompressed =
1154            MiniBlockDecompressor::decompress(&decompressor, data.data, data.num_values).unwrap();
1155
1156        let decompressed = make_array(
1157            decompressed
1158                .into_arrow(sample_array.data_type().clone(), true)
1159                .unwrap(),
1160        );
1161
1162        assert_eq!(decompressed.as_ref(), sample_array.as_ref());
1163    }
1164
1165    #[test]
1166    fn fsl_value_per_value_stress() {
1167        let sample_array = create_random_fsl();
1168
1169        let starting_data = DataBlock::from_arrays(
1170            std::slice::from_ref(&sample_array),
1171            sample_array.len() as u64,
1172        );
1173
1174        let encoder = ValueEncoder::default();
1175        let (data, compression) = PerValueCompressor::compress(&encoder, starting_data).unwrap();
1176
1177        let Compression::FixedSizeList(fsl) = compression.compression.unwrap() else {
1178            panic!()
1179        };
1180
1181        let decompressor = ValueDecompressor::from_fsl(fsl.as_ref());
1182
1183        let PerValueDataBlock::Fixed(data) = data else {
1184            panic!()
1185        };
1186
1187        let num_values = data.num_values;
1188        let decompressed =
1189            FixedPerValueDecompressor::decompress(&decompressor, data, num_values).unwrap();
1190
1191        let decompressed = make_array(
1192            decompressed
1193                .into_arrow(sample_array.data_type().clone(), true)
1194                .unwrap(),
1195        );
1196
1197        assert_eq!(decompressed.as_ref(), sample_array.as_ref());
1198    }
1199
1200    #[test_log::test(tokio::test)]
1201    async fn test_fsl_nullable_items() {
1202        let datagen = Box::new(FnArrayGeneratorProvider::new(move || {
1203            lance_datagen::array::rand_vec_nullable::<UInt32Type>(Dimension::from(128), 0.5)
1204        }));
1205
1206        let field = Field::new(
1207            "",
1208            DataType::FixedSizeList(Arc::new(Field::new("item", DataType::UInt32, true)), 128),
1209            false,
1210        );
1211        check_round_trip_encoding_generated(field, datagen, TestCases::default()).await;
1212    }
1213
1214    #[test_log::test(tokio::test)]
1215    async fn test_value_encoding_verification() {
1216        use std::collections::HashMap;
1217
1218        let test_cases = TestCases::default()
1219            .with_expected_encoding("flat")
1220            .with_min_file_version(LanceFileVersion::V2_1);
1221
1222        // Test both explicit configuration and automatic fallback scenarios
1223        // 1. Test explicit "none" compression to force flat encoding
1224        // Also explicitly disable BSS to ensure value encoding is tested
1225        let mut metadata_explicit = HashMap::new();
1226        metadata_explicit.insert("lance-encoding:compression".to_string(), "none".to_string());
1227        metadata_explicit.insert("lance-encoding:bss".to_string(), "off".to_string());
1228
1229        let arr_explicit =
1230            Arc::new(Int32Array::from((0..1000).collect::<Vec<i32>>())) as Arc<dyn Array>;
1231        check_round_trip_encoding_of_data(vec![arr_explicit], &test_cases, metadata_explicit).await;
1232
1233        // 2. Test automatic fallback to flat encoding when bitpacking conditions aren't met
1234        // Use unique values to avoid RLE encoding
1235        // Explicitly disable BSS to ensure value encoding is tested
1236        let mut metadata = HashMap::new();
1237        metadata.insert("lance-encoding:bss".to_string(), "off".to_string());
1238
1239        let arr_fallback = Arc::new(Int32Array::from(
1240            (0..100).map(|i| i * 73 + 19).collect::<Vec<i32>>(),
1241        )) as Arc<dyn Array>;
1242        check_round_trip_encoding_of_data(vec![arr_fallback], &test_cases, metadata).await;
1243    }
1244
1245    #[test_log::test(tokio::test)]
1246    async fn test_mixed_page_validity() {
1247        let no_nulls = Arc::new(Int32Array::from_iter_values([1, 2]));
1248        let has_nulls = Arc::new(Int32Array::from_iter([Some(3), None, Some(5)]));
1249
1250        let test_cases = TestCases::default().with_page_sizes(vec![1]);
1251        check_round_trip_encoding_of_data(vec![no_nulls, has_nulls], &test_cases, HashMap::new())
1252            .await;
1253    }
1254}