// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright The Lance Authors //! The top-level encoding module for Lance files. //! //! Lance files are encoded using a [`FieldEncodingStrategy`] which choose //! what encoder to use for each field. //! //! Structural strategies build a tree of encoders for each field from the //! version-free builders in [`structural`]. Struct and list encoders collect //! validity and offsets; primitive leaf encoders accumulate values and emit //! miniblock or full-zip pages. use std::{collections::HashMap, sync::Arc}; use arrow_array::{Array, ArrayRef, RecordBatch}; use bytes::{Bytes, BytesMut}; use futures::future::BoxFuture; use lance_core::datatypes::{Field, Schema}; use lance_core::utils::bit::{is_pwr_two, pad_bytes_to}; use lance_core::{Error, Result}; use crate::buffer::LanceBuffer; use crate::data::DataBlock; use crate::decoder::PageEncoding; use crate::repdef::RepDefBuilder; use crate::{ decoder::{ColumnInfo, PageInfo}, format::pb, }; pub use crate::array_encoding::ArrayFieldEncodingStrategy; pub mod structural; /// The minimum alignment for a page buffer. Writers must respect this. pub const MIN_PAGE_BUFFER_ALIGNMENT: u64 = 8; /// An array encoded with the `pb::ArrayEncoding` grammar. #[derive(Debug)] pub struct EncodedArray { pub data: DataBlock, pub encoding: pb::ArrayEncoding, } impl EncodedArray { pub fn new(data: DataBlock, encoding: pb::ArrayEncoding) -> Self { Self { data, encoding } } pub fn into_buffers(self) -> (Vec, pb::ArrayEncoding) { (self.data.into_buffers(), self.encoding) } } /// Encodes one data block and describes it with `pb::ArrayEncoding`. pub trait ArrayEncoder: std::fmt::Debug + Send + Sync { fn encode( &self, data: DataBlock, data_type: &arrow_schema::DataType, buffer_index: &mut u32, ) -> Result; } /// Selects an `ArrayEncoder` for one page. pub trait ArrayEncodingStrategy: Send + Sync + std::fmt::Debug { fn create_array_encoder( &self, arrays: &[ArrayRef], field: &Field, ) -> Result>; } /// An encoded page of data /// /// Maps to a top-level array /// /// For example, `FixedSizeList` will have two EncodedArray instances and one EncodedPage #[derive(Debug)] pub struct EncodedPage { // The encoded page buffers pub data: Vec, // A description of the encoding used to encode the page pub description: PageEncoding, /// The number of rows in the encoded page pub num_rows: u64, /// The top-level row number of the first row in the page /// /// Generally the number of "top-level" rows and the number of rows are the same. However, /// when there is repetition (list/fixed-size-list) there will be more or less items than rows. /// /// A top-level row can never be split across a page boundary. pub row_number: u64, /// The index of the column pub column_idx: u32, } pub struct EncodedColumn { pub column_buffers: Vec, pub encoding: pb::ColumnEncoding, pub final_pages: Vec, } impl Default for EncodedColumn { fn default() -> Self { Self { column_buffers: Default::default(), encoding: pb::ColumnEncoding { column_encoding: Some(pb::column_encoding::ColumnEncoding::Values(())), }, final_pages: Default::default(), } } } /// A tool to reserve space for buffers that are not in-line with the data /// /// In most cases, buffers are stored in the page and referred to in the encoding /// metadata by their index in the page. This keeps all buffers within a page together. /// As a result, most encoders should not need to use this structure. /// /// In some cases (currently only the large binary encoding) there is a need to access /// buffers that are not in the page (because storing the position / offset of every page /// in the page metadata would be too expensive). /// /// To do this you can add a buffer with `add_buffer` and then use the returned position /// in some way (in the large binary encoding the returned position is stored in the page /// data as a position / size array). pub struct OutOfLineBuffers { position: u64, buffer_alignment: u64, buffers: Vec, } impl OutOfLineBuffers { pub fn new(base_position: u64, buffer_alignment: u64) -> Self { Self { position: base_position, buffer_alignment, buffers: Vec::new(), } } pub fn add_buffer(&mut self, buffer: LanceBuffer) -> u64 { let position = self.position; self.position += buffer.len() as u64; self.position += pad_bytes_to(buffer.len(), self.buffer_alignment as usize) as u64; self.buffers.push(buffer); position } pub fn take_buffers(self) -> Vec { self.buffers } pub fn reset_position(&mut self, position: u64) { self.position = position; } } /// A task to create a page of data pub type EncodeTask = BoxFuture<'static, Result>; /// Top level encoding trait to code any Arrow array type into one or more pages. /// /// The field encoder implements buffering and encoding of a single input column /// but it may map to multiple output columns. For example, a list array or struct /// array will be encoded into multiple columns. /// /// Also, fields may be encoded at different speeds. For example, given a struct /// column with three fields (a boolean field, an int32 field, and a 4096-dimension /// tensor field) the tensor field is likely to emit encoded pages much more frequently /// than the boolean field. pub trait FieldEncoder: Send { /// Validate and prepare an array before any encoder state is mutated. /// /// Batch-oriented callers invoke this for every root field before calling /// [`Self::maybe_encode`], so a later validation failure cannot leave an /// earlier field encoder partially advanced. fn prepare_array(&mut self, array: ArrayRef) -> Result { Ok(array) } /// Buffer the data and, if there is enough data in the buffer to form a page, return /// an encoding task to encode the data. /// /// This may return more than one task because a single column may be mapped to multiple /// output columns. For example, if encoding a struct column with three children then /// up to three tasks may be returned from each call to maybe_encode. /// /// It may also return multiple tasks for a single column if the input array is larger /// than a single disk page. /// /// It could also return an empty Vec if there is not enough data yet to encode any pages. /// /// The `row_number` must be passed which is the top-level row number currently being encoded /// This is stored in any pages produced by this call so that we can know the priority of the /// page. /// /// The `num_rows` is the number of top level rows. It is initially the same as `array.len()` /// however it is passed seprately because array will become flattened over time (if there is /// repetition) and we need to know the original number of rows for various purposes. fn maybe_encode( &mut self, array: ArrayRef, external_buffers: &mut OutOfLineBuffers, repdef: RepDefBuilder, row_number: u64, num_rows: u64, ) -> Result>; /// Flush any remaining data from the buffers into encoding tasks /// /// Each encode task produces a single page. The order of these pages will be maintained /// in the file (we do not worry about order between columns but all pages in the same /// column should maintain order) /// /// This may be called intermittently throughout encoding but will always be called /// once at the end of encoding just before calling finish fn flush(&mut self, external_buffers: &mut OutOfLineBuffers) -> Result>; /// Finish encoding and return column metadata /// /// This is called only once, after all encode tasks have completed /// /// This returns a Vec because a single field may have created multiple columns fn finish( &mut self, external_buffers: &mut OutOfLineBuffers, ) -> BoxFuture<'_, Result>>; /// The number of output columns this encoding will create fn num_columns(&self) -> u32; } /// Keeps track of the current column index and makes a mapping /// from field id to column index #[derive(Debug, Default)] pub struct ColumnIndexSequence { current_index: u32, mapping: Vec<(u32, u32)>, } impl ColumnIndexSequence { pub fn next_column_index(&mut self, field_id: u32) -> u32 { let idx = self.current_index; self.current_index += 1; self.mapping.push((field_id, idx)); idx } pub fn skip(&mut self) { self.current_index += 1; } } /// Options that control the encoding process pub struct EncodingOptions { /// How much data (in bytes) to cache in-memory before writing a page /// /// This cache is applied on a per-column basis pub cache_bytes_per_column: u64, /// The maximum size of a page in bytes, if a single array would create /// a page larger than this then it will be split into multiple pages pub max_page_bytes: u64, /// If false (the default) then arrays will be copied (deeply) before /// being cached. This ensures any data kept alive by the array can /// be discarded safely and helps avoid writer accumulation. However, /// there is an associated cost. pub keep_original_array: bool, /// The alignment that the writer is applying to buffers /// /// The encoder needs to know this so it figures the position of out-of-line /// buffers correctly pub buffer_alignment: u64, } impl Default for EncodingOptions { fn default() -> Self { Self { cache_bytes_per_column: 8 * 1024 * 1024, max_page_bytes: 32 * 1024 * 1024, keep_original_array: true, buffer_alignment: 64, } } } /// A trait to pick which kind of field encoding to use for a field /// /// Unlike the ArrayEncodingStrategy, the field encoding strategy is /// chosen before any data is generated and the same field encoder is /// used for all data in the field. pub trait FieldEncodingStrategy: Send + Sync + std::fmt::Debug { /// Validate one top-level array before any field encoder state is mutated. fn validate_array(&self, _array: &dyn Array, _field: &Field) -> Result<()> { Ok(()) } /// Choose and create an appropriate field encoder for the given /// field. /// /// The field encoder can be chosen on the data type as well as /// any metadata that is attached to the field. /// fn create_field_encoder( &self, field: &Field, column_index: &mut ColumnIndexSequence, context: &FieldEncodingContext<'_>, ) -> Result>; } /// Context shared while one top-level field and all of its children are mapped /// to concrete field encoders. pub struct FieldEncodingContext<'a> { /// The complete strategy composition used for recursive child fields. pub strategy: &'a dyn FieldEncodingStrategy, /// Runtime-only writer options. pub options: &'a EncodingOptions, /// Metadata inherited from the top-level field. pub root_field_metadata: &'a HashMap, } /// A batch encoder that encodes RecordBatch objects by delegating /// to field encoders for each top-level field in the batch. pub struct BatchEncoder { pub field_encoders: Vec>, pub field_id_to_column_index: Vec<(u32, u32)>, } impl BatchEncoder { pub fn try_new( schema: &Schema, strategy: &dyn FieldEncodingStrategy, options: &EncodingOptions, ) -> Result { let mut col_idx = 0; let mut col_idx_sequence = ColumnIndexSequence::default(); let field_encoders = schema .fields .iter() .map(|field| { let context = FieldEncodingContext { strategy, options, root_field_metadata: &field.metadata, }; let encoder = strategy.create_field_encoder(field, &mut col_idx_sequence, &context)?; col_idx += encoder.as_ref().num_columns(); Ok(encoder) }) .collect::>>()?; Ok(Self { field_encoders, field_id_to_column_index: col_idx_sequence.mapping, }) } pub fn num_columns(&self) -> u32 { self.field_encoders .iter() .map(|field_encoder| field_encoder.num_columns()) .sum::() } } /// An encoded batch of data and a page table describing it /// /// This is returned by [`crate::encoder::encode_batch`] #[derive(Debug)] pub struct EncodedBatch { pub data: Bytes, pub page_table: Vec>, pub schema: Arc, pub top_level_columns: Vec, pub num_rows: u64, } fn write_page_to_data_buffer(page: EncodedPage, data_buffer: &mut BytesMut) -> PageInfo { let buffers = page.data; let mut buffer_offsets_and_sizes = Vec::with_capacity(buffers.len()); for buffer in buffers { let buffer_offset = data_buffer.len() as u64; data_buffer.extend_from_slice(&buffer); let size = data_buffer.len() as u64 - buffer_offset; buffer_offsets_and_sizes.push((buffer_offset, size)); } PageInfo { buffer_offsets_and_sizes: Arc::from(buffer_offsets_and_sizes), encoding: page.description, num_rows: page.num_rows, priority: page.row_number, } } /// Helper method to encode a batch of data into memory /// /// This is primarily for testing and benchmarking but could be useful in other /// niche situations like IPC. pub async fn encode_batch( batch: &RecordBatch, schema: Arc, encoding_strategy: &dyn FieldEncodingStrategy, options: &EncodingOptions, ) -> Result { if !is_pwr_two(options.buffer_alignment) || options.buffer_alignment < MIN_PAGE_BUFFER_ALIGNMENT { return Err(Error::invalid_input_source( format!( "buffer_alignment must be a power of two and at least {}", MIN_PAGE_BUFFER_ALIGNMENT ) .into(), )); } let mut data_buffer = BytesMut::new(); let lance_schema = Schema::try_from(batch.schema().as_ref())?; let options = EncodingOptions { keep_original_array: true, ..*options }; let mut batch_encoder = BatchEncoder::try_new(&lance_schema, encoding_strategy, &options)?; let arrays = batch .columns() .iter() .cloned() .zip(batch_encoder.field_encoders.iter_mut()) .map(|(array, encoder)| encoder.prepare_array(array)) .collect::>>()?; let mut page_table = Vec::new(); let mut col_idx_offset = 0; for (arr, mut encoder) in arrays.into_iter().zip(batch_encoder.field_encoders) { let mut external_buffers = OutOfLineBuffers::new(data_buffer.len() as u64, options.buffer_alignment); let repdef = RepDefBuilder::default(); let encoder = encoder.as_mut(); let num_rows = arr.len() as u64; let mut tasks = encoder.maybe_encode(arr, &mut external_buffers, repdef, 0, num_rows)?; tasks.extend(encoder.flush(&mut external_buffers)?); for buffer in external_buffers.take_buffers() { data_buffer.extend_from_slice(&buffer); } let mut pages = HashMap::>::new(); for task in tasks { let encoded_page = task.await?; // Write external buffers first pages .entry(encoded_page.column_idx) .or_default() .push(write_page_to_data_buffer(encoded_page, &mut data_buffer)); } let mut external_buffers = OutOfLineBuffers::new(data_buffer.len() as u64, options.buffer_alignment); let encoded_columns = encoder.finish(&mut external_buffers).await?; for buffer in external_buffers.take_buffers() { data_buffer.extend_from_slice(&buffer); } let num_columns = encoded_columns.len(); for (col_idx, encoded_column) in encoded_columns.into_iter().enumerate() { let col_idx = col_idx + col_idx_offset; let mut col_buffer_offsets_and_sizes = Vec::new(); for buffer in encoded_column.column_buffers { let buffer_offset = data_buffer.len() as u64; data_buffer.extend_from_slice(&buffer); let size = data_buffer.len() as u64 - buffer_offset; col_buffer_offsets_and_sizes.push((buffer_offset, size)); } for page in encoded_column.final_pages { pages .entry(page.column_idx) .or_default() .push(write_page_to_data_buffer(page, &mut data_buffer)); } let col_pages = std::mem::take(pages.entry(col_idx as u32).or_default()); page_table.push(Arc::new(ColumnInfo { index: col_idx as u32, buffer_offsets_and_sizes: Arc::from( col_buffer_offsets_and_sizes.into_boxed_slice(), ), page_infos: Arc::from(col_pages.into_boxed_slice()), encoding: encoded_column.encoding, })) } col_idx_offset += num_columns; } let top_level_columns = batch_encoder .field_id_to_column_index .iter() .map(|(_, idx)| *idx) .collect(); Ok(EncodedBatch { data: data_buffer.freeze(), top_level_columns, page_table, schema, num_rows: batch.num_rows() as u64, }) } #[cfg(test)] mod tests { use super::*; use crate::testing::{TestEncoding, create_test_field_encoder, test_encoding_strategy}; use arrow_array::make_array; use arrow_buffer::Buffer; use arrow_data::ArrayData; use arrow_schema::{DataType as ArrowDataType, Field as ArrowField, Fields as ArrowFields}; use rstest::rstest; #[rstest] fn test_nested_variable_width_offsets_are_validated_before_dispatch( #[values(TestEncoding::Array, TestEncoding::StructuralU32)] encoding: TestEncoding, #[values(ArrowDataType::Utf8, ArrowDataType::LargeUtf8)] item_type: ArrowDataType, ) { let offsets = match &item_type { ArrowDataType::Utf8 => Buffer::from_slice_ref([0_i32, 2, 1, 3]), ArrowDataType::LargeUtf8 => Buffer::from_slice_ref([0_i64, 2, 1, 3]), _ => unreachable!(), }; let child_data = unsafe { ArrayData::builder(item_type.clone()) .len(3) .add_buffer(offsets) .add_buffer(Buffer::from(b"abc")) .build_unchecked() }; let item_field = Arc::new(ArrowField::new("item", item_type, false)); let data_type = ArrowDataType::FixedSizeList(item_field, 1); let array_data = unsafe { ArrayData::builder(data_type.clone()) .len(3) .add_child_data(child_data) .build_unchecked() }; let array = make_array(array_data); let field = Field::try_from(&ArrowField::new("payload", data_type, false)).unwrap(); let strategy = test_encoding_strategy(encoding); let mut column_index = ColumnIndexSequence::default(); let options = EncodingOptions { cache_bytes_per_column: 0, ..Default::default() }; let mut encoder = create_test_field_encoder(strategy.as_ref(), &field, &mut column_index, &options) .unwrap(); let mut external_buffers = OutOfLineBuffers::new(0, MIN_PAGE_BUFFER_ALIGNMENT); let error = encoder .maybe_encode(array, &mut external_buffers, RepDefBuilder::default(), 0, 3) .err() .expect("malformed nested offsets should fail before task dispatch"); assert!(matches!(error, Error::InvalidInput { .. })); let message = error.to_string(); assert!( message.contains("field 'payload'"), "unexpected message: {message}" ); assert!( message.contains("non-monotonic offset at position 2"), "unexpected message: {message}" ); } #[test] fn test_fixed_size_list_struct_requires_v2_2() { let list_item = ArrowField::new( "item", ArrowDataType::Struct(ArrowFields::from(vec![ArrowField::new( "x", ArrowDataType::Int32, true, )])), true, ); let arrow_field = ArrowField::new( "list_struct", ArrowDataType::FixedSizeList(Arc::new(list_item), 2), true, ); let field = Field::try_from(&arrow_field).unwrap(); let strategy = test_encoding_strategy(TestEncoding::StructuralU16); let mut column_index = ColumnIndexSequence::default(); let options = EncodingOptions::default(); let result = create_test_field_encoder(strategy.as_ref(), &field, &mut column_index, &options); assert!( result.is_err(), "FixedSizeList should be rejected for file version 2.1" ); let err = result.err().unwrap(); assert!( err.to_string() .contains("FixedSizeList is not enabled by the selected file format") ); } }