1use std::collections::{HashSet, VecDeque};
4use std::pin::pin;
5use std::sync::Arc;
6
7use arrow::array::{RecordBatch, RecordBatchIterator};
8use arrow::datatypes::{FieldRef, Schema, SchemaRef};
9use futures::{FutureExt, Stream};
10use lance::Dataset;
11use lance::dataset::fragment::write::FragmentCreateBuilder;
12use lance::dataset::transaction::{Operation, Transaction};
13use lance::dataset::{CommitBuilder, NewColumnTransform, WriteMode as LanceWriteMode, WriteParams};
14use lance::table::format::Fragment;
15use lance_core::datatypes::{OnMissing, OnTypeMismatch, Schema as LanceSchema};
16use nd_arrow_array::NdRecordBatch;
17use nd_arrow_array::extension::{GLOBAL_ATTRIBUTES_FIELD, is_global_attributes_field};
18use tokio::task::JoinHandle;
19
20use crate::cast::Casting;
21use crate::encode::{RowDecoder, encode_rows};
22use crate::error::{Error, Result};
23use crate::format::check_format;
24use crate::options::ColumnOptions;
25use crate::schema::{concat_conformed, holds_schema, merge_schemas, missing_fields, used_fields};
26use crate::stats::{STATS_PREFIX, is_stats_name};
27use crate::store::{StorageOptions, open_dataset, store_params};
28use crate::widen::{promotions, widen};
29use crate::write::{
30 FILE_VERSION, WriteMode, WriteOptions, next_group, prepare_batch, table_defaults,
31 table_has_stats,
32};
33
34pub struct NdWriter {
54 uri: String,
55 base: Option<Dataset>,
57 overwrite: bool,
59 defaults: ColumnOptions,
60 stats: bool,
62 options: WriteOptions,
63 schema: Option<(Schema, LanceSchema)>,
65 buffer: Vec<RecordBatch>,
67 buffered_bytes: usize,
68 pending: VecDeque<(JoinHandle<Result<Vec<Fragment>>>, usize)>,
71 fragments: Vec<Fragment>,
72 written: DiskRatio,
74 failed: Option<Arc<Error>>,
76}
77
78impl NdWriter {
79 #[tracing::instrument(skip_all, fields(uri = %uri, mode = ?options.mode), err)]
85 pub async fn create(uri: &str, options: &WriteOptions) -> Result<Self> {
86 let existing = open_existing(uri, &options.storage_options).await?;
87 let overwrite = match options.mode {
88 WriteMode::Create if existing.is_some() => {
89 return Err(Error::TableExists(uri.to_string()));
90 }
91 WriteMode::Create => false,
92 WriteMode::Overwrite => true,
93 WriteMode::Append => {
94 return Err(Error::InvalidArgument(
95 "use NdWriter::append to append to a table".to_string(),
96 ));
97 }
98 };
99 Ok(Self {
100 uri: uri.to_string(),
101 base: existing,
102 overwrite,
103 defaults: options.column_defaults.clone().unwrap_or_default(),
104 stats: options.stats,
105 options: options.clone(),
106 schema: None,
107 buffer: Vec::new(),
108 buffered_bytes: 0,
109 pending: VecDeque::new(),
110 fragments: Vec::new(),
111 written: DiskRatio::default(),
112 failed: None,
113 })
114 }
115
116 #[tracing::instrument(skip_all, fields(uri = %uri), err)]
119 pub async fn append(uri: &str, options: &WriteOptions) -> Result<Self> {
120 let dataset = open_existing(uri, &options.storage_options)
121 .await?
122 .ok_or_else(|| Error::TableNotFound(uri.to_string()))?;
123 check_format(&dataset)?;
124 let defaults = table_defaults(&dataset, options)?;
125 let stats = table_has_stats(&dataset);
126 let schema = (Schema::from(dataset.schema()), dataset.schema().clone());
127 Ok(Self {
128 uri: uri.to_string(),
129 base: Some(dataset),
130 overwrite: false,
131 defaults,
132 stats,
133 options: options.clone(),
134 schema: Some(schema),
135 buffer: Vec::new(),
136 buffered_bytes: 0,
137 pending: VecDeque::new(),
138 fragments: Vec::new(),
139 written: DiskRatio::default(),
140 failed: None,
141 })
142 }
143
144 pub async fn write(&mut self, rows: impl Stream<Item = Result<NdRecordBatch>>) -> Result<()> {
152 self.check()?;
153 let mut rows = pin!(rows);
154 while let Some(group) = next_group(&mut rows, &self.options).await? {
155 let batch = encode_rows(&group)?;
156 self.promote(&batch).await?;
157 let table = self.schema.as_ref().map(|(arrow, _)| arrow);
158 let batch = prepare_batch(batch, &self.defaults, &self.options, self.stats, table)?;
159 self.push(batch).await?;
160 }
161 Ok(())
162 }
163
164 pub async fn write_batch(&mut self, batch: RecordBatch) -> Result<()> {
172 self.check()?;
173 self.write_encoded(batch).await
174 }
175
176 fn check(&self) -> Result<()> {
177 match &self.failed {
178 Some(error) => Err(Error::WriterFailed(error.clone())),
179 None => Ok(()),
180 }
181 }
182
183 fn fail_on(&mut self, result: Result<()>) -> Result<()> {
184 result.map_err(|error| {
185 let error = Arc::new(error);
186 self.failed = Some(error.clone());
187 Error::WriterFailed(error)
188 })
189 }
190
191 async fn write_encoded(&mut self, batch: RecordBatch) -> Result<()> {
192 if let Some(field) = batch.schema().fields().iter().find(|field| {
193 is_stats_name(field.name())
194 || (field.name() == GLOBAL_ATTRIBUTES_FIELD && !is_global_attributes_field(field))
195 }) {
196 return Err(Error::SchemaMismatch(format!(
197 "variable name '{}' is reserved, also each name that starts with {STATS_PREFIX}",
198 field.name()
199 )));
200 }
201 RowDecoder::try_new(&batch)?;
203 if batch.num_rows() == 0 {
204 return Ok(());
205 }
206 let batch = nullable_global_attributes(batch)?;
207 self.promote(&batch).await?;
208 let table = self.schema.as_ref().map(|(arrow, _)| arrow);
209 let batch = prepare_batch(batch, &self.defaults, &self.options, self.stats, table)?;
210 self.push(batch).await
211 }
212
213 async fn promote(&mut self, batch: &RecordBatch) -> Result<()> {
217 if self.options.casting != Casting::Promote {
218 return Ok(());
219 }
220 let Some((arrow, _)) = &self.schema else {
221 return Ok(());
222 };
223 let widenings = promotions(batch, arrow);
224 if widenings.is_empty() {
225 return Ok(());
226 }
227 let result = async {
228 let staged =
229 !(self.buffer.is_empty() && self.pending.is_empty() && self.fragments.is_empty());
230 if staged || self.base.is_none() {
231 self.commit().await?;
232 }
233 let dataset = self.base.as_mut().expect("a committed table");
234 for (variable, to) in &widenings {
235 widen(dataset, variable, to).boxed().await?;
236 }
237 self.schema = Some((Schema::from(dataset.schema()), dataset.schema().clone()));
238 Ok(())
239 }
240 .await;
241 self.fail_on(result)
242 }
243
244 async fn push(&mut self, batch: RecordBatch) -> Result<()> {
250 let grows = match &self.schema {
253 Some((arrow, _)) => !holds_schema(arrow, &batch.schema()),
254 None => true,
255 };
256 if grows {
257 let (arrow, lance) = match &self.schema {
258 Some((arrow, lance)) => (
259 merge_schemas(arrow, &batch.schema())?,
260 lance.merge(batch.schema().as_ref())?,
261 ),
262 None => (
263 batch.schema().as_ref().clone(),
264 LanceSchema::try_from(batch.schema().as_ref())?,
265 ),
266 };
267 let mut lance = lance;
268 lance.set_field_id(self.base_max_field_id());
269 self.schema = Some((arrow, lance));
270 }
271 self.buffered_bytes += batch.get_array_memory_size();
272 self.buffer.push(batch);
273 if self.written.on_disk(self.buffered_bytes) >= self.options.target_fragment_bytes as f64 {
274 let result = self.flush().boxed().await;
275 return self.fail_on(result);
276 }
277 Ok(())
278 }
279
280 #[tracing::instrument(level = "debug", skip_all, fields(rows = self.buffer.len(), bytes = self.buffered_bytes))]
283 async fn flush(&mut self) -> Result<()> {
284 if self.buffer.is_empty() {
285 return Ok(());
286 }
287 self.evolve().await?;
288 let batches = std::mem::take(&mut self.buffer);
289 let memory = std::mem::take(&mut self.buffered_bytes);
290 let (arrow, lance) = self.schema.as_ref().expect("a buffered row has a schema");
291 let schemas: Vec<SchemaRef> = batches
294 .iter()
295 .map(|batch| batch.schema())
296 .collect::<HashSet<_>>()
297 .into_iter()
298 .collect();
299 let present = Arc::new(used_fields(arrow, &schemas));
300 let batch = concat_conformed(&present, &batches)?;
303 let projected =
304 lance.project_by_schema(present.as_ref(), OnMissing::Error, OnTypeMismatch::Error)?;
305 let uri = self.uri.clone();
306 let store_params = store_params(&self.options.storage_options);
307 let task = tokio::spawn(async move {
308 let params = WriteParams {
309 mode: LanceWriteMode::Append,
310 data_storage_version: Some(FILE_VERSION),
311 store_params,
312 ..Default::default()
313 };
314 let reader = RecordBatchIterator::new([Ok(batch)], present);
315 Ok(FragmentCreateBuilder::new(&uri)
316 .schema(&projected)
317 .write_params(¶ms)
318 .write_fragments(reader)
319 .await?)
320 });
321 self.pending.push_back((task, memory));
322 while self.pending.len() > self.options.write_parallelism.max(1) {
323 self.wait_oldest().await?;
324 }
325 Ok(())
326 }
327
328 async fn evolve(&mut self) -> Result<()> {
336 let (Some(base), Some((arrow, _))) = (self.base.as_mut(), self.schema.as_ref()) else {
337 return Ok(());
338 };
339 if self.overwrite {
340 return Ok(());
341 }
342 let missing = missing_fields(Schema::from(base.schema()).fields(), arrow.fields());
343 if missing.is_empty() {
344 return Ok(());
345 }
346 base.add_columns(
349 NewColumnTransform::AllNulls(Arc::new(Schema::new(missing))),
350 None,
351 None,
352 )
353 .boxed()
354 .await?;
355 self.schema = Some((Schema::from(base.schema()), base.schema().clone()));
356 Ok(())
357 }
358
359 async fn wait_oldest(&mut self) -> Result<()> {
361 let Some((task, memory)) = self.pending.pop_front() else {
362 return Ok(());
363 };
364 let fragments = task.await??;
365 self.written.add(memory, &fragments);
366 self.fragments.extend(fragments);
367 Ok(())
368 }
369
370 fn base_max_field_id(&self) -> Option<i32> {
372 match (&self.base, self.overwrite) {
373 (Some(dataset), false) => Some(dataset.manifest.max_field_id()),
374 _ => None,
375 }
376 }
377
378 #[tracing::instrument(skip_all, fields(uri = %self.uri), err)]
383 pub async fn finish(mut self) -> Result<Dataset> {
384 if let Some(error) = &self.failed {
385 return Err(Error::WriterFailed(error.clone()));
386 }
387 self.commit().boxed().await
390 }
391
392 #[tracing::instrument(level = "debug", skip_all, fields(uri = %self.uri))]
394 async fn commit(&mut self) -> Result<Dataset> {
395 self.flush().boxed().await?;
396 while !self.pending.is_empty() {
397 self.wait_oldest().await?;
398 }
399 let Some((_, schema)) = self.schema.clone().filter(|_| !self.fragments.is_empty()) else {
400 return match (&self.base, self.overwrite) {
401 (Some(dataset), false) => Ok(dataset.clone()),
402 _ => Err(Error::InvalidArgument(format!(
403 "no rows to create the table at {}",
404 self.uri
405 ))),
406 };
407 };
408 let fragments = std::mem::take(&mut self.fragments);
409 let dataset = match (self.base.take(), self.overwrite) {
410 (None, overwrite) => {
411 let storage_options = &self.options.storage_options;
413 if !overwrite && open_existing(&self.uri, storage_options).await?.is_some() {
414 return Err(Error::TableExists(self.uri.clone()));
415 }
416 let operation = overwrite_operation(numbered(fragments, 0), schema);
417 let mut builder =
418 CommitBuilder::new(self.uri.as_str()).with_storage_format(FILE_VERSION);
419 if let Some(params) = store_params(storage_options) {
420 builder = builder.with_store_params(params);
421 }
422 builder
423 .execute(Transaction::new(0, operation, None))
424 .await?
425 }
426 (Some(base), true) => {
427 let version = base.version().version;
428 let operation = overwrite_operation(numbered(fragments, 0), schema);
429 CommitBuilder::new(Arc::new(base))
430 .execute(Transaction::new(version, operation, None))
431 .await?
432 }
433 (Some(base), false) if schema.max_field_id() == base.schema().max_field_id() => {
435 commit_rows(base, fragments, &schema).boxed().await?
436 }
437 (Some(base), false) => {
438 let version = base.version().version;
439 let mut all: Vec<Fragment> = base
440 .get_fragments()
441 .iter()
442 .map(|f| f.metadata().clone())
443 .collect();
444 let first_id = base.manifest.max_fragment_id().map_or(0, |id| id + 1);
445 all.extend(numbered(fragments, first_id));
446 let operation = Operation::Merge {
448 fragments: all,
449 schema,
450 preserves_nullability: true,
451 };
452 CommitBuilder::new(Arc::new(base))
453 .execute(Transaction::new(version, operation, None))
454 .await?
455 }
456 };
457 tracing::info!(version = dataset.version().version, "committed the rows");
458 self.base = Some(dataset.clone());
459 self.overwrite = false;
460 self.schema = Some((Schema::from(dataset.schema()), dataset.schema().clone()));
461 Ok(dataset)
462 }
463
464 pub fn abort(self) {}
467}
468
469impl Drop for NdWriter {
470 fn drop(&mut self) {
472 for (task, _) in &self.pending {
473 task.abort();
474 }
475 }
476}
477
478fn nullable_global_attributes(batch: RecordBatch) -> Result<RecordBatch> {
482 let schema = batch.schema();
483 let Some((index, field)) = schema.column_with_name(GLOBAL_ATTRIBUTES_FIELD) else {
484 return Ok(batch);
485 };
486 if field.is_nullable() {
487 return Ok(batch);
488 }
489 let mut fields: Vec<FieldRef> = schema.fields().iter().cloned().collect();
490 fields[index] = Arc::new(field.clone().with_nullable(true));
491 let schema = Schema::new_with_metadata(fields, schema.metadata().clone());
492 Ok(RecordBatch::try_new(
493 Arc::new(schema),
494 batch.columns().to_vec(),
495 )?)
496}
497
498#[derive(Debug, Default)]
501struct DiskRatio {
502 memory: u64,
503 disk: u64,
504}
505
506impl DiskRatio {
507 const MIN: f64 = 1.0 / 8.0;
510
511 fn add(&mut self, memory: usize, fragments: &[Fragment]) {
514 let files = fragments.iter().flat_map(|fragment| fragment.files.iter());
515 let sizes: Option<Vec<u64>> = files
516 .map(|file| file.file_size_bytes.get().map(|size| size.get()))
517 .collect();
518 if let Some(sizes) = sizes {
519 self.memory += memory as u64;
520 self.disk += sizes.iter().sum::<u64>();
521 }
522 }
523
524 fn on_disk(&self, memory: usize) -> f64 {
527 let ratio = if self.memory == 0 {
528 1.0
529 } else {
530 (self.disk as f64 / self.memory as f64).max(Self::MIN)
531 };
532 memory as f64 * ratio
533 }
534}
535
536const MAX_COMMIT_ATTEMPTS: usize = 10;
538
539#[tracing::instrument(level = "debug", skip_all, fields(fragments = fragments.len()))]
546async fn commit_rows(
547 mut base: Dataset,
548 fragments: Vec<Fragment>,
549 staged: &LanceSchema,
550) -> Result<Dataset> {
551 for attempt in 1..=MAX_COMMIT_ATTEMPTS {
552 check_fields(&base, &fragments, staged)?;
553 let operation = Operation::Update {
554 removed_fragment_ids: Vec::new(),
555 updated_fragments: Vec::new(),
556 new_fragments: fragments.clone(),
557 fields_modified: Vec::new(),
558 compacted_sstables: Vec::new(),
559 fields_for_preserving_frag_bitmap: Vec::new(),
560 update_mode: None,
561 inserted_rows_filter: None,
562 updated_fragment_offsets: None,
563 };
564 let version = base.version().version;
565 match CommitBuilder::new(Arc::new(base.clone()))
566 .execute(Transaction::new(version, operation, None))
567 .await
568 {
569 Ok(dataset) => return Ok(dataset),
570 Err(lance::Error::RetryableCommitConflict { .. }) if attempt < MAX_COMMIT_ATTEMPTS => {
571 tracing::debug!(attempt, "a concurrent commit preempted the rows, try again");
572 base.checkout_latest().await?;
573 }
574 Err(error) => return Err(error.into()),
575 }
576 }
577 unreachable!("the last attempt returns")
578}
579
580fn check_fields(table: &Dataset, fragments: &[Fragment], staged: &LanceSchema) -> Result<()> {
584 let ids: HashSet<i32> = table.schema().field_ids().into_iter().collect();
585 let lost = fragments
586 .iter()
587 .flat_map(|fragment| fragment.files.iter())
588 .flat_map(|file| file.fields.iter())
589 .find(|id| **id >= 0 && !ids.contains(*id));
590 match lost {
591 Some(id) => Err(Error::SchemaMismatch(format!(
592 "the table changed or dropped '{}' during the write, nothing is committed",
593 staged
594 .field_path(*id)
595 .unwrap_or_else(|_| format!("field {id}"))
596 ))),
597 None => Ok(()),
598 }
599}
600
601fn numbered(mut fragments: Vec<Fragment>, first_id: u64) -> Vec<Fragment> {
603 for (id, fragment) in (first_id..).zip(&mut fragments) {
604 fragment.id = id;
605 }
606 fragments
607}
608
609fn overwrite_operation(fragments: Vec<Fragment>, schema: LanceSchema) -> Operation {
610 Operation::Overwrite {
611 fragments,
612 schema,
613 config_upsert_values: None,
614 initial_bases: None,
615 }
616}
617
618pub(crate) async fn open_existing(uri: &str, options: &StorageOptions) -> Result<Option<Dataset>> {
620 match open_dataset(uri, options).await {
621 Ok(dataset) => Ok(Some(dataset)),
622 Err(lance::Error::DatasetNotFound { .. }) => Ok(None),
623 Err(error) => Err(error.into()),
624 }
625}