Skip to main content

lance_encoding/encodings/logical/
primitive.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::{any::Any, collections::VecDeque, fmt::Debug, ops::Range, sync::Arc, vec};
5
6use crate::format::metadata::{self, CompressiveEncoding};
7use arrow_buffer::BooleanBufferBuilder;
8use arrow_schema::DataType;
9use bytes::Bytes;
10use futures::{FutureExt, future::BoxFuture};
11use lance_core::{
12    cache::{CacheKey, CacheKeySchema, Context, DeepSizeOf, KeyBuilder, LanceCache},
13    error::Error,
14};
15
16use crate::{compression::DecompressionStrategy, data::DataBlock};
17use lance_core::Result;
18
19use crate::{
20    EncodingsIo,
21    buffer::LanceBuffer,
22    data::DataBlockBuilder,
23    decoder::{DecodePageTask, DecodedPage, StructuralPageDecoder},
24    repdef::RepDefUnraveler,
25};
26
27// Reached only by `sparse.rs` tests through `use super::*`.
28#[cfg(test)]
29use crate::format::EncodingUtils;
30
31pub mod blob;
32mod chunk_index;
33pub mod constant;
34pub mod dict;
35pub mod fullzip;
36pub mod miniblock;
37mod planar;
38pub(crate) mod sparse;
39
40mod all_null;
41mod field;
42mod fullzip_decode;
43mod miniblock_decode;
44mod structural_encoder;
45
46pub(crate) use all_null::LevelCodec;
47pub use all_null::{
48    ComplexAllNullPageDecoder, ComplexAllNullScheduler, DecodeComplexAllNullTask,
49    SimpleAllNullPageDecoder, SimpleAllNullScheduler,
50};
51pub use field::{
52    StructuralCompositeDecodeArrayTask, StructuralPrimitiveFieldDecoder,
53    StructuralPrimitiveFieldScheduler,
54};
55pub use fullzip_decode::FullZipScheduler;
56pub use miniblock_decode::MiniBlockScheduler;
57pub use structural_encoder::{PrimitivePageEncoding, PrimitiveStructuralEncoder};
58
59const FILL_BYTE: u8 = 0xFE;
60const DEFAULT_DICT_DIVISOR: u64 = 2;
61const DEFAULT_DICT_MAX_CARDINALITY: u64 = 100_000;
62const DEFAULT_DICT_SIZE_RATIO: f64 = 0.8;
63/// Default for `DICT_MAX_SIZE_META_KEY`.
64const DEFAULT_DICT_MAX_SIZE_BYTES: usize = 8 * 1024 * 1024;
65const DEFAULT_DICT_VALUES_COMPRESSION: &str = "lz4";
66
67/// Largest level count a direct legacy u16-value/U8-run RLE frame can prove.
68///
69/// Mini-block level payload sizes are u16. After the eight-byte frame header, each run needs a
70/// two-byte value and a one-byte length, and each U8 run can represent at most 255 levels.
71const MAX_LEGACY_RLE_LEVELS: u64 = ((u16::MAX as u64 - std::mem::size_of::<u64>() as u64)
72    / (std::mem::size_of::<u16>() as u64 + std::mem::size_of::<u8>() as u64))
73    * u8::MAX as u64;
74
75struct PageLoadTask {
76    decoder_fut: BoxFuture<'static, Result<Box<dyn StructuralPageDecoder>>>,
77    num_rows: u64,
78}
79
80/// The ordered initialization-buffer slots for one page.
81///
82/// Optional slots preserve the semantic position of absent buffers so range
83/// declaration and buffer consumption share one representation.
84#[derive(Debug)]
85struct PageInitialization {
86    buffer_ranges: Vec<Option<Range<u64>>>,
87}
88
89impl PageInitialization {
90    fn new(buffer_ranges: impl IntoIterator<Item = Option<Range<u64>>>) -> Self {
91        Self {
92            buffer_ranges: buffer_ranges.into_iter().collect(),
93        }
94    }
95
96    fn required_ranges(&self) -> impl Iterator<Item = &Range<u64>> {
97        self.buffer_ranges.iter().flatten()
98    }
99
100    fn num_required_ranges(&self) -> usize {
101        self.required_ranges().count()
102    }
103
104    fn with_buffers(self, buffers: Vec<Bytes>) -> Result<PageInitializationBuffers> {
105        let expected = self.num_required_ranges();
106        if buffers.len() != expected {
107            return Err(Error::invalid_input_source(
108                format!(
109                    "Page initialization received {} buffers for {expected} declared ranges",
110                    buffers.len()
111                )
112                .into(),
113            ));
114        }
115
116        let mut buffers = buffers.into_iter();
117        let mut buffer_slots = Vec::with_capacity(self.buffer_ranges.len());
118        for range in self.buffer_ranges {
119            if range.is_some() {
120                let buffer = buffers.next().ok_or_else(|| {
121                    Error::internal("Page initialization buffer was not assigned")
122                })?;
123                buffer_slots.push(Some(buffer));
124            } else {
125                buffer_slots.push(None);
126            }
127        }
128        debug_assert!(
129            buffers.next().is_none(),
130            "validated page initialization buffer count must be exhausted"
131        );
132        Ok(PageInitializationBuffers { buffer_slots })
133    }
134}
135
136#[derive(Debug)]
137struct PageInitializationBuffers {
138    buffer_slots: Vec<Option<Bytes>>,
139}
140
141impl PageInitializationBuffers {
142    fn try_into_array<const N: usize>(self, layout: &str) -> Result<[Option<Bytes>; N]> {
143        let actual = self.buffer_slots.len();
144        self.buffer_slots.try_into().map_err(|_| {
145            Error::internal(format!(
146                "{layout} page initialization declared {actual} buffer slots, expected {N}"
147            ))
148        })
149    }
150
151    fn into_required(self, layout: &str) -> Result<Vec<Bytes>> {
152        self.buffer_slots
153            .into_iter()
154            .enumerate()
155            .map(|(index, buffer)| {
156                buffer.ok_or_else(|| {
157                    Error::internal(format!(
158                        "{layout} page initialization buffer slot {index} is absent"
159                    ))
160                })
161            })
162            .collect()
163    }
164}
165
166/// A trait for figuring out how to schedule the data within
167/// a single page.
168trait StructuralPageScheduler: std::fmt::Debug + Send {
169    /// Attach the reader's file-scoped cache for independently cached value data.
170    /// Legacy schedulers keep their existing initialization-only cache behavior.
171    fn set_cache(&mut self, _cache: Arc<LanceCache>) {}
172
173    /// Whether this page requires initialized metadata before scheduling.
174    fn needs_initialization(&self) -> bool {
175        true
176    }
177
178    /// Describes the file byte ranges this page must read to initialize its
179    /// metadata (chunk metadata, dictionary, repetition index, ...).
180    ///
181    /// The field scheduler sorts the combined I/O request by file offset and
182    /// restores buffers to the declared slots afterward.
183    fn init_layout(&self) -> Result<PageInitialization>;
184
185    /// Returns initialized state from the bytes declared by [`Self::init_layout`].
186    ///
187    /// The caller attaches the returned state with [`Self::try_load`].
188    fn init_from_buffers<'a>(
189        &'a mut self,
190        buffers: PageInitializationBuffers,
191        io: &Arc<dyn EncodingsIo>,
192    ) -> BoxFuture<'a, Result<Arc<dyn CachedPageData>>>;
193
194    /// Tries to attach metadata from a previous initialization.
195    fn try_load(&mut self, data: &Arc<dyn CachedPageData>) -> Result<()>;
196
197    // Identifies the requested decode shape (e.g. blob descriptor struct vs
198    // raw bytes). Blob columns can produce multiple page scheduler variants
199    // for the same physical column depending on the target field's data type,
200    // and the cached page state types differ per variant. The stable view is
201    // mixed into the cache key so different variants do not collide.
202    fn cache_view(&self) -> PageCacheView {
203        PageCacheView::Native
204    }
205
206    /// Schedules the read of the given ranges in the page
207    ///
208    /// The read may be split into multiple "shards" if the page is extremely large.
209    /// Each shard maps to one or more rows and can be decoded independently.
210    ///
211    /// Note: this sharding is for splitting up very large pages into smaller reads to
212    /// avoid buffering too much data in memory.  It is not related to the batch size or
213    /// compute units in any way.
214    fn schedule_ranges(
215        &self,
216        ranges: &[Range<u64>],
217        io: &Arc<dyn EncodingsIo>,
218    ) -> Result<Vec<PageLoadTask>>;
219}
220
221/// A chunk that has been loaded by the miniblock scheduler (but not
222/// yet decoded)
223#[derive(Debug)]
224struct LoadedChunk {
225    data: LanceBuffer,
226    items_in_chunk: u64,
227    byte_range: Range<u64>,
228    chunk_idx: usize,
229}
230
231impl Clone for LoadedChunk {
232    fn clone(&self) -> Self {
233        Self {
234            // Safe as we always create borrowed buffers here
235            data: self.data.clone(),
236            items_in_chunk: self.items_in_chunk,
237            byte_range: self.byte_range.clone(),
238            chunk_idx: self.chunk_idx,
239        }
240    }
241}
242
243pub trait CachedPageData: Any + Send + Sync + DeepSizeOf + 'static {
244    fn as_arc_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync + 'static>;
245}
246
247pub struct NoCachedPageData;
248
249impl DeepSizeOf for NoCachedPageData {
250    fn deep_size_of_children(&self, _ctx: &mut Context) -> usize {
251        0
252    }
253}
254impl CachedPageData for NoCachedPageData {
255    fn as_arc_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync + 'static> {
256        self
257    }
258}
259
260#[doc = "Deprecated compatibility wrapper; new code uses per-page PageDataCacheKey entries."]
261pub struct CachedFieldData {
262    pages: Vec<Arc<dyn CachedPageData>>,
263}
264
265impl DeepSizeOf for CachedFieldData {
266    fn deep_size_of_children(&self, ctx: &mut Context) -> usize {
267        self.pages.deep_size_of_children(ctx)
268    }
269}
270
271/// Compatibility key for the former whole-field page metadata cache.
272#[derive(Debug, Clone)]
273pub struct FieldDataCacheKey {
274    pub column_index: u32,
275    pub view_tag: String,
276}
277
278impl CacheKey for FieldDataCacheKey {
279    type ValueType = CachedFieldData;
280
281    fn key(&self) -> std::borrow::Cow<'_, str> {
282        format!("{}:{}", self.column_index, self.view_tag).into()
283    }
284
285    fn type_name() -> &'static str {
286        "FieldData"
287    }
288
289    fn schema() -> CacheKeySchema {
290        CacheKeySchema::new("lance.encoding.logical.primitive.field-data-key", 1)
291    }
292
293    fn write_key(&self, builder: &mut KeyBuilder) {
294        builder.write_u32(self.column_index);
295        builder.write_str(&self.view_tag);
296    }
297}
298
299/// Sized wrapper around a page's cached metadata so [`PageDataCacheKey`] can use
300/// the sized [`CacheKey`] path; the concrete per-layout state still lives behind
301/// `dyn CachedPageData`.
302pub struct CachedPage {
303    data: Arc<dyn CachedPageData>,
304}
305
306impl DeepSizeOf for CachedPage {
307    fn deep_size_of_children(&self, ctx: &mut Context) -> usize {
308        self.data.deep_size_of_children(ctx)
309    }
310}
311
312// Cache key for a single page's initialized metadata, keyed per page (not per
313// column) so a read populates and reuses only the pages it touches.
314//
315// `view` distinguishes decode shapes of one physical column: a blob column
316// materializes as a `Struct<position, size>` descriptor in one variant and raw
317// `LargeBinary` in another, each caching a different `CachedPageData` type.
318// Without it, two readers at the same column/page but different shapes collide
319// and crash on a downcast.
320/// Decode shape used to isolate cached page metadata for one physical column.
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
322#[non_exhaustive]
323pub enum PageCacheView {
324    /// The column's native non-blob representation.
325    Native,
326    /// Blob position and size descriptors materialized as a struct.
327    BlobDescriptor,
328    /// Blob contents materialized as binary values.
329    BlobPayload,
330}
331
332impl PageCacheView {
333    fn cache_tag(self) -> &'static str {
334        match self {
335            Self::Native => "n",
336            Self::BlobDescriptor => "bd",
337            Self::BlobPayload => "bp",
338        }
339    }
340}
341
342#[derive(Debug, Clone)]
343pub struct PageDataCacheKey {
344    pub column_index: u32,
345    pub page_index: u32,
346    pub view: PageCacheView,
347    pub target_data_type_tag: Arc<str>,
348}
349
350impl CacheKey for PageDataCacheKey {
351    type ValueType = CachedPage;
352
353    fn key(&self) -> std::borrow::Cow<'_, str> {
354        format!(
355            "v2/{}/{}/{}/{}",
356            self.column_index,
357            self.page_index,
358            self.view.cache_tag(),
359            self.target_data_type_tag
360        )
361        .into()
362    }
363
364    fn type_name() -> &'static str {
365        "PageData"
366    }
367
368    fn schema() -> CacheKeySchema {
369        CacheKeySchema::new("lance.encoding.logical.primitive.page-data-key", 2)
370    }
371
372    fn write_key(&self, builder: &mut KeyBuilder) {
373        builder.write_u32(self.column_index);
374        builder.write_u32(self.page_index);
375        builder.write_variant(match self.view {
376            PageCacheView::Native => 0,
377            PageCacheView::BlobDescriptor => 1,
378            PageCacheView::BlobPayload => 2,
379        });
380        builder.write_str(&self.target_data_type_tag);
381    }
382}
383
384// We align and pad mini-blocks to 8 byte boundaries for two reasons.  First,
385// to allow us to store a chunk size in 12 bits.
386//
387// If we directly record the size in bytes with 12 bits we would be limited to
388// 4KiB which is too small.  Since we know each mini-block consists of 8 byte
389// words we can store the # of words instead which gives us 32KiB.
390//
391// Second, each chunk in a mini-block is aligned to 8 bytes.  This allows multi-byte
392// values like offsets to be stored in a mini-block and safely read back out.  It also
393// helps ensure zero-copy reads in cases where zero-copy is possible (e.g. no decoding
394// needed).
395//
396// Note: by "aligned to 8 bytes" we mean BOTH "aligned to 8 bytes from the start of
397// the page" and "aligned to 8 bytes from the start of the file."
398const MINIBLOCK_ALIGNMENT: usize = 8;
399
400#[cfg(test)]
401#[allow(clippy::single_range_in_vec_init)]
402mod tests {
403    use std::sync::Arc;
404
405    use crate::buffer::LanceBuffer;
406    use crate::compression::BlockCompressor;
407    use crate::data::{BlockInfo, DataBlock, FixedWidthDataBlock};
408    use crate::encodings::physical::rle::{RleEncoder, RunLengthWidth};
409
410    use super::PageInitialization;
411
412    #[test]
413    fn page_initialization_preserves_optional_buffer_slots() {
414        let initialization = PageInitialization::new([None, Some(20..22), None, Some(10..11)]);
415        assert_eq!(
416            initialization
417                .required_ranges()
418                .cloned()
419                .collect::<Vec<_>>(),
420            [20..22, 10..11]
421        );
422
423        let buffers = initialization
424            .with_buffers(vec![
425                bytes::Bytes::from_static(b"ab"),
426                bytes::Bytes::from_static(b"c"),
427            ])
428            .unwrap();
429        let [first, second, third, fourth] = buffers.try_into_array("test").unwrap();
430        assert!(first.is_none());
431        assert_eq!(second.unwrap(), bytes::Bytes::from_static(b"ab"));
432        assert!(third.is_none());
433        assert_eq!(fourth.unwrap(), bytes::Bytes::from_static(b"c"));
434
435        let error = PageInitialization::new([Some(0..1)])
436            .with_buffers(Vec::new())
437            .unwrap_err();
438        assert!(matches!(error, lance_core::Error::InvalidInput { .. }));
439        assert!(error.to_string().contains("1 declared ranges"));
440    }
441
442    pub(super) fn encoded_u16_frame(
443        levels: &[u16],
444        run_length_width: RunLengthWidth,
445    ) -> LanceBuffer {
446        let block = DataBlock::FixedWidth(FixedWidthDataBlock {
447            data: LanceBuffer::reinterpret_slice(Arc::from(levels)),
448            bits_per_value: 16,
449            num_values: levels.len() as u64,
450            block_info: BlockInfo::new(),
451        });
452        BlockCompressor::compress(&RleEncoder::with_run_length_width(run_length_width), block)
453            .unwrap()
454            .0
455            .unwrap()
456    }
457}