Skip to main content

lance_encoding/encodings/logical/primitive/
miniblock.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Routines for encoding and decoding miniblock data
5//!
6//! Miniblock encoding is one of the two structural encodings in Lance 2.1.
7//! In this approach the data is compressed into a series of chunks put into
8//! a single buffer.
9//!
10//! A chunk must be encoded or decoded as a unit.  There is a small amount of
11//! chunk metadata such as the number and size of each buffer in the chunk.
12//!
13//! Any form of compression can be used since we are compressing and decompressing
14//! entire chunks.
15use crate::{buffer::LanceBuffer, data::DataBlock, format::metadata::CompressiveEncoding};
16
17use lance_core::Result;
18
19pub const MAX_MINIBLOCK_BYTES: u64 = 8 * 1024 - 6;
20
21const DEFAULT_MAX_MINIBLOCK_VALUES: u64 = 4096;
22/// Maximum number of values that any mini-block decoder accepts from page metadata.
23pub(crate) const MAX_CONFIGURABLE_MINIBLOCK_VALUES: u64 = 32768;
24
25fn parse_max_miniblock_values() -> u64 {
26    let val = std::env::var("LANCE_MINIBLOCK_MAX_VALUES")
27        .ok()
28        .and_then(|v| v.parse().ok())
29        .unwrap_or(DEFAULT_MAX_MINIBLOCK_VALUES);
30    val.clamp(1, MAX_CONFIGURABLE_MINIBLOCK_VALUES)
31}
32
33pub static MAX_MINIBLOCK_VALUES: std::sync::LazyLock<u64> =
34    std::sync::LazyLock::new(parse_max_miniblock_values);
35
36/// Maximum number of rep/def levels the structural planner should place into
37/// a single mini-block chunk.
38pub fn max_repdef_levels_per_chunk(bits_per_level: u64) -> u64 {
39    debug_assert!(bits_per_level > 0);
40    const REPDEF_BUDGET_BITS: u64 = 16 * 1024 * 8;
41    let budgeted_levels = REPDEF_BUDGET_BITS / bits_per_level;
42    budgeted_levels.min(u16::MAX as u64)
43}
44
45/// Page data that has been compressed into a series of chunks put into
46/// a single buffer.
47#[derive(Debug)]
48pub struct MiniBlockCompressed {
49    /// The buffers of compressed data
50    pub data: Vec<LanceBuffer>,
51    /// Describes the size of each chunk
52    pub chunks: Vec<MiniBlockChunk>,
53    /// The number of values in the entire page
54    pub num_values: u64,
55}
56
57/// Per-page framing details that can affect a mini-block compressor's choice.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub struct MiniBlockCompressionContext {
60    common_chunk_buffers: u64,
61    support_large_chunk: bool,
62    allow_generic_offsets: bool,
63}
64
65impl MiniBlockCompressionContext {
66    /// Creates the framing context supplied by the owning mini-block page.
67    pub fn new(
68        common_chunk_buffers: u64,
69        support_large_chunk: bool,
70        allow_generic_offsets: bool,
71    ) -> Self {
72        Self {
73            common_chunk_buffers,
74            support_large_chunk,
75            allow_generic_offsets,
76        }
77    }
78
79    pub(crate) fn chunk_header_bytes(self, value_buffers: u64) -> u64 {
80        let value_buffer_size_bytes = if self.support_large_chunk { 4 } else { 2 };
81        2_u64
82            .saturating_add(self.common_chunk_buffers.saturating_mul(2))
83            .saturating_add(value_buffers.saturating_mul(value_buffer_size_bytes))
84            .next_multiple_of(8)
85    }
86
87    pub(crate) fn allows_generic_offsets(self) -> bool {
88        self.allow_generic_offsets
89    }
90}
91
92/// Describes the size of a mini-block chunk of data
93///
94/// Mini-block chunks are designed to be small (just a few disk sectors)
95/// and contain a power-of-two number of values (except for the last chunk)
96///
97/// By default we limit a chunk to 4Ki values and slightly less than
98/// 8KiB of compressed value data.  The byte budget remains the primary
99/// constraint, so only encodings that compress many values into that
100/// budget can use larger value counts when explicitly configured.
101///
102/// The maximum number of values per chunk can be configured via the
103/// `LANCE_MINIBLOCK_MAX_VALUES` environment variable.  This is only
104/// useful in extremely bandwidth-limited environments; the default is
105/// appropriate for local disks and same-region cloud object storage.
106#[derive(Debug)]
107pub struct MiniBlockChunk {
108    // The size in bytes of each buffer in the chunk.
109    //
110    // In Lance 2.1, the chunk size is limited to 32KiB, so only 16-bits are used.
111    // Since Lance 2.2, the chunk size uses u32 to support larger chunk size
112    pub buffer_sizes: Vec<u32>,
113    // The log (base 2) of the number of values in the chunk.  If this is the final chunk
114    // then this should be 0 (the number of values will be calculated by subtracting the
115    // size of all other chunks from the total size of the page)
116    //
117    // For example, 1 would mean there are 2 values in the chunk and 15 would mean there
118    // are 32Ki values in the chunk.
119    //
120    // This must be <= log2(MAX_MINIBLOCK_VALUES) (i.e. <= 12 at the default of 4096)
121    pub log_num_values: u8,
122}
123
124impl MiniBlockChunk {
125    /// Gets the number of values in this block
126    ///
127    /// This requires `vals_in_prev_blocks` and `total_num_values` because the
128    /// last block in a page is a special case which stores 0 for log_num_values
129    /// and, in that case, the number of values is determined by subtracting
130    /// `vals_in_prev_blocks` from `total_num_values`
131    pub fn num_values(&self, vals_in_prev_blocks: u64, total_num_values: u64) -> u64 {
132        if self.log_num_values == 0 {
133            total_num_values - vals_in_prev_blocks
134        } else {
135            1 << self.log_num_values
136        }
137    }
138}
139
140/// Trait for compression algorithms that are suitable for use in the miniblock structural encoding
141///
142/// These compression algorithms should be capable of encoding the data into small chunks
143/// where each chunk (except the last) has 2^N values (N can vary between chunks)
144pub trait MiniBlockCompressor: std::fmt::Debug + Send + Sync {
145    /// Compress a `page` of data into multiple chunks
146    ///
147    /// See [`MiniBlockCompressed`] for details on how chunks should be sized.
148    ///
149    /// This method also returns a description of the encoding applied that will be
150    /// used at decode time to read the data.
151    fn compress(
152        &self,
153        context: MiniBlockCompressionContext,
154        page: DataBlock,
155    ) -> Result<(MiniBlockCompressed, CompressiveEncoding)>;
156}
157
158#[cfg(test)]
159mod tests {
160    use serial_test::serial;
161
162    use super::*;
163
164    #[test]
165    #[serial]
166    fn test_parse_default() {
167        unsafe { std::env::remove_var("LANCE_MINIBLOCK_MAX_VALUES") };
168        assert_eq!(parse_max_miniblock_values(), 4096);
169    }
170
171    #[test]
172    #[serial]
173    fn test_parse_custom_value() {
174        unsafe { std::env::set_var("LANCE_MINIBLOCK_MAX_VALUES", "256") };
175        assert_eq!(parse_max_miniblock_values(), 256);
176        unsafe { std::env::remove_var("LANCE_MINIBLOCK_MAX_VALUES") };
177    }
178
179    #[test]
180    #[serial]
181    fn test_parse_can_raise_to_32k() {
182        unsafe { std::env::set_var("LANCE_MINIBLOCK_MAX_VALUES", "32768") };
183        assert_eq!(parse_max_miniblock_values(), 32768);
184        unsafe { std::env::remove_var("LANCE_MINIBLOCK_MAX_VALUES") };
185    }
186
187    #[test]
188    #[serial]
189    fn test_parse_clamps_zero_to_one() {
190        unsafe { std::env::set_var("LANCE_MINIBLOCK_MAX_VALUES", "0") };
191        assert_eq!(parse_max_miniblock_values(), 1);
192        unsafe { std::env::remove_var("LANCE_MINIBLOCK_MAX_VALUES") };
193    }
194
195    #[test]
196    #[serial]
197    fn test_parse_clamps_above_max() {
198        unsafe { std::env::set_var("LANCE_MINIBLOCK_MAX_VALUES", "99999") };
199        assert_eq!(
200            parse_max_miniblock_values(),
201            MAX_CONFIGURABLE_MINIBLOCK_VALUES
202        );
203        unsafe { std::env::remove_var("LANCE_MINIBLOCK_MAX_VALUES") };
204    }
205
206    #[test]
207    #[serial]
208    fn test_parse_invalid_falls_back_to_default() {
209        unsafe { std::env::set_var("LANCE_MINIBLOCK_MAX_VALUES", "not_a_number") };
210        assert_eq!(parse_max_miniblock_values(), DEFAULT_MAX_MINIBLOCK_VALUES);
211        unsafe { std::env::remove_var("LANCE_MINIBLOCK_MAX_VALUES") };
212    }
213}