Skip to main content

lance_file/versions/v2_3/
writer.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::sync::Arc;
5
6use arrow_array::{ArrayRef, RecordBatch};
7use bytes::{BufMut, Bytes};
8use lance_core::{Result, datatypes::Schema};
9use lance_encoding::{
10    compression_config::CompressionParams,
11    encoder::{BatchEncoder, EncodedBatch},
12};
13use lance_io::{object_store::ObjectStore, traits::Writer as ObjectWriter};
14use object_store::path::Path;
15use tokio::io::AsyncWriteExt;
16
17use crate::{
18    format::{MAGIC, pbfile},
19    writer::{
20        FileWriteSummary, FileWriterOptions,
21        structural::{EncodedBatchBody, EncodingPipeline, StructuralFileSink, encode_batch_body},
22    },
23};
24
25use super::encoding_strategy;
26
27/// A writer for the Lance v2.3 file grammar.
28///
29/// The concrete writer owns the v2.3 encoding composition, finish ordering,
30/// and exact footer identity. Shared components only execute the structural
31/// encoding and I/O mechanisms selected here.
32pub struct Writer {
33    sink: StructuralFileSink,
34    encoding: EncodingPipeline,
35    compression: CompressionParams,
36}
37
38impl Writer {
39    /// Create a v2.3 writer with an explicit schema.
40    pub fn try_new(
41        object_writer: Box<dyn ObjectWriter>,
42        schema: Schema,
43        options: FileWriterOptions,
44    ) -> Result<Self> {
45        Self::try_new_with_compression(object_writer, schema, options, Default::default())
46    }
47
48    /// Create a v2.3 writer with explicit compression tuning.
49    pub fn try_new_with_compression(
50        object_writer: Box<dyn ObjectWriter>,
51        schema: Schema,
52        options: FileWriterOptions,
53        compression: CompressionParams,
54    ) -> Result<Self> {
55        let mut writer = Self::new_lazy_with_compression(object_writer, options, compression);
56        writer.initialize(schema)?;
57        Ok(writer)
58    }
59
60    /// Create a v2.3 writer whose schema is inferred from the first batch.
61    pub fn new_lazy(object_writer: Box<dyn ObjectWriter>, options: FileWriterOptions) -> Self {
62        Self::new_lazy_with_compression(object_writer, options, Default::default())
63    }
64
65    /// Create a lazy v2.3 writer with explicit compression tuning.
66    pub fn new_lazy_with_compression(
67        object_writer: Box<dyn ObjectWriter>,
68        options: FileWriterOptions,
69        compression: CompressionParams,
70    ) -> Self {
71        Self {
72            sink: StructuralFileSink::new(object_writer),
73            encoding: EncodingPipeline::new(options),
74            compression,
75        }
76    }
77
78    fn initialize(&mut self, schema: Schema) -> Result<()> {
79        let encoding_options = self.encoding.encoding_options(&schema);
80        schema.validate()?;
81        let strategy = encoding_strategy(self.compression.clone());
82        let encoder = BatchEncoder::try_new(&schema, strategy.as_ref(), &encoding_options)?;
83        self.encoding.initialize(schema, encoder, &mut self.sink);
84        Ok(())
85    }
86
87    fn ensure_initialized(&mut self, batch: &RecordBatch) -> Result<()> {
88        if !self.encoding.is_initialized() {
89            self.initialize(Schema::try_from(batch.schema().as_ref())?)?;
90        }
91        Ok(())
92    }
93
94    /// Spill page metadata to a sidecar file instead of retaining it in memory.
95    pub fn with_page_metadata_spill(mut self, object_store: Arc<ObjectStore>, path: Path) -> Self {
96        self.sink.with_page_metadata_spill(object_store, path);
97        self
98    }
99
100    /// Schedule batches to be written in iteration order.
101    pub async fn write_batches(
102        &mut self,
103        batches: impl Iterator<Item = &RecordBatch>,
104    ) -> Result<()> {
105        for batch in batches {
106            self.write_batch(batch).await?;
107        }
108        Ok(())
109    }
110
111    /// Schedule one record batch for writing.
112    pub async fn write_batch(&mut self, batch: &RecordBatch) -> Result<()> {
113        self.ensure_initialized(batch)?;
114        self.encoding.write_batch(batch, &mut self.sink).await
115    }
116
117    /// Write one top-level field, advancing only that field's row count.
118    pub async fn write_column(&mut self, column_index: usize, array: ArrayRef) -> Result<()> {
119        self.encoding
120            .write_column(column_index, array, &mut self.sink)
121            .await
122    }
123
124    /// Append a buffer whose page or column metadata is supplied externally.
125    pub async fn write_external_buffer(&mut self, bytes: &[u8]) -> Result<(u64, u64)> {
126        self.sink.write_external_buffer(bytes).await
127    }
128
129    /// Add an entry to the schema metadata written in the file descriptor.
130    pub fn add_schema_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
131        self.encoding.add_schema_metadata(key, value);
132    }
133
134    /// Prepare the writer for encoded column data produced externally.
135    pub fn initialize_with_external_metadata(
136        &mut self,
137        schema: Schema,
138        column_metadata: Vec<pbfile::ColumnMetadata>,
139        rows_written: u64,
140    ) {
141        self.encoding
142            .initialize_with_external_metadata(schema, rows_written);
143        self.sink.initialize_with_external_metadata(column_metadata);
144    }
145
146    /// Add an arbitrary global buffer and return its one-based index.
147    pub async fn add_global_buffer(&mut self, buffer: Bytes) -> Result<u32> {
148        self.sink.add_global_buffer(buffer).await
149    }
150
151    /// Finish the v2.3 file and close its object writer.
152    pub async fn finish(&mut self) -> Result<FileWriteSummary> {
153        // The order below is the v2.3 wire contract.
154        self.encoding.flush(&mut self.sink).await?;
155        self.encoding.finish_encoders(&mut self.sink).await?;
156
157        let descriptor = self.encoding.make_file_descriptor()?;
158        let global_buffer_offsets = self.sink.write_global_buffers(descriptor).await?;
159        let num_global_buffers = global_buffer_offsets.len() as u32;
160
161        let column_metadata_start = self.sink.tell().await?;
162        let column_metadata_offsets = self.sink.write_column_metadatas().await?;
163        let column_metadata_offsets_start = self
164            .sink
165            .write_offset_table(&column_metadata_offsets)
166            .await?;
167        let global_buffer_offsets_start =
168            self.sink.write_offset_table(&global_buffer_offsets).await?;
169        let num_columns = self.sink.num_columns();
170
171        let output = self.sink.output_mut();
172        output.write_u64_le(column_metadata_start).await?;
173        output.write_u64_le(column_metadata_offsets_start).await?;
174        output.write_u64_le(global_buffer_offsets_start).await?;
175        output.write_u32_le(num_global_buffers).await?;
176        output.write_u32_le(num_columns).await?;
177        output.write_u16_le(2).await?;
178        output.write_u16_le(3).await?;
179        output.write_all(MAGIC).await?;
180
181        Ok(FileWriteSummary {
182            num_rows: self.encoding.rows_written(),
183            size_bytes: self.sink.shutdown().await?,
184        })
185    }
186
187    /// Abandon this write.
188    pub async fn abort(&mut self) {
189        // Dropping a multipart ObjectWriter aborts the upload.
190    }
191
192    /// Return the current object-writer position.
193    pub async fn tell(&mut self) -> Result<u64> {
194        self.sink.tell().await
195    }
196
197    /// Return the field-id to physical-column mapping selected by v2.3.
198    pub fn field_id_to_column_indices(&self) -> &[(u32, u32)] {
199        self.encoding.field_id_to_column_indices()
200    }
201}
202
203/// Append a self-described or mini-lance v2.3 footer to an encoded batch.
204pub fn concat_lance_footer(batch: &EncodedBatch, write_schema: bool) -> Result<Bytes> {
205    let EncodedBatchBody {
206        mut data,
207        column_metadata_start,
208        column_metadata_offsets_start,
209        global_buffer_offsets_start,
210        num_global_buffers,
211        num_columns,
212    } = encode_batch_body(batch, write_schema)?;
213
214    data.put_u64_le(column_metadata_start);
215    data.put_u64_le(column_metadata_offsets_start);
216    data.put_u64_le(global_buffer_offsets_start);
217    data.put_u32_le(num_global_buffers);
218    data.put_u32_le(num_columns);
219    data.put_u16_le(2);
220    data.put_u16_le(3);
221    data.extend_from_slice(MAGIC);
222    Ok(data.freeze())
223}