1use core::panic;
5use std::collections::HashMap;
6use std::sync::Arc;
7use std::sync::atomic::AtomicBool;
8
9use arrow_array::{ArrayRef, RecordBatch};
10
11use arrow_data::ArrayData;
12use bytes::{Buf, BufMut, Bytes, BytesMut};
13use futures::StreamExt;
14use futures::stream::FuturesOrdered;
15use lance_core::datatypes::{Field, Schema as LanceSchema};
16use lance_core::utils::bit::pad_bytes;
17use lance_core::{Error, Result};
18use lance_encoding::compression_config::CompressionParams;
19use lance_encoding::decoder::PageEncoding;
20use lance_encoding::encoder::{
21 BatchEncoder, EncodeTask, EncodedBatch, EncodedPage, EncodingOptions, FieldEncoder,
22 FieldEncodingStrategy, OutOfLineBuffers,
23};
24use lance_encoding::repdef::RepDefBuilder;
25use lance_io::object_store::ObjectStore;
26use lance_io::traits::Writer;
27use log::{debug, warn};
28use object_store::path::Path;
29use prost::Message;
30use prost_types::Any;
31use tokio::io::AsyncWrite;
32use tokio::io::AsyncWriteExt;
33use tracing::instrument;
34
35use crate::datatypes::FieldsWithMeta;
36use crate::format::MAGIC;
37use crate::format::pb;
38use crate::format::pbfile;
39use crate::format::pbfile::DirectEncoding;
40use crate::version::{ConcreteFileVersion, LanceFileVersion};
41use crate::versions;
42
43pub(crate) mod structural;
44
45pub(crate) const PAGE_BUFFER_ALIGNMENT: usize = 64;
47const PAD_BUFFER: [u8; PAGE_BUFFER_ALIGNMENT] = [72; PAGE_BUFFER_ALIGNMENT];
48const MAX_PAGE_BYTES: usize = 32 * 1024 * 1024;
54pub(crate) const ENV_LANCE_FILE_WRITER_MAX_PAGE_BYTES: &str = "LANCE_FILE_WRITER_MAX_PAGE_BYTES";
55
56#[cfg(test)]
57fn encoding_strategy_with_params(
58 version: LanceFileVersion,
59 params: CompressionParams,
60) -> Result<Arc<dyn FieldEncodingStrategy>> {
61 match version.resolve() {
62 LanceFileVersion::Legacy | LanceFileVersion::V2_0 => Err(Error::invalid_input(
63 "Compression parameters are only supported in Lance file version 2.1 and later",
64 )),
65 LanceFileVersion::V2_1 => Ok(versions::v2_1::encoding_strategy(params)),
66 LanceFileVersion::V2_2 => Ok(versions::v2_2::encoding_strategy(params)),
67 LanceFileVersion::V2_3 => Ok(versions::v2_3::encoding_strategy(params)),
68 LanceFileVersion::Stable | LanceFileVersion::Next => {
69 unreachable!("resolved file-version selector must be exact")
70 }
71 }
72}
73
74fn encoding_strategy(version: LanceFileVersion) -> Arc<dyn FieldEncodingStrategy> {
75 match version.resolve() {
76 LanceFileVersion::Legacy => {
77 panic!("legacy v1 files require versions::v1::writer::FileWriter")
78 }
79 LanceFileVersion::V2_0 => versions::v2_0::encoding_strategy(),
80 LanceFileVersion::V2_1 => versions::v2_1::encoding_strategy(CompressionParams::default()),
81 LanceFileVersion::V2_2 => versions::v2_2::encoding_strategy(CompressionParams::default()),
82 LanceFileVersion::V2_3 => versions::v2_3::encoding_strategy(CompressionParams::default()),
83 LanceFileVersion::Stable | LanceFileVersion::Next => {
84 unreachable!("resolved file-version selector must be exact")
85 }
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct FileWriteSummary {
92 pub num_rows: u64,
94 pub size_bytes: u64,
96}
97
98#[derive(Debug, Clone, Default)]
99pub struct FileWriterOptions {
100 pub data_cache_bytes: Option<u64>,
115 pub max_page_bytes: Option<u64>,
121 pub keep_original_array: Option<bool>,
139 pub encoding_strategy: Option<Arc<dyn FieldEncodingStrategy>>,
140 pub format_version: Option<LanceFileVersion>,
146}
147
148const DEFAULT_SPILL_BUFFER_LIMIT: usize = 256 * 1024;
151
152struct PageMetadataSpill {
162 writer: Box<dyn Writer>,
163 object_store: Arc<ObjectStore>,
164 path: Path,
165 position: u64,
167 column_buffers: Vec<Vec<u8>>,
170 column_chunks: Vec<Vec<(u64, u32)>>,
173 per_column_limit: usize,
175}
176
177impl PageMetadataSpill {
178 async fn new(object_store: Arc<ObjectStore>, path: Path, num_columns: usize) -> Result<Self> {
179 let writer = object_store.create(&path).await?;
180 let per_column_limit = (DEFAULT_SPILL_BUFFER_LIMIT / num_columns.max(1)).max(64);
181 Ok(Self {
182 writer,
183 object_store,
184 path,
185 position: 0,
186 column_buffers: vec![Vec::new(); num_columns],
187 column_chunks: vec![Vec::new(); num_columns],
188 per_column_limit,
189 })
190 }
191
192 async fn append_page(
193 &mut self,
194 column_idx: usize,
195 page: &pbfile::column_metadata::Page,
196 ) -> Result<()> {
197 page.encode_length_delimited(&mut self.column_buffers[column_idx])
198 .map_err(|e| {
199 Error::io_source(Box::new(std::io::Error::new(
200 std::io::ErrorKind::InvalidData,
201 e,
202 )))
203 })?;
204 if self.column_buffers[column_idx].len() >= self.per_column_limit {
205 self.flush_column(column_idx).await?;
206 }
207 Ok(())
208 }
209
210 async fn flush_column(&mut self, column_idx: usize) -> Result<()> {
211 let buf = &self.column_buffers[column_idx];
212 if buf.is_empty() {
213 return Ok(());
214 }
215 let len = buf.len();
216 self.writer.write_all(buf).await?;
217 self.column_chunks[column_idx].push((self.position, len as u32));
218 self.position += len as u64;
219 self.column_buffers[column_idx].clear();
220 Ok(())
221 }
222
223 async fn shutdown_writer(&mut self) -> Result<()> {
224 for col_idx in 0..self.column_buffers.len() {
225 self.flush_column(col_idx).await?;
226 }
227 Writer::shutdown(self.writer.as_mut()).await?;
228 Ok(())
229 }
230}
231
232fn decode_spilled_chunk(data: &Bytes) -> Result<Vec<pbfile::column_metadata::Page>> {
233 let mut pages = Vec::new();
234 let mut cursor = data.clone();
235 while cursor.has_remaining() {
236 let page =
237 pbfile::column_metadata::Page::decode_length_delimited(&mut cursor).map_err(|e| {
238 Error::io_source(Box::new(std::io::Error::new(
239 std::io::ErrorKind::InvalidData,
240 e,
241 )))
242 })?;
243 pages.push(page);
244 }
245 Ok(pages)
246}
247
248enum PageSpillState {
249 Pending(Arc<ObjectStore>, Path),
250 Active(PageMetadataSpill),
251}
252
253pub struct FileWriter {
254 writer: Box<dyn Writer>,
255 schema: Option<LanceSchema>,
256 column_writers: Vec<Box<dyn FieldEncoder>>,
257 column_metadata: Vec<pbfile::ColumnMetadata>,
258 field_id_to_column_indices: Vec<(u32, u32)>,
259 num_columns: u32,
260 rows_written: u64,
261 field_rows_written: Vec<u64>,
266 global_buffers: Vec<(u64, u64)>,
267 schema_metadata: HashMap<String, String>,
268 options: FileWriterOptions,
269 page_spill: Option<PageSpillState>,
270}
271
272fn initial_column_metadata() -> pbfile::ColumnMetadata {
273 pbfile::ColumnMetadata {
274 pages: Vec::new(),
275 buffer_offsets: Vec::new(),
276 buffer_sizes: Vec::new(),
277 encoding: None,
278 }
279}
280
281static WARNED_ON_UNSTABLE_API: AtomicBool = AtomicBool::new(false);
282
283impl FileWriter {
284 pub fn try_new(
286 object_writer: Box<dyn Writer>,
287 schema: LanceSchema,
288 options: FileWriterOptions,
289 ) -> Result<Self> {
290 let mut writer = Self::new_lazy(object_writer, options);
291 writer.initialize(schema)?;
292 Ok(writer)
293 }
294
295 pub fn new_lazy(object_writer: Box<dyn Writer>, options: FileWriterOptions) -> Self {
300 if let Some(format_version) = options.format_version
301 && format_version.is_unstable()
302 && WARNED_ON_UNSTABLE_API
303 .compare_exchange(
304 false,
305 true,
306 std::sync::atomic::Ordering::Relaxed,
307 std::sync::atomic::Ordering::Relaxed,
308 )
309 .is_ok()
310 {
311 warn!(
312 "You have requested an unstable format version. Files written with this format version may not be readable in the future! This is a development feature and should only be used for experimentation and never for production data."
313 );
314 }
315 Self {
316 writer: object_writer,
317 schema: None,
318 column_writers: Vec::new(),
319 column_metadata: Vec::new(),
320 num_columns: 0,
321 rows_written: 0,
322 field_rows_written: Vec::new(),
323 field_id_to_column_indices: Vec::new(),
324 global_buffers: Vec::new(),
325 schema_metadata: HashMap::new(),
326 page_spill: None,
327 options,
328 }
329 }
330
331 pub fn with_page_metadata_spill(mut self, object_store: Arc<ObjectStore>, path: Path) -> Self {
339 self.page_spill = Some(PageSpillState::Pending(object_store, path));
340 self
341 }
342
343 pub async fn create_file_with_batches(
347 store: &ObjectStore,
348 path: &Path,
349 schema: lance_core::datatypes::Schema,
350 batches: impl Iterator<Item = RecordBatch> + Send,
351 options: FileWriterOptions,
352 ) -> Result<usize> {
353 let writer = store.create(path).await?;
354 let mut writer = Self::try_new(writer, schema, options)?;
355 for batch in batches {
356 writer.write_batch(&batch).await?;
357 }
358 Ok(writer.finish().await?.num_rows as usize)
359 }
360
361 async fn do_write_buffer(writer: &mut (impl AsyncWrite + Unpin), buf: &[u8]) -> Result<()> {
362 writer.write_all(buf).await?;
363 let pad_bytes = pad_bytes::<PAGE_BUFFER_ALIGNMENT>(buf.len());
364 writer.write_all(&PAD_BUFFER[..pad_bytes]).await?;
365 Ok(())
366 }
367
368 pub fn version(&self) -> LanceFileVersion {
370 self.options.format_version.unwrap_or_default()
371 }
372
373 async fn write_page(&mut self, encoded_page: EncodedPage) -> Result<()> {
374 let buffers = encoded_page.data;
375 let mut buffer_offsets = Vec::with_capacity(buffers.len());
376 let mut buffer_sizes = Vec::with_capacity(buffers.len());
377 for buffer in buffers {
378 buffer_offsets.push(self.writer.tell().await? as u64);
379 buffer_sizes.push(buffer.len() as u64);
380 Self::do_write_buffer(&mut self.writer, &buffer).await?;
381 }
382 let encoded_encoding = match encoded_page.description {
383 PageEncoding::Legacy(array_encoding) => Any::from_msg(&array_encoding)?.encode_to_vec(),
384 PageEncoding::Structural(page_layout) => Any::from_msg(&page_layout)?.encode_to_vec(),
385 };
386 let page = pbfile::column_metadata::Page {
387 buffer_offsets,
388 buffer_sizes,
389 encoding: Some(pbfile::Encoding {
390 location: Some(pbfile::encoding::Location::Direct(DirectEncoding {
391 encoding: encoded_encoding,
392 })),
393 }),
394 length: encoded_page.num_rows,
395 priority: encoded_page.row_number,
396 };
397 let col_idx = encoded_page.column_idx as usize;
398 if matches!(&self.page_spill, Some(PageSpillState::Pending(..))) {
399 let Some(PageSpillState::Pending(store, path)) = self.page_spill.take() else {
400 unreachable!()
401 };
402 self.page_spill = Some(PageSpillState::Active(
403 PageMetadataSpill::new(store, path, self.num_columns as usize).await?,
404 ));
405 }
406 match &mut self.page_spill {
407 Some(PageSpillState::Active(spill)) => spill.append_page(col_idx, &page).await?,
408 None => self.column_metadata[col_idx].pages.push(page),
409 Some(PageSpillState::Pending(..)) => unreachable!(),
410 }
411 Ok(())
412 }
413
414 #[instrument(skip_all, level = "debug")]
415 async fn write_pages(&mut self, mut encoding_tasks: FuturesOrdered<EncodeTask>) -> Result<()> {
416 while let Some(encoding_task) = encoding_tasks.next().await {
425 let encoded_page = encoding_task?;
426 self.write_page(encoded_page).await?;
427 }
428 self.writer.flush().await?;
433 Ok(())
434 }
435
436 pub async fn write_batches(
438 &mut self,
439 batches: impl Iterator<Item = &RecordBatch>,
440 ) -> Result<()> {
441 for batch in batches {
442 self.write_batch(batch).await?;
443 }
444 Ok(())
445 }
446
447 fn verify_field_nullability(arr: &ArrayData, field: &Field) -> Result<()> {
448 if !field.nullable && arr.null_count() > 0 {
449 return Err(Error::invalid_input(format!(
450 "The field `{}` contained null values even though the field is marked non-null in the schema",
451 field.name
452 )));
453 }
454
455 for (child_field, child_arr) in field.children.iter().zip(arr.child_data()) {
456 Self::verify_field_nullability(child_arr, child_field)?;
457 }
458
459 Ok(())
460 }
461
462 fn verify_nullability_constraints(&self, batch: &RecordBatch) -> Result<()> {
463 for (col, field) in batch
464 .columns()
465 .iter()
466 .zip(self.schema.as_ref().unwrap().fields.iter())
467 {
468 Self::verify_field_nullability(&col.to_data(), field)?;
469 }
470 Ok(())
471 }
472
473 fn initialize(&mut self, mut schema: LanceSchema) -> Result<()> {
474 let cache_bytes_per_column = if let Some(data_cache_bytes) = self.options.data_cache_bytes {
475 data_cache_bytes / schema.fields.len() as u64
476 } else {
477 8 * 1024 * 1024
478 };
479
480 let max_page_bytes = self.options.max_page_bytes.unwrap_or_else(|| {
481 std::env::var(ENV_LANCE_FILE_WRITER_MAX_PAGE_BYTES)
482 .map(|s| {
483 s.parse::<u64>().unwrap_or_else(|e| {
484 warn!(
485 "Failed to parse {}: {}, using default",
486 ENV_LANCE_FILE_WRITER_MAX_PAGE_BYTES, e
487 );
488 MAX_PAGE_BYTES as u64
489 })
490 })
491 .unwrap_or(MAX_PAGE_BYTES as u64)
492 });
493
494 schema.validate()?;
495
496 let keep_original_array = self.options.keep_original_array.unwrap_or(false);
497 let encoding_strategy = self
498 .options
499 .encoding_strategy
500 .clone()
501 .unwrap_or_else(|| encoding_strategy(self.version()));
502
503 let encoding_options = EncodingOptions {
504 cache_bytes_per_column,
505 max_page_bytes,
506 keep_original_array,
507 buffer_alignment: PAGE_BUFFER_ALIGNMENT as u64,
508 };
509 let encoder =
510 BatchEncoder::try_new(&schema, encoding_strategy.as_ref(), &encoding_options)?;
511 self.num_columns = encoder.num_columns();
512
513 self.field_rows_written = vec![0; encoder.field_encoders.len()];
514 self.column_writers = encoder.field_encoders;
515 self.column_metadata = vec![initial_column_metadata(); self.num_columns as usize];
516 self.field_id_to_column_indices = encoder.field_id_to_column_index;
517 self.schema_metadata
518 .extend(std::mem::take(&mut schema.metadata));
519 self.schema = Some(schema);
520 Ok(())
521 }
522
523 fn ensure_initialized(&mut self, batch: &RecordBatch) -> Result<&LanceSchema> {
524 if self.schema.is_none() {
525 let schema = LanceSchema::try_from(batch.schema().as_ref())?;
526 self.initialize(schema)?;
527 }
528 Ok(self.schema.as_ref().unwrap())
529 }
530
531 #[instrument(skip_all, level = "debug")]
532 fn encode_batch(
533 &mut self,
534 batch: &RecordBatch,
535 external_buffers: &mut OutOfLineBuffers,
536 ) -> Result<Vec<Vec<EncodeTask>>> {
537 let field_arrays = self
538 .schema
539 .as_ref()
540 .unwrap()
541 .fields
542 .iter()
543 .enumerate()
544 .map(|(field_idx, field)| {
545 let array =
546 batch
547 .column_by_name(&field.name)
548 .ok_or(Error::invalid_input_source(
549 format!(
550 "Cannot write batch. The batch was missing the column `{}`",
551 field.name
552 )
553 .into(),
554 ))?;
555 Ok((field_idx, array.clone()))
556 })
557 .collect::<Result<Vec<_>>>()?;
558 self.encode_columns(&field_arrays, external_buffers)
559 }
560
561 fn encode_columns(
566 &mut self,
567 field_arrays: &[(usize, ArrayRef)],
568 external_buffers: &mut OutOfLineBuffers,
569 ) -> Result<Vec<Vec<EncodeTask>>> {
570 let row_numbers = field_arrays
573 .iter()
574 .map(|(field_idx, _)| self.field_rows_written[*field_idx])
575 .collect::<Vec<_>>();
576 field_arrays
577 .iter()
578 .zip(row_numbers)
579 .map(|((field_idx, array), row_number)| {
580 let repdef = RepDefBuilder::default();
581 let num_rows = array.len() as u64;
582 self.column_writers[*field_idx].maybe_encode(
583 array.clone(),
584 external_buffers,
585 repdef,
586 row_number,
587 num_rows,
588 )
589 })
590 .collect::<Result<Vec<_>>>()
591 }
592
593 fn advance_columns(&mut self, field_arrays: &[(usize, ArrayRef)]) {
599 for (field_idx, array) in field_arrays {
600 let new_total = self.field_rows_written[*field_idx] + array.len() as u64;
601 self.field_rows_written[*field_idx] = new_total;
602 self.rows_written = self.rows_written.max(new_total);
603 }
604 }
605
606 pub async fn write_batch(&mut self, batch: &RecordBatch) -> Result<()> {
611 debug!(
612 "write_batch called with {} rows, {} columns, and {} bytes of data",
613 batch.num_rows(),
614 batch.num_columns(),
615 batch.get_array_memory_size()
616 );
617 self.ensure_initialized(batch)?;
618 self.verify_nullability_constraints(batch)?;
619 let num_rows = batch.num_rows() as u64;
620 if num_rows == 0 {
621 return Ok(());
622 }
623 if num_rows > u32::MAX as u64 {
624 return Err(Error::invalid_input_source(
625 "cannot write Lance files with more than 2^32 rows".into(),
626 ));
627 }
628 let mut external_buffers =
631 OutOfLineBuffers::new(self.tell().await?, PAGE_BUFFER_ALIGNMENT as u64);
632 let encoding_tasks = self.encode_batch(batch, &mut external_buffers)?;
633 for external_buffer in external_buffers.take_buffers() {
635 Self::do_write_buffer(&mut self.writer, &external_buffer).await?;
636 }
637
638 let encoding_tasks = encoding_tasks
639 .into_iter()
640 .flatten()
641 .collect::<FuturesOrdered<_>>();
642
643 if self.rows_written.checked_add(num_rows).is_none() {
647 return Err(Error::invalid_input_source(format!("cannot write batch with {} rows because {} rows have already been written and Lance files cannot contain more than 2^64 rows", num_rows, self.rows_written).into()));
648 }
649 for field_rows in self.field_rows_written.iter_mut() {
650 *field_rows += num_rows;
651 }
652 self.rows_written += num_rows;
653
654 self.write_pages(encoding_tasks).await?;
655
656 Ok(())
657 }
658
659 pub async fn write_column(&mut self, column_index: usize, array: ArrayRef) -> Result<()> {
689 let schema = self.schema.as_ref().ok_or_else(|| {
690 Error::invalid_input_source(
691 "write_column requires the writer to be created with an explicit schema".into(),
692 )
693 })?;
694 let field = schema.fields.get(column_index).ok_or_else(|| {
695 Error::invalid_input_source(
696 format!(
697 "write_column: field index {} is out of bounds (schema has {} fields)",
698 column_index,
699 schema.fields.len()
700 )
701 .into(),
702 )
703 })?;
704 if array.len() as u64 > u32::MAX as u64 {
705 return Err(Error::invalid_input_source(
706 "cannot write Lance files with more than 2^32 rows".into(),
707 ));
708 }
709 Self::verify_field_nullability(&array.to_data(), field)?;
710
711 if array.is_empty() {
714 return Ok(());
715 }
716
717 let columns = [(column_index, array)];
718 let mut external_buffers =
719 OutOfLineBuffers::new(self.tell().await?, PAGE_BUFFER_ALIGNMENT as u64);
720 let encoding_tasks = self.encode_columns(&columns, &mut external_buffers)?;
721 for external_buffer in external_buffers.take_buffers() {
722 Self::do_write_buffer(&mut self.writer, &external_buffer).await?;
723 }
724 let encoding_tasks = encoding_tasks
725 .into_iter()
726 .flatten()
727 .collect::<FuturesOrdered<_>>();
728
729 self.advance_columns(&columns);
730 self.write_pages(encoding_tasks).await?;
731 Ok(())
732 }
733
734 async fn write_column_metadata(
735 &mut self,
736 metadata: pbfile::ColumnMetadata,
737 ) -> Result<(u64, u64)> {
738 let metadata_bytes = metadata.encode_to_vec();
739 let position = self.writer.tell().await? as u64;
740 let len = metadata_bytes.len() as u64;
741 self.writer.write_all(&metadata_bytes).await?;
742 Ok((position, len))
743 }
744
745 async fn write_column_metadatas(&mut self) -> Result<Vec<(u64, u64)>> {
746 let metadatas = std::mem::take(&mut self.column_metadata);
747
748 let spill_state = self.page_spill.take();
752 let (spill_chunks, spill_reader) =
753 if let Some(PageSpillState::Active(mut spill)) = spill_state {
754 spill.shutdown_writer().await?;
755 let reader = spill.object_store.open(&spill.path).await?;
756 let chunks = std::mem::take(&mut spill.column_chunks);
757 (chunks, Some(reader))
758 } else {
759 (Vec::new(), None)
760 };
761
762 let mut metadata_positions = Vec::with_capacity(metadatas.len());
763 for (col_idx, mut metadata) in metadatas.into_iter().enumerate() {
764 if let Some(reader) = &spill_reader {
765 let mut pages = Vec::new();
766 for &(offset, len) in &spill_chunks[col_idx] {
767 let data = reader
768 .get_range(offset as usize..(offset as usize + len as usize))
769 .await
770 .map_err(|e| Error::io_source(Box::new(e)))?;
771 pages.extend(decode_spilled_chunk(&data)?);
772 }
773 metadata.pages = pages;
774 }
775 metadata_positions.push(self.write_column_metadata(metadata).await?);
776 }
777
778 Ok(metadata_positions)
779 }
780
781 fn make_file_descriptor(
782 schema: &lance_core::datatypes::Schema,
783 num_rows: u64,
784 ) -> Result<pb::FileDescriptor> {
785 let fields_with_meta = FieldsWithMeta::from(schema);
786 Ok(pb::FileDescriptor {
787 schema: Some(pb::Schema {
788 fields: fields_with_meta.fields.0,
789 metadata: fields_with_meta.metadata,
790 }),
791 length: num_rows,
792 })
793 }
794
795 async fn write_global_buffers(&mut self) -> Result<Vec<(u64, u64)>> {
796 let schema = self.schema.as_mut().ok_or(Error::invalid_input("No schema provided on writer open and no data provided. Schema is unknown and file cannot be created"))?;
797 schema.metadata = std::mem::take(&mut self.schema_metadata);
798 schema
800 .fields
801 .iter_mut()
802 .for_each(|f| f.unload_blobs_recursive());
803
804 let file_descriptor = Self::make_file_descriptor(schema, self.rows_written)?;
805 let file_descriptor_bytes = file_descriptor.encode_to_vec();
806 let file_descriptor_len = file_descriptor_bytes.len() as u64;
807 let file_descriptor_position = self.writer.tell().await? as u64;
808 self.writer.write_all(&file_descriptor_bytes).await?;
809 let mut gbo_table = Vec::with_capacity(1 + self.global_buffers.len());
810 gbo_table.push((file_descriptor_position, file_descriptor_len));
811 gbo_table.append(&mut self.global_buffers);
812 Ok(gbo_table)
813 }
814
815 pub fn add_schema_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
821 self.schema_metadata.insert(key.into(), value.into());
822 }
823
824 pub fn initialize_with_external_metadata(
833 &mut self,
834 schema: lance_core::datatypes::Schema,
835 column_metadata: Vec<pbfile::ColumnMetadata>,
836 rows_written: u64,
837 ) {
838 self.schema = Some(schema);
839 self.num_columns = column_metadata.len() as u32;
840 self.column_metadata = column_metadata;
841 self.rows_written = rows_written;
842 }
843
844 pub async fn add_global_buffer(&mut self, buffer: Bytes) -> Result<u32> {
850 let position = self.writer.tell().await? as u64;
851 let len = buffer.len() as u64;
852 Self::do_write_buffer(&mut self.writer, &buffer).await?;
853 self.global_buffers.push((position, len));
854 Ok(self.global_buffers.len() as u32)
855 }
856
857 async fn finish_writers(&mut self) -> Result<()> {
858 let mut col_idx = 0;
859 for mut writer in std::mem::take(&mut self.column_writers) {
860 let mut external_buffers =
861 OutOfLineBuffers::new(self.tell().await?, PAGE_BUFFER_ALIGNMENT as u64);
862 let columns = writer.finish(&mut external_buffers).await?;
863 for buffer in external_buffers.take_buffers() {
864 self.writer.write_all(&buffer).await?;
865 }
866 debug_assert_eq!(
867 columns.len(),
868 writer.num_columns() as usize,
869 "Expected {} columns from column at index {} and got {}",
870 writer.num_columns(),
871 col_idx,
872 columns.len()
873 );
874 for column in columns {
875 for page in column.final_pages {
876 self.write_page(page).await?;
877 }
878 let column_metadata = &mut self.column_metadata[col_idx];
879 let mut buffer_pos = self.writer.tell().await? as u64;
880 for buffer in column.column_buffers {
881 column_metadata.buffer_offsets.push(buffer_pos);
882 let mut size = 0;
883 Self::do_write_buffer(&mut self.writer, &buffer).await?;
884 size += buffer.len() as u64;
885 buffer_pos += size;
886 column_metadata.buffer_sizes.push(size);
887 }
888 let encoded_encoding = Any::from_msg(&column.encoding)?.encode_to_vec();
889 column_metadata.encoding = Some(pbfile::Encoding {
890 location: Some(pbfile::encoding::Location::Direct(pbfile::DirectEncoding {
891 encoding: encoded_encoding,
892 })),
893 });
894 col_idx += 1;
895 }
896 }
897 if col_idx != self.column_metadata.len() {
898 panic!(
899 "Column writers finished with {} columns but we expected {}",
900 col_idx,
901 self.column_metadata.len()
902 );
903 }
904 Ok(())
905 }
906
907 fn standard_footer_numbers(&self) -> (u16, u16) {
908 let version = self.version();
909 let exact_version = ConcreteFileVersion::from(version);
910 if exact_version == crate::version::ConcreteFileVersion::V1 {
911 panic!("Unsupported version: {}", version);
912 }
913 exact_version.to_standard_footer_numbers()
914 }
915
916 pub async fn finish(&mut self) -> Result<FileWriteSummary> {
924 let mut external_buffers =
926 OutOfLineBuffers::new(self.tell().await?, PAGE_BUFFER_ALIGNMENT as u64);
927 let encoding_tasks = self
928 .column_writers
929 .iter_mut()
930 .map(|writer| writer.flush(&mut external_buffers))
931 .collect::<Result<Vec<_>>>()?;
932 for external_buffer in external_buffers.take_buffers() {
933 Self::do_write_buffer(&mut self.writer, &external_buffer).await?;
934 }
935 let encoding_tasks = encoding_tasks
936 .into_iter()
937 .flatten()
938 .collect::<FuturesOrdered<_>>();
939 self.write_pages(encoding_tasks).await?;
940
941 if !self.column_writers.is_empty() {
942 self.finish_writers().await?;
943 }
944
945 let global_buffer_offsets = self.write_global_buffers().await?;
947 let num_global_buffers = global_buffer_offsets.len() as u32;
948
949 let column_metadata_start = self.writer.tell().await? as u64;
951 let metadata_positions = self.write_column_metadatas().await?;
952
953 let cmo_table_start = self.writer.tell().await? as u64;
955 for (meta_pos, meta_len) in metadata_positions {
956 self.writer.write_u64_le(meta_pos).await?;
957 self.writer.write_u64_le(meta_len).await?;
958 }
959
960 let gbo_table_start = self.writer.tell().await? as u64;
962 for (gbo_pos, gbo_len) in global_buffer_offsets {
963 self.writer.write_u64_le(gbo_pos).await?;
964 self.writer.write_u64_le(gbo_len).await?;
965 }
966
967 let (major, minor) = self.standard_footer_numbers();
968 self.writer.write_u64_le(column_metadata_start).await?;
970 self.writer.write_u64_le(cmo_table_start).await?;
971 self.writer.write_u64_le(gbo_table_start).await?;
972 self.writer.write_u32_le(num_global_buffers).await?;
973 self.writer.write_u32_le(self.num_columns).await?;
974 self.writer.write_u16_le(major).await?;
975 self.writer.write_u16_le(minor).await?;
976 self.writer.write_all(MAGIC).await?;
977
978 let write_result = Writer::shutdown(self.writer.as_mut()).await?;
980
981 Ok(FileWriteSummary {
982 num_rows: self.rows_written,
983 size_bytes: write_result.size as u64,
984 })
985 }
986
987 pub async fn abort(&mut self) {
988 }
991
992 pub async fn tell(&mut self) -> Result<u64> {
993 Ok(self.writer.tell().await? as u64)
994 }
995
996 pub fn field_id_to_column_indices(&self) -> &[(u32, u32)] {
997 &self.field_id_to_column_indices
998 }
999}
1000
1001pub trait EncodedBatchWriteExt {
1004 fn try_to_self_described_lance(&self, version: LanceFileVersion) -> Result<Bytes>;
1006 fn try_to_mini_lance(&self, version: LanceFileVersion) -> Result<Bytes>;
1010}
1011
1012fn concat_lance_footer(
1017 batch: &EncodedBatch,
1018 write_schema: bool,
1019 version: LanceFileVersion,
1020) -> Result<Bytes> {
1021 let mut data = BytesMut::with_capacity(batch.data.len() + 1024 * 1024);
1023 data.put(batch.data.clone());
1024 let global_buffers = if write_schema {
1026 let schema_start = data.len() as u64;
1027 let lance_schema = lance_core::datatypes::Schema::try_from(batch.schema.as_ref())?;
1028 let descriptor = FileWriter::make_file_descriptor(&lance_schema, batch.num_rows)?;
1029 let descriptor_bytes = descriptor.encode_to_vec();
1030 let descriptor_len = descriptor_bytes.len() as u64;
1031 data.put(descriptor_bytes.as_slice());
1032
1033 vec![(schema_start, descriptor_len)]
1034 } else {
1035 vec![]
1036 };
1037 let col_metadata_start = data.len() as u64;
1038
1039 let mut col_metadata_positions = Vec::new();
1040 for col in &batch.page_table {
1042 let position = data.len() as u64;
1043 let pages = col
1044 .page_infos
1045 .iter()
1046 .map(|page_info| {
1047 let encoded_encoding = match &page_info.encoding {
1048 PageEncoding::Legacy(array_encoding) => {
1049 Any::from_msg(array_encoding)?.encode_to_vec()
1050 }
1051 PageEncoding::Structural(page_layout) => {
1052 Any::from_msg(page_layout)?.encode_to_vec()
1053 }
1054 };
1055 let (buffer_offsets, buffer_sizes): (Vec<_>, Vec<_>) = page_info
1056 .buffer_offsets_and_sizes
1057 .as_ref()
1058 .iter()
1059 .cloned()
1060 .unzip();
1061 Ok(pbfile::column_metadata::Page {
1062 buffer_offsets,
1063 buffer_sizes,
1064 encoding: Some(pbfile::Encoding {
1065 location: Some(pbfile::encoding::Location::Direct(DirectEncoding {
1066 encoding: encoded_encoding,
1067 })),
1068 }),
1069 length: page_info.num_rows,
1070 priority: page_info.priority,
1071 })
1072 })
1073 .collect::<Result<Vec<_>>>()?;
1074 let (buffer_offsets, buffer_sizes): (Vec<_>, Vec<_>) =
1075 col.buffer_offsets_and_sizes.iter().cloned().unzip();
1076 let encoded_col_encoding = Any::from_msg(&col.encoding)?.encode_to_vec();
1077 let column = pbfile::ColumnMetadata {
1078 pages,
1079 buffer_offsets,
1080 buffer_sizes,
1081 encoding: Some(pbfile::Encoding {
1082 location: Some(pbfile::encoding::Location::Direct(pbfile::DirectEncoding {
1083 encoding: encoded_col_encoding,
1084 })),
1085 }),
1086 };
1087 let column_bytes = column.encode_to_vec();
1088 col_metadata_positions.push((position, column_bytes.len() as u64));
1089 data.put(column_bytes.as_slice());
1090 }
1091 let cmo_table_start = data.len() as u64;
1093 for (meta_pos, meta_len) in col_metadata_positions {
1094 data.put_u64_le(meta_pos);
1095 data.put_u64_le(meta_len);
1096 }
1097 let gbo_table_start = data.len() as u64;
1099 let num_global_buffers = global_buffers.len() as u32;
1100 for (gbo_pos, gbo_len) in global_buffers {
1101 data.put_u64_le(gbo_pos);
1102 data.put_u64_le(gbo_len);
1103 }
1104
1105 let (major, minor) = ConcreteFileVersion::from(version).to_embedded_footer_numbers();
1106
1107 data.put_u64_le(col_metadata_start);
1109 data.put_u64_le(cmo_table_start);
1110 data.put_u64_le(gbo_table_start);
1111 data.put_u32_le(num_global_buffers);
1112 data.put_u32_le(batch.page_table.len() as u32);
1113 data.put_u16_le(major);
1114 data.put_u16_le(minor);
1115 data.put(MAGIC.as_slice());
1116
1117 Ok(data.freeze())
1118}
1119
1120impl EncodedBatchWriteExt for EncodedBatch {
1121 fn try_to_self_described_lance(&self, version: LanceFileVersion) -> Result<Bytes> {
1122 concat_lance_footer(self, true, version)
1123 }
1124
1125 fn try_to_mini_lance(&self, version: LanceFileVersion) -> Result<Bytes> {
1126 concat_lance_footer(self, false, version)
1127 }
1128}
1129
1130#[cfg(test)]
1131mod tests {
1132 use std::collections::HashMap;
1133 use std::sync::Arc;
1134
1135 use crate::reader::{FileReader, FileReaderOptions, ReaderProjection, describe_encoding};
1136 use crate::testing::FsFixture;
1137 use crate::writer::{ENV_LANCE_FILE_WRITER_MAX_PAGE_BYTES, FileWriter, FileWriterOptions};
1138 use arrow_array::builder::{Float32Builder, Int32Builder};
1139 use arrow_array::{ArrayRef, Int32Array, RecordBatch, UInt64Array};
1140 use arrow_array::{RecordBatchReader, StringArray, types::Float64Type};
1141 use arrow_schema::{DataType, Field, Field as ArrowField, Schema, Schema as ArrowSchema};
1142 use lance_core::cache::LanceCache;
1143 use lance_core::datatypes::Schema as LanceSchema;
1144 use lance_core::utils::tempfile::TempObjFile;
1145 use lance_datagen::{BatchCount, RowCount, array, gen_batch};
1146 use lance_encoding::compression_config::{CompressionFieldParams, CompressionParams};
1147 use lance_encoding::decoder::DecoderPlugins;
1148 use lance_encoding::version::LanceFileVersion;
1149 use lance_io::object_store::ObjectStore;
1150 use lance_io::utils::CachedFileSize;
1151 use rstest::rstest;
1152
1153 #[tokio::test]
1154 async fn test_basic_write() {
1155 let tmp_path = TempObjFile::default();
1156 let obj_store = Arc::new(ObjectStore::local());
1157
1158 let reader = gen_batch()
1159 .col("score", array::rand::<Float64Type>())
1160 .into_reader_rows(RowCount::from(1000), BatchCount::from(10));
1161
1162 let writer = obj_store.create(&tmp_path).await.unwrap();
1163
1164 let lance_schema =
1165 lance_core::datatypes::Schema::try_from(reader.schema().as_ref()).unwrap();
1166
1167 let mut file_writer =
1168 FileWriter::try_new(writer, lance_schema, FileWriterOptions::default()).unwrap();
1169
1170 for batch in reader {
1171 file_writer.write_batch(&batch.unwrap()).await.unwrap();
1172 }
1173 file_writer.add_schema_metadata("foo", "bar");
1174 file_writer.finish().await.unwrap();
1175 }
1177
1178 #[tokio::test]
1179 async fn test_write_empty() {
1180 let tmp_path = TempObjFile::default();
1181 let obj_store = Arc::new(ObjectStore::local());
1182
1183 let reader = gen_batch()
1184 .col("score", array::rand::<Float64Type>())
1185 .into_reader_rows(RowCount::from(0), BatchCount::from(0));
1186
1187 let writer = obj_store.create(&tmp_path).await.unwrap();
1188
1189 let lance_schema =
1190 lance_core::datatypes::Schema::try_from(reader.schema().as_ref()).unwrap();
1191
1192 let mut file_writer =
1193 FileWriter::try_new(writer, lance_schema, FileWriterOptions::default()).unwrap();
1194
1195 for batch in reader {
1196 file_writer.write_batch(&batch.unwrap()).await.unwrap();
1197 }
1198 file_writer.add_schema_metadata("foo", "bar");
1199 file_writer.finish().await.unwrap();
1200 }
1201
1202 async fn read_int32_column(
1207 reader: &FileReader,
1208 schema: &LanceSchema,
1209 version: LanceFileVersion,
1210 name: &str,
1211 params: lance_io::ReadBatchParams,
1212 ) -> Vec<Option<i32>> {
1213 use futures::TryStreamExt;
1214 use lance_encoding::decoder::FilterExpression;
1215
1216 let projection = ReaderProjection::from_column_names(version, schema, &[name]).unwrap();
1217 let batches: Vec<RecordBatch> = reader
1218 .read_stream_projected(params, 1024, 16, projection, FilterExpression::no_filter())
1219 .await
1220 .unwrap()
1221 .try_collect()
1222 .await
1223 .unwrap();
1224 batches
1225 .iter()
1226 .flat_map(|b| {
1227 b.column(0)
1228 .as_any()
1229 .downcast_ref::<Int32Array>()
1230 .unwrap()
1231 .iter()
1232 .collect::<Vec<_>>()
1233 })
1234 .collect()
1235 }
1236
1237 #[rstest]
1241 #[tokio::test]
1242 async fn test_write_columns_unequal_lengths(
1243 #[values(LanceFileVersion::V2_0, LanceFileVersion::V2_1)] version: LanceFileVersion,
1244 ) {
1245 use lance_io::ReadBatchParams;
1246
1247 let arrow_schema = Arc::new(ArrowSchema::new(vec![
1248 ArrowField::new("a", DataType::Int32, true),
1249 ArrowField::new("b", DataType::Int32, true),
1250 ArrowField::new("c", DataType::Int32, true),
1251 ]));
1252 let lance_schema = LanceSchema::try_from(arrow_schema.as_ref()).unwrap();
1253
1254 let fs = FsFixture::default();
1255 let options = FileWriterOptions {
1256 format_version: Some(version),
1257 ..Default::default()
1258 };
1259 let mut writer = FileWriter::try_new(
1260 fs.object_store.create(&fs.tmp_path).await.unwrap(),
1261 lance_schema.clone(),
1262 options,
1263 )
1264 .unwrap();
1265
1266 let a1: ArrayRef = Arc::new(Int32Array::from(vec![1, 2, 3]));
1269 let b: ArrayRef = Arc::new(Int32Array::from(vec![10]));
1270 writer.write_column(0, a1).await.unwrap();
1271 writer.write_column(1, b).await.unwrap();
1272 let a2: ArrayRef = Arc::new(Int32Array::from(vec![4, 5]));
1273 writer.write_column(0, a2).await.unwrap();
1274 let empty: ArrayRef = Arc::new(Int32Array::from(Vec::<i32>::new()));
1277 writer.write_column(0, empty.clone()).await.unwrap();
1278 writer.write_column(2, empty).await.unwrap();
1279
1280 let summary = writer.finish().await.unwrap();
1281 assert_eq!(summary.num_rows, 5);
1283
1284 let file_scheduler = fs
1285 .scheduler
1286 .open_file(&fs.tmp_path, &CachedFileSize::unknown())
1287 .await
1288 .unwrap();
1289 let reader = FileReader::try_open(
1290 file_scheduler,
1291 None,
1292 Arc::<DecoderPlugins>::default(),
1293 &LanceCache::no_cache(),
1294 FileReaderOptions::default(),
1295 )
1296 .await
1297 .unwrap();
1298
1299 assert_eq!(reader.num_rows(), 5);
1301 assert_eq!(reader.column_num_rows(0).unwrap(), 5);
1302 assert_eq!(reader.column_num_rows(1).unwrap(), 1);
1303 assert_eq!(reader.column_num_rows(2).unwrap(), 0);
1304 assert!(reader.column_num_rows(3).is_err());
1305
1306 assert_eq!(
1308 read_int32_column(
1309 &reader,
1310 &lance_schema,
1311 version,
1312 "a",
1313 ReadBatchParams::Range(0..5)
1314 )
1315 .await,
1316 vec![Some(1), Some(2), Some(3), Some(4), Some(5)],
1317 );
1318 assert_eq!(
1319 read_int32_column(
1320 &reader,
1321 &lance_schema,
1322 version,
1323 "b",
1324 ReadBatchParams::Range(0..1)
1325 )
1326 .await,
1327 vec![Some(10)],
1328 );
1329
1330 assert_eq!(
1334 read_int32_column(
1335 &reader,
1336 &lance_schema,
1337 version,
1338 "a",
1339 ReadBatchParams::Indices(arrow_array::UInt32Array::from(vec![0, 2, 4])),
1340 )
1341 .await,
1342 vec![Some(1), Some(3), Some(5)],
1343 );
1344 }
1345
1346 #[rstest]
1354 #[tokio::test]
1355 async fn test_read_unequal_length_projection(
1356 #[values(LanceFileVersion::V2_0, LanceFileVersion::V2_1)] version: LanceFileVersion,
1357 ) {
1358 use futures::TryStreamExt;
1359 use lance_encoding::decoder::FilterExpression;
1360 use lance_io::ReadBatchParams;
1361
1362 let arrow_schema = Arc::new(ArrowSchema::new(vec![
1363 ArrowField::new("a", DataType::Int32, true),
1364 ArrowField::new("b", DataType::Int32, true),
1365 ArrowField::new("c", DataType::Int32, true),
1366 ]));
1367 let lance_schema = LanceSchema::try_from(arrow_schema.as_ref()).unwrap();
1368 let fs = FsFixture::default();
1369 let options = FileWriterOptions {
1370 format_version: Some(version),
1371 ..Default::default()
1372 };
1373 let mut writer = FileWriter::try_new(
1374 fs.object_store.create(&fs.tmp_path).await.unwrap(),
1375 lance_schema.clone(),
1376 options,
1377 )
1378 .unwrap();
1379 writer
1381 .write_column(0, Arc::new(Int32Array::from(vec![1, 2, 3, 4, 5])))
1382 .await
1383 .unwrap();
1384 writer
1385 .write_column(1, Arc::new(Int32Array::from(vec![6, 7, 8, 9, 10])))
1386 .await
1387 .unwrap();
1388 writer
1389 .write_column(2, Arc::new(Int32Array::from(vec![100])))
1390 .await
1391 .unwrap();
1392 writer.finish().await.unwrap();
1393
1394 let file_scheduler = fs
1395 .scheduler
1396 .open_file(&fs.tmp_path, &CachedFileSize::unknown())
1397 .await
1398 .unwrap();
1399 let reader = FileReader::try_open(
1400 file_scheduler,
1401 None,
1402 Arc::<DecoderPlugins>::default(),
1403 &LanceCache::no_cache(),
1404 FileReaderOptions::default(),
1405 )
1406 .await
1407 .unwrap();
1408
1409 let read = |names: &'static [&'static str], params: ReadBatchParams| {
1410 let projection =
1411 ReaderProjection::from_column_names(version, &lance_schema, names).unwrap();
1412 async {
1413 match reader
1414 .read_stream_projected(
1415 params,
1416 1024,
1417 16,
1418 projection,
1419 FilterExpression::no_filter(),
1420 )
1421 .await
1422 {
1423 Ok(stream) => stream.try_collect::<Vec<RecordBatch>>().await,
1424 Err(e) => Err(e),
1425 }
1426 }
1427 };
1428 let col_values = |batches: &[RecordBatch], idx: usize| -> Vec<Option<i32>> {
1429 batches
1430 .iter()
1431 .flat_map(|b| {
1432 b.column(idx)
1433 .as_any()
1434 .downcast_ref::<Int32Array>()
1435 .unwrap()
1436 .iter()
1437 .collect::<Vec<_>>()
1438 })
1439 .collect()
1440 };
1441
1442 let batches = read(&["a", "b"], ReadBatchParams::RangeFull).await.unwrap();
1444 assert_eq!(
1445 col_values(&batches, 0),
1446 vec![Some(1), Some(2), Some(3), Some(4), Some(5)]
1447 );
1448 assert_eq!(
1449 col_values(&batches, 1),
1450 vec![Some(6), Some(7), Some(8), Some(9), Some(10)]
1451 );
1452
1453 let err = read(&["a", "c"], ReadBatchParams::RangeFull)
1458 .await
1459 .unwrap_err()
1460 .to_string();
1461 assert!(
1462 err.contains("a=5") && err.contains("c=1"),
1463 "error should name each column's length, got: {err}"
1464 );
1465 assert!(
1466 read(&["a", "c"], ReadBatchParams::Range(0..1))
1467 .await
1468 .is_err(),
1469 "even a common-prefix read of unequal-length columns must error"
1470 );
1471
1472 let batches = read(&["c"], ReadBatchParams::RangeFull).await.unwrap();
1474 assert_eq!(col_values(&batches, 0), vec![Some(100)]);
1475 let batches = read(&["a"], ReadBatchParams::RangeFull).await.unwrap();
1476 assert_eq!(
1477 col_values(&batches, 0),
1478 vec![Some(1), Some(2), Some(3), Some(4), Some(5)]
1479 );
1480
1481 let batches = read(&["a"], ReadBatchParams::RangeFrom(2..)).await.unwrap();
1484 assert_eq!(col_values(&batches, 0), vec![Some(3), Some(4), Some(5)]);
1485 let batches = read(&["c"], ReadBatchParams::RangeFrom(0..)).await.unwrap();
1487 assert_eq!(col_values(&batches, 0), vec![Some(100)]);
1488 let batches = read(&["a"], ReadBatchParams::RangeTo(..3)).await.unwrap();
1489 assert_eq!(col_values(&batches, 0), vec![Some(1), Some(2), Some(3)]);
1490 assert!(
1492 read(&["a"], ReadBatchParams::RangeTo(..6)).await.is_err(),
1493 "RangeTo past the column length must error"
1494 );
1495 assert!(
1496 read(&["c"], ReadBatchParams::RangeFrom(2..)).await.is_err(),
1497 "RangeFrom past the column length must error"
1498 );
1499 }
1500
1501 #[rstest]
1507 #[tokio::test]
1508 async fn test_read_nested_columns_under_validation(
1509 #[values(LanceFileVersion::V2_0, LanceFileVersion::V2_1)] version: LanceFileVersion,
1510 ) {
1511 use arrow_array::types::Int32Type;
1512 use arrow_array::{ListArray, StructArray};
1513 use futures::TryStreamExt;
1514 use lance_encoding::decoder::FilterExpression;
1515 use lance_io::ReadBatchParams;
1516
1517 let struct_type = DataType::Struct(
1518 vec![
1519 ArrowField::new("x", DataType::Int32, true),
1520 ArrowField::new("y", DataType::Int32, true),
1521 ]
1522 .into(),
1523 );
1524 let list_type = DataType::List(Arc::new(ArrowField::new("item", DataType::Int32, true)));
1525 let arrow_schema = Arc::new(ArrowSchema::new(vec![
1526 ArrowField::new("a", DataType::Int32, true),
1527 ArrowField::new("s", struct_type, true),
1528 ArrowField::new("lst", list_type, true),
1529 ]));
1530 let lance_schema = LanceSchema::try_from(arrow_schema.as_ref()).unwrap();
1531
1532 let a: ArrayRef = Arc::new(Int32Array::from(vec![1, 2, 3]));
1533 let s: ArrayRef = Arc::new(StructArray::from(vec![
1534 (
1535 Arc::new(ArrowField::new("x", DataType::Int32, true)),
1536 Arc::new(Int32Array::from(vec![10, 20, 30])) as ArrayRef,
1537 ),
1538 (
1539 Arc::new(ArrowField::new("y", DataType::Int32, true)),
1540 Arc::new(Int32Array::from(vec![11, 21, 31])) as ArrayRef,
1541 ),
1542 ]));
1543 let lst: ArrayRef = Arc::new(ListArray::from_iter_primitive::<Int32Type, _, _>(vec![
1545 Some(vec![Some(1), Some(2)]),
1546 Some(vec![Some(3)]),
1547 Some(vec![Some(4), Some(5), Some(6)]),
1548 ]));
1549 let batch = RecordBatch::try_new(arrow_schema.clone(), vec![a, s, lst]).unwrap();
1550
1551 let fs = FsFixture::default();
1552 let options = FileWriterOptions {
1553 format_version: Some(version),
1554 ..Default::default()
1555 };
1556 let mut writer = FileWriter::try_new(
1557 fs.object_store.create(&fs.tmp_path).await.unwrap(),
1558 lance_schema.clone(),
1559 options,
1560 )
1561 .unwrap();
1562 writer.write_batch(&batch).await.unwrap();
1563 writer.finish().await.unwrap();
1564
1565 let file_scheduler = fs
1566 .scheduler
1567 .open_file(&fs.tmp_path, &CachedFileSize::unknown())
1568 .await
1569 .unwrap();
1570 let reader = FileReader::try_open(
1571 file_scheduler,
1572 None,
1573 Arc::<DecoderPlugins>::default(),
1574 &LanceCache::no_cache(),
1575 FileReaderOptions::default(),
1576 )
1577 .await
1578 .unwrap();
1579
1580 for names in [&["a", "s", "lst"][..], &["a", "lst"][..], &["a", "s"][..]] {
1584 let projection =
1585 ReaderProjection::from_column_names(version, &lance_schema, names).unwrap();
1586 let batches: Vec<RecordBatch> = reader
1587 .read_stream_projected(
1588 ReadBatchParams::RangeFull,
1589 1024,
1590 16,
1591 projection,
1592 FilterExpression::no_filter(),
1593 )
1594 .await
1595 .unwrap()
1596 .try_collect()
1597 .await
1598 .unwrap();
1599 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
1600 assert_eq!(
1601 total_rows, 3,
1602 "projection {names:?} should read 3 top-level rows"
1603 );
1604 }
1605 }
1606
1607 #[tokio::test]
1611 async fn test_write_column_validation_errors() {
1612 let fs = FsFixture::default();
1614 let mut lazy_writer = FileWriter::new_lazy(
1615 fs.object_store.create(&fs.tmp_path).await.unwrap(),
1616 FileWriterOptions::default(),
1617 );
1618 let err = lazy_writer
1619 .write_column(0, Arc::new(Int32Array::from(vec![1, 2, 3])))
1620 .await
1621 .unwrap_err()
1622 .to_string();
1623 assert!(
1624 err.contains("explicit schema"),
1625 "expected explicit-schema error, got: {err}"
1626 );
1627
1628 let arrow_schema = Arc::new(ArrowSchema::new(vec![
1629 ArrowField::new("a", DataType::Int32, false),
1630 ArrowField::new("b", DataType::Int32, true),
1631 ]));
1632 let lance_schema = LanceSchema::try_from(arrow_schema.as_ref()).unwrap();
1633
1634 let fs = FsFixture::default();
1636 let mut writer = FileWriter::try_new(
1637 fs.object_store.create(&fs.tmp_path).await.unwrap(),
1638 lance_schema.clone(),
1639 FileWriterOptions::default(),
1640 )
1641 .unwrap();
1642 let err = writer
1643 .write_column(5, Arc::new(Int32Array::from(vec![1])))
1644 .await
1645 .unwrap_err()
1646 .to_string();
1647 assert!(
1648 err.contains('5') && err.contains('2'),
1649 "expected out-of-bounds error naming index 5 and 2 fields, got: {err}"
1650 );
1651
1652 let err = writer
1654 .write_column(0, Arc::new(Int32Array::from(vec![Some(1), None, Some(3)])))
1655 .await
1656 .unwrap_err()
1657 .to_string();
1658 assert!(
1659 err.contains("non-null"),
1660 "expected nullability error, got: {err}"
1661 );
1662 }
1663
1664 #[rstest]
1668 #[tokio::test]
1669 async fn test_blocking_read_unequal_length(
1670 #[values(LanceFileVersion::V2_0, LanceFileVersion::V2_1)] version: LanceFileVersion,
1671 ) {
1672 use lance_encoding::decoder::FilterExpression;
1673 use lance_io::ReadBatchParams;
1674
1675 let arrow_schema = Arc::new(ArrowSchema::new(vec![
1676 ArrowField::new("a", DataType::Int32, true),
1677 ArrowField::new("c", DataType::Int32, true),
1678 ]));
1679 let lance_schema = LanceSchema::try_from(arrow_schema.as_ref()).unwrap();
1680 let fs = FsFixture::default();
1681 let options = FileWriterOptions {
1682 format_version: Some(version),
1683 ..Default::default()
1684 };
1685 let mut writer = FileWriter::try_new(
1686 fs.object_store.create(&fs.tmp_path).await.unwrap(),
1687 lance_schema.clone(),
1688 options,
1689 )
1690 .unwrap();
1691 writer
1692 .write_column(0, Arc::new(Int32Array::from(vec![1, 2, 3, 4, 5])))
1693 .await
1694 .unwrap();
1695 writer
1696 .write_column(1, Arc::new(Int32Array::from(vec![100])))
1697 .await
1698 .unwrap();
1699 writer.finish().await.unwrap();
1700
1701 let file_scheduler = fs
1702 .scheduler
1703 .open_file(&fs.tmp_path, &CachedFileSize::unknown())
1704 .await
1705 .unwrap();
1706 let reader = Arc::new(
1707 FileReader::try_open(
1708 file_scheduler,
1709 None,
1710 Arc::<DecoderPlugins>::default(),
1711 &LanceCache::no_cache(),
1712 FileReaderOptions::default(),
1713 )
1714 .await
1715 .unwrap(),
1716 );
1717
1718 let proj_c = ReaderProjection::from_column_names(version, &lance_schema, &["c"]).unwrap();
1720 let reader_c = reader.clone();
1721 let batches = tokio::task::spawn_blocking(move || {
1722 reader_c
1723 .read_stream_projected_blocking(
1724 ReadBatchParams::RangeFull,
1725 1024,
1726 Some(proj_c),
1727 FilterExpression::no_filter(),
1728 )
1729 .unwrap()
1730 .collect::<std::result::Result<Vec<RecordBatch>, _>>()
1731 .unwrap()
1732 })
1733 .await
1734 .unwrap();
1735 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
1736 assert_eq!(total_rows, 1);
1737
1738 let proj_ac =
1740 ReaderProjection::from_column_names(version, &lance_schema, &["a", "c"]).unwrap();
1741 let reader_ac = reader.clone();
1742 let is_err = tokio::task::spawn_blocking(move || {
1743 reader_ac
1744 .read_stream_projected_blocking(
1745 ReadBatchParams::RangeFull,
1746 1024,
1747 Some(proj_ac),
1748 FilterExpression::no_filter(),
1749 )
1750 .is_err()
1751 })
1752 .await
1753 .unwrap();
1754 assert!(
1755 is_err,
1756 "blocking full scan across unequal-length columns must error"
1757 );
1758 }
1759
1760 #[tokio::test]
1763 async fn test_write_batch_keeps_equal_lengths() {
1764 let arrow_schema = Arc::new(ArrowSchema::new(vec![
1765 ArrowField::new("a", DataType::Int32, true),
1766 ArrowField::new("b", DataType::Int32, true),
1767 ]));
1768 let lance_schema = LanceSchema::try_from(arrow_schema.as_ref()).unwrap();
1769
1770 let fs = FsFixture::default();
1771 let mut writer = FileWriter::try_new(
1772 fs.object_store.create(&fs.tmp_path).await.unwrap(),
1773 lance_schema,
1774 FileWriterOptions::default(),
1775 )
1776 .unwrap();
1777 let batch = RecordBatch::try_new(
1778 arrow_schema.clone(),
1779 vec![
1780 Arc::new(Int32Array::from(vec![1, 2, 3])),
1781 Arc::new(Int32Array::from(vec![4, 5, 6])),
1782 ],
1783 )
1784 .unwrap();
1785 writer.write_batch(&batch).await.unwrap();
1786 let summary = writer.finish().await.unwrap();
1787 assert_eq!(summary.num_rows, 3);
1788
1789 let file_scheduler = fs
1790 .scheduler
1791 .open_file(&fs.tmp_path, &CachedFileSize::unknown())
1792 .await
1793 .unwrap();
1794 let reader = FileReader::try_open(
1795 file_scheduler,
1796 None,
1797 Arc::<DecoderPlugins>::default(),
1798 &LanceCache::no_cache(),
1799 FileReaderOptions::default(),
1800 )
1801 .await
1802 .unwrap();
1803 assert_eq!(reader.column_num_rows(0).unwrap(), 3);
1804 assert_eq!(reader.column_num_rows(1).unwrap(), 3);
1805 }
1806
1807 #[tokio::test]
1808 async fn test_max_page_bytes_enforced() {
1809 let arrow_field = Field::new("data", DataType::UInt64, false);
1810 let arrow_schema = Schema::new(vec![arrow_field]);
1811 let lance_schema = LanceSchema::try_from(&arrow_schema).unwrap();
1812
1813 let data: Vec<u64> = (0..1_000_000).collect();
1815 let array = UInt64Array::from(data);
1816 let batch =
1817 RecordBatch::try_new(arrow_schema.clone().into(), vec![Arc::new(array)]).unwrap();
1818
1819 let options = FileWriterOptions {
1820 max_page_bytes: Some(1024 * 1024), format_version: Some(LanceFileVersion::V2_0),
1823 ..Default::default()
1824 };
1825
1826 let path = TempObjFile::default();
1827 let object_store = ObjectStore::local();
1828 let mut writer = FileWriter::try_new(
1829 object_store.create(&path).await.unwrap(),
1830 lance_schema,
1831 options,
1832 )
1833 .unwrap();
1834
1835 writer.write_batch(&batch).await.unwrap();
1836 writer.finish().await.unwrap();
1837
1838 let fs = FsFixture::default();
1839 let file_scheduler = fs
1840 .scheduler
1841 .open_file(&path, &CachedFileSize::unknown())
1842 .await
1843 .unwrap();
1844 let file_reader = FileReader::try_open(
1845 file_scheduler,
1846 None,
1847 Arc::<DecoderPlugins>::default(),
1848 &LanceCache::no_cache(),
1849 FileReaderOptions::default(),
1850 )
1851 .await
1852 .unwrap();
1853
1854 let column_meta = file_reader.metadata();
1855
1856 let mut total_page_num: u32 = 0;
1857 for (col_idx, col_metadata) in column_meta.column_metadatas.iter().enumerate() {
1858 assert!(
1859 !col_metadata.pages.is_empty(),
1860 "Column {} has no pages",
1861 col_idx
1862 );
1863
1864 for (page_idx, page) in col_metadata.pages.iter().enumerate() {
1865 total_page_num += 1;
1866 let total_size: u64 = page.buffer_sizes.iter().sum();
1867 assert!(
1868 total_size <= 1024 * 1024,
1869 "Column {} Page {} size {} exceeds 1MB limit",
1870 col_idx,
1871 page_idx,
1872 total_size
1873 );
1874 }
1875 }
1876
1877 assert_eq!(total_page_num, 8)
1878 }
1879
1880 #[tokio::test(flavor = "current_thread")]
1881 async fn test_max_page_bytes_env_var() {
1882 let arrow_field = Field::new("data", DataType::UInt64, false);
1883 let arrow_schema = Schema::new(vec![arrow_field]);
1884 let lance_schema = LanceSchema::try_from(&arrow_schema).unwrap();
1885 let data: Vec<u64> = (0..500_000).collect();
1887 let array = UInt64Array::from(data);
1888 let batch =
1889 RecordBatch::try_new(arrow_schema.clone().into(), vec![Arc::new(array)]).unwrap();
1890
1891 unsafe {
1893 std::env::set_var(ENV_LANCE_FILE_WRITER_MAX_PAGE_BYTES, "2097152");
1894 }
1895
1896 let options = FileWriterOptions {
1897 max_page_bytes: None, ..Default::default()
1899 };
1900
1901 let path = TempObjFile::default();
1902 let object_store = ObjectStore::local();
1903 let mut writer = FileWriter::try_new(
1904 object_store.create(&path).await.unwrap(),
1905 lance_schema.clone(),
1906 options,
1907 )
1908 .unwrap();
1909
1910 writer.write_batch(&batch).await.unwrap();
1911 writer.finish().await.unwrap();
1912
1913 let fs = FsFixture::default();
1914 let file_scheduler = fs
1915 .scheduler
1916 .open_file(&path, &CachedFileSize::unknown())
1917 .await
1918 .unwrap();
1919 let file_reader = FileReader::try_open(
1920 file_scheduler,
1921 None,
1922 Arc::<DecoderPlugins>::default(),
1923 &LanceCache::no_cache(),
1924 FileReaderOptions::default(),
1925 )
1926 .await
1927 .unwrap();
1928
1929 for col_metadata in file_reader.metadata().column_metadatas.iter() {
1930 for page in col_metadata.pages.iter() {
1931 let total_size: u64 = page.buffer_sizes.iter().sum();
1932 assert!(
1933 total_size <= 2 * 1024 * 1024,
1934 "Page size {} exceeds 2MB limit",
1935 total_size
1936 );
1937 }
1938 }
1939
1940 unsafe {
1941 std::env::set_var(ENV_LANCE_FILE_WRITER_MAX_PAGE_BYTES, "");
1942 }
1943 }
1944
1945 #[tokio::test]
1946 async fn test_compression_overrides_end_to_end() {
1947 let arrow_schema = Arc::new(ArrowSchema::new(vec![
1949 ArrowField::new("customer_id", DataType::Int32, false),
1950 ArrowField::new("product_id", DataType::Int32, false),
1951 ArrowField::new("quantity", DataType::Int32, false),
1952 ArrowField::new("price", DataType::Float32, false),
1953 ArrowField::new("description", DataType::Utf8, false),
1954 ]));
1955
1956 let lance_schema = LanceSchema::try_from(arrow_schema.as_ref()).unwrap();
1957
1958 let mut customer_ids = Int32Builder::new();
1960 let mut product_ids = Int32Builder::new();
1961 let mut quantities = Int32Builder::new();
1962 let mut prices = Float32Builder::new();
1963 let mut descriptions = Vec::new();
1964
1965 for i in 0..10000 {
1972 customer_ids.append_value(i / 100);
1975
1976 product_ids.append_value(i / 2000);
1978
1979 quantities.append_value(if i % 10 == 0 { 5 } else { 1 });
1981
1982 prices.append_value(match i % 3 {
1984 0 => 9.99,
1985 1 => 19.99,
1986 _ => 29.99,
1987 });
1988
1989 descriptions.push(format!("Product {}", i / 2000));
1991 }
1992
1993 let batch = RecordBatch::try_new(
1994 arrow_schema.clone(),
1995 vec![
1996 Arc::new(customer_ids.finish()),
1997 Arc::new(product_ids.finish()),
1998 Arc::new(quantities.finish()),
1999 Arc::new(prices.finish()),
2000 Arc::new(StringArray::from(descriptions)),
2001 ],
2002 )
2003 .unwrap();
2004
2005 let mut params = CompressionParams::new();
2007
2008 params.columns.insert(
2010 "*_id".to_string(),
2011 CompressionFieldParams {
2012 rle_threshold: Some(0.5), compression: None, compression_level: None,
2015 bss: Some(lance_encoding::compression_config::BssMode::Off), minichunk_size: None,
2017 },
2018 );
2019
2020 let encoding_strategy =
2025 super::encoding_strategy_with_params(LanceFileVersion::V2_1, params).unwrap();
2026
2027 let options = FileWriterOptions {
2029 encoding_strategy: Some(encoding_strategy),
2030 format_version: Some(LanceFileVersion::V2_1),
2031 max_page_bytes: Some(64 * 1024), ..Default::default()
2033 };
2034
2035 let path = TempObjFile::default();
2037 let object_store = ObjectStore::local();
2038
2039 let mut writer = FileWriter::try_new(
2040 object_store.create(&path).await.unwrap(),
2041 lance_schema.clone(),
2042 options,
2043 )
2044 .unwrap();
2045
2046 writer.write_batch(&batch).await.unwrap();
2047 writer.add_schema_metadata("compression_test", "configured_compression");
2048 writer.finish().await.unwrap();
2049
2050 let path_no_compression = TempObjFile::default();
2052 let default_options = FileWriterOptions {
2053 format_version: Some(LanceFileVersion::V2_1),
2054 max_page_bytes: Some(64 * 1024),
2055 ..Default::default()
2056 };
2057
2058 let mut writer_no_compression = FileWriter::try_new(
2059 object_store.create(&path_no_compression).await.unwrap(),
2060 lance_schema.clone(),
2061 default_options,
2062 )
2063 .unwrap();
2064
2065 writer_no_compression.write_batch(&batch).await.unwrap();
2066 writer_no_compression.finish().await.unwrap();
2067
2068 let fs = FsFixture::default();
2074 let file_scheduler = fs
2075 .scheduler
2076 .open_file(&path, &CachedFileSize::unknown())
2077 .await
2078 .unwrap();
2079
2080 let file_reader = FileReader::try_open(
2081 file_scheduler,
2082 None,
2083 Arc::<DecoderPlugins>::default(),
2084 &LanceCache::no_cache(),
2085 FileReaderOptions::default(),
2086 )
2087 .await
2088 .unwrap();
2089
2090 let metadata = file_reader.metadata();
2092 assert_eq!(metadata.major_version, 2);
2093 assert_eq!(metadata.minor_version, 1);
2094
2095 let schema = file_reader.schema();
2096 assert_eq!(
2097 schema.metadata.get("compression_test"),
2098 Some(&"configured_compression".to_string())
2099 );
2100
2101 let column_metadatas = &metadata.column_metadatas;
2103
2104 assert!(!column_metadatas[0].pages.is_empty());
2106 let customer_id_encoding = describe_encoding(&column_metadatas[0].pages[0]);
2107 assert!(
2108 customer_id_encoding.contains("RLE") || customer_id_encoding.contains("Rle"),
2109 "customer_id column should use RLE encoding due to '*_id' pattern match, but got: {}",
2110 customer_id_encoding
2111 );
2112
2113 assert!(!column_metadatas[1].pages.is_empty());
2115 let product_id_encoding = describe_encoding(&column_metadatas[1].pages[0]);
2116 assert!(
2117 product_id_encoding.contains("RLE") || product_id_encoding.contains("Rle"),
2118 "product_id column should use RLE encoding due to '*_id' pattern match, but got: {}",
2119 product_id_encoding
2120 );
2121 }
2122
2123 #[tokio::test]
2124 async fn test_field_metadata_compression() {
2125 let mut metadata = HashMap::new();
2127 metadata.insert(
2128 lance_encoding::constants::COMPRESSION_META_KEY.to_string(),
2129 "zstd".to_string(),
2130 );
2131 metadata.insert(
2132 lance_encoding::constants::COMPRESSION_LEVEL_META_KEY.to_string(),
2133 "6".to_string(),
2134 );
2135
2136 let arrow_schema = Arc::new(ArrowSchema::new(vec![
2137 ArrowField::new("id", DataType::Int32, false),
2138 ArrowField::new("text", DataType::Utf8, false).with_metadata(metadata.clone()),
2139 ArrowField::new("data", DataType::Int32, false).with_metadata(HashMap::from([(
2140 lance_encoding::constants::COMPRESSION_META_KEY.to_string(),
2141 "none".to_string(),
2142 )])),
2143 ]));
2144
2145 let lance_schema = LanceSchema::try_from(arrow_schema.as_ref()).unwrap();
2146
2147 let id_array = Int32Array::from_iter_values(0..1000);
2149 let text_array = StringArray::from_iter_values(
2150 (0..1000).map(|i| format!("test string {} repeated text", i)),
2151 );
2152 let data_array = Int32Array::from_iter_values((0..1000).map(|i| i * 2));
2153
2154 let batch = RecordBatch::try_new(
2155 arrow_schema.clone(),
2156 vec![
2157 Arc::new(id_array),
2158 Arc::new(text_array),
2159 Arc::new(data_array),
2160 ],
2161 )
2162 .unwrap();
2163
2164 let path = TempObjFile::default();
2165 let object_store = ObjectStore::local();
2166
2167 let params = CompressionParams::new();
2169 let encoding_strategy =
2170 super::encoding_strategy_with_params(LanceFileVersion::V2_1, params).unwrap();
2171
2172 let options = FileWriterOptions {
2173 encoding_strategy: Some(encoding_strategy),
2174 format_version: Some(LanceFileVersion::V2_1),
2175 ..Default::default()
2176 };
2177 let mut writer = FileWriter::try_new(
2178 object_store.create(&path).await.unwrap(),
2179 lance_schema.clone(),
2180 options,
2181 )
2182 .unwrap();
2183
2184 writer.write_batch(&batch).await.unwrap();
2185 writer.finish().await.unwrap();
2186
2187 let fs = FsFixture::default();
2189 let file_scheduler = fs
2190 .scheduler
2191 .open_file(&path, &CachedFileSize::unknown())
2192 .await
2193 .unwrap();
2194 let file_reader = FileReader::try_open(
2195 file_scheduler,
2196 None,
2197 Arc::<DecoderPlugins>::default(),
2198 &LanceCache::no_cache(),
2199 FileReaderOptions::default(),
2200 )
2201 .await
2202 .unwrap();
2203
2204 let column_metadatas = &file_reader.metadata().column_metadatas;
2205
2206 let text_encoding = describe_encoding(&column_metadatas[1].pages[0]);
2208 assert!(
2210 text_encoding.contains("Zstd"),
2211 "text column should use zstd compression from field metadata, but got: {}",
2212 text_encoding
2213 );
2214
2215 let data_encoding = describe_encoding(&column_metadatas[2].pages[0]);
2217 assert!(
2219 data_encoding.contains("Flat") && data_encoding.contains("compression: None"),
2220 "data column should use no compression from field metadata, but got: {}",
2221 data_encoding
2222 );
2223 }
2224
2225 #[tokio::test]
2226 async fn test_field_metadata_rle_threshold() {
2227 let mut metadata = HashMap::new();
2229 metadata.insert(
2230 lance_encoding::constants::RLE_THRESHOLD_META_KEY.to_string(),
2231 "0.9".to_string(),
2232 );
2233 metadata.insert(
2235 lance_encoding::constants::COMPRESSION_META_KEY.to_string(),
2236 "lz4".to_string(),
2237 );
2238 metadata.insert(
2240 lance_encoding::constants::BSS_META_KEY.to_string(),
2241 "off".to_string(),
2242 );
2243
2244 let arrow_schema = Arc::new(ArrowSchema::new(vec![
2245 ArrowField::new("status", DataType::Int32, false).with_metadata(metadata),
2246 ]));
2247
2248 let lance_schema = LanceSchema::try_from(arrow_schema.as_ref()).unwrap();
2249
2250 let status_array = Int32Array::from_iter_values(
2252 std::iter::repeat_n(200, 8000)
2253 .chain(std::iter::repeat_n(404, 1500))
2254 .chain(std::iter::repeat_n(500, 500)),
2255 );
2256
2257 let batch =
2258 RecordBatch::try_new(arrow_schema.clone(), vec![Arc::new(status_array)]).unwrap();
2259
2260 let path = TempObjFile::default();
2261 let object_store = ObjectStore::local();
2262
2263 let params = CompressionParams::new();
2265 let encoding_strategy =
2266 super::encoding_strategy_with_params(LanceFileVersion::V2_1, params).unwrap();
2267
2268 let options = FileWriterOptions {
2269 encoding_strategy: Some(encoding_strategy),
2270 format_version: Some(LanceFileVersion::V2_1),
2271 ..Default::default()
2272 };
2273 let mut writer = FileWriter::try_new(
2274 object_store.create(&path).await.unwrap(),
2275 lance_schema.clone(),
2276 options,
2277 )
2278 .unwrap();
2279
2280 writer.write_batch(&batch).await.unwrap();
2281 writer.finish().await.unwrap();
2282
2283 let fs = FsFixture::default();
2285 let file_scheduler = fs
2286 .scheduler
2287 .open_file(&path, &CachedFileSize::unknown())
2288 .await
2289 .unwrap();
2290 let file_reader = FileReader::try_open(
2291 file_scheduler,
2292 None,
2293 Arc::<DecoderPlugins>::default(),
2294 &LanceCache::no_cache(),
2295 FileReaderOptions::default(),
2296 )
2297 .await
2298 .unwrap();
2299
2300 let column_metadatas = &file_reader.metadata().column_metadatas;
2301 let status_encoding = describe_encoding(&column_metadatas[0].pages[0]);
2302 assert!(
2303 status_encoding.contains("RLE") || status_encoding.contains("Rle"),
2304 "status column should use RLE encoding due to metadata threshold, but got: {}",
2305 status_encoding
2306 );
2307 }
2308
2309 #[tokio::test]
2310 async fn test_large_page_split_on_read() {
2311 use arrow_array::Array;
2312 use futures::TryStreamExt;
2313 use lance_encoding::decoder::FilterExpression;
2314 use lance_io::ReadBatchParams;
2315
2316 let arrow_field = ArrowField::new("data", DataType::Binary, false);
2319 let arrow_schema = ArrowSchema::new(vec![arrow_field]);
2320 let lance_schema = LanceSchema::try_from(&arrow_schema).unwrap();
2321
2322 let large_value = vec![42u8; 40 * 1024 * 1024];
2324 let array = arrow_array::BinaryArray::from(vec![
2325 Some(large_value.as_slice()),
2326 Some(b"small value"),
2327 ]);
2328 let batch = RecordBatch::try_new(Arc::new(arrow_schema), vec![Arc::new(array)]).unwrap();
2329
2330 let options = FileWriterOptions {
2332 max_page_bytes: Some(128 * 1024 * 1024),
2333 format_version: Some(LanceFileVersion::V2_1),
2334 ..Default::default()
2335 };
2336
2337 let fs = FsFixture::default();
2338 let path = fs.tmp_path;
2339
2340 let mut writer = FileWriter::try_new(
2341 fs.object_store.create(&path).await.unwrap(),
2342 lance_schema.clone(),
2343 options,
2344 )
2345 .unwrap();
2346
2347 writer.write_batch(&batch).await.unwrap();
2348 let write_summary = writer.finish().await.unwrap();
2349 assert_eq!(write_summary.num_rows, 2);
2350 assert_eq!(
2351 write_summary.size_bytes,
2352 fs.object_store.size(&path).await.unwrap()
2353 );
2354
2355 let file_scheduler = fs
2357 .scheduler
2358 .open_file(&path, &CachedFileSize::unknown())
2359 .await
2360 .unwrap();
2361
2362 let reader_options = FileReaderOptions {
2364 read_chunk_size: 10 * 1024 * 1024, ..Default::default()
2366 };
2367
2368 let file_reader = FileReader::try_open(
2369 file_scheduler,
2370 None,
2371 Arc::<DecoderPlugins>::default(),
2372 &LanceCache::no_cache(),
2373 reader_options,
2374 )
2375 .await
2376 .unwrap();
2377
2378 let stream = file_reader
2380 .read_stream(
2381 ReadBatchParams::RangeFull,
2382 1024,
2383 10, FilterExpression::no_filter(),
2385 )
2386 .await
2387 .unwrap();
2388
2389 let batches: Vec<RecordBatch> = stream.try_collect().await.unwrap();
2390 assert_eq!(batches.len(), 1);
2391
2392 let read_array = batches[0].column(0);
2394 let read_binary = read_array
2395 .as_any()
2396 .downcast_ref::<arrow_array::BinaryArray>()
2397 .unwrap();
2398
2399 assert_eq!(read_binary.len(), 2);
2400 assert_eq!(read_binary.value(0).len(), 40 * 1024 * 1024);
2401 assert_eq!(read_binary.value(1), b"small value");
2402
2403 assert!(read_binary.value(0).iter().all(|&b| b == 42u8));
2405 }
2406
2407 fn spill_config() -> (TempObjFile, Arc<ObjectStore>) {
2408 let spill_path = TempObjFile::default();
2409 (spill_path, Arc::new(ObjectStore::local()))
2410 }
2411
2412 fn make_batches(num_batches: i32, num_cols: usize, rows_per_batch: i32) -> Vec<RecordBatch> {
2413 let fields: Vec<_> = (0..num_cols)
2414 .map(|c| ArrowField::new(format!("c{c}"), DataType::Int32, false))
2415 .collect();
2416 let schema = Arc::new(ArrowSchema::new(fields));
2417 (0..num_batches)
2418 .map(|i| {
2419 let cols: Vec<Arc<dyn arrow_array::Array>> = (0..num_cols)
2420 .map(|c| {
2421 let start = (i * rows_per_batch + c as i32) * 100;
2422 Arc::new(Int32Array::from_iter_values(start..start + rows_per_batch))
2423 as Arc<dyn arrow_array::Array>
2424 })
2425 .collect();
2426 RecordBatch::try_new(schema.clone(), cols).unwrap()
2427 })
2428 .collect()
2429 }
2430
2431 async fn write_and_read_batches(
2432 batches: &[RecordBatch],
2433 spill: Option<(Arc<ObjectStore>, object_store::path::Path)>,
2434 ) -> Vec<RecordBatch> {
2435 let fs = FsFixture::default();
2436 let lance_schema = LanceSchema::try_from(batches[0].schema().as_ref()).unwrap();
2437 let writer = fs.object_store.create(&fs.tmp_path).await.unwrap();
2438 let mut file_writer =
2439 FileWriter::try_new(writer, lance_schema, FileWriterOptions::default()).unwrap();
2440 if let Some((store, path)) = spill {
2441 file_writer = file_writer.with_page_metadata_spill(store, path);
2442 }
2443 for batch in batches {
2444 file_writer.write_batch(batch).await.unwrap();
2445 }
2446 file_writer.add_schema_metadata("foo", "bar");
2447 file_writer.finish().await.unwrap();
2448
2449 crate::testing::read_lance_file(
2450 &fs,
2451 Arc::<DecoderPlugins>::default(),
2452 lance_encoding::decoder::FilterExpression::no_filter(),
2453 )
2454 .await
2455 }
2456
2457 #[rstest::rstest]
2458 #[case::multi_col(20, 2, 100)]
2459 #[case::many_batches(50, 2, 100)]
2460 #[tokio::test]
2461 async fn test_page_metadata_spill_roundtrip(
2462 #[case] num_batches: i32,
2463 #[case] num_cols: usize,
2464 #[case] rows_per_batch: i32,
2465 ) {
2466 let batches = make_batches(num_batches, num_cols, rows_per_batch);
2467 let baseline = write_and_read_batches(&batches, None).await;
2468 let (spill_path, spill_store) = spill_config();
2469 let spilled =
2470 write_and_read_batches(&batches, Some((spill_store, spill_path.as_ref().clone())))
2471 .await;
2472 assert_eq!(baseline, spilled);
2473 }
2474
2475 #[tokio::test]
2476 async fn test_page_metadata_spill_many_columns() {
2477 let batches = make_batches(10, 500, 100);
2479 let baseline = write_and_read_batches(&batches, None).await;
2480 let (spill_path, spill_store) = spill_config();
2481 let spilled =
2482 write_and_read_batches(&batches, Some((spill_store, spill_path.as_ref().clone())))
2483 .await;
2484 assert_eq!(baseline, spilled);
2485 }
2486}