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