use std::collections::HashMap;
use std::sync::Arc;
use arrow::array::RecordBatch;
use arrow::datatypes::Schema;
use futures::{Stream, StreamExt};
use lance::Dataset;
use lance_file::version::LanceFileVersion;
use nd_arrow_array::NdRecordBatch;
use crate::cast::{Casting, cast_to};
use crate::error::{Error, Result};
use crate::format::add_format;
use crate::options::{ColumnOptions, column_defaults, with_column_options};
use crate::schema::conform_batch;
use crate::stats::{add_stats, has_stats};
use crate::store::StorageOptions;
use crate::writer::{NdWriter, open_existing};
pub(crate) const FILE_VERSION: LanceFileVersion = LanceFileVersion::V2_2;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum WriteMode {
#[default]
Create,
Append,
Overwrite,
}
#[derive(Debug, Clone)]
pub struct WriteOptions {
pub mode: WriteMode,
pub max_rows_per_group: usize,
pub max_values_per_group: usize,
pub column_defaults: Option<ColumnOptions>,
pub column_options: HashMap<String, ColumnOptions>,
pub stats: bool,
pub casting: Casting,
pub target_fragment_bytes: usize,
pub write_parallelism: usize,
pub storage_options: StorageOptions,
}
impl Default for WriteOptions {
fn default() -> Self {
Self {
mode: WriteMode::Create,
max_rows_per_group: 1024,
max_values_per_group: 1 << 26,
column_defaults: None,
column_options: HashMap::new(),
stats: false,
casting: Casting::No,
target_fragment_bytes: 128 << 20,
write_parallelism: 4,
storage_options: StorageOptions::new(),
}
}
}
#[tracing::instrument(skip_all, fields(uri = %uri, mode = ?options.mode), err)]
pub async fn write(
uri: &str,
rows: impl Stream<Item = Result<NdRecordBatch>>,
options: &WriteOptions,
) -> Result<Dataset> {
let writer = match options.mode {
WriteMode::Append
if open_existing(uri, &options.storage_options)
.await?
.is_some() =>
{
NdWriter::append(uri, options).await?
}
WriteMode::Append => {
let options = WriteOptions {
mode: WriteMode::Create,
..options.clone()
};
NdWriter::create(uri, &options).await?
}
WriteMode::Create | WriteMode::Overwrite => NdWriter::create(uri, options).await?,
};
write_all(writer, rows).await
}
#[tracing::instrument(skip_all, fields(uri = %dataset.uri()), err)]
pub async fn append(
dataset: &mut Dataset,
rows: impl Stream<Item = Result<NdRecordBatch>>,
options: &WriteOptions,
) -> Result<()> {
let mut options = options.clone();
if options.storage_options.is_empty() {
options.storage_options = dataset
.initial_storage_options()
.cloned()
.unwrap_or_default();
}
let writer = NdWriter::append(dataset.uri(), &options).await?;
*dataset = write_all(writer, rows).await?;
Ok(())
}
async fn write_all(
mut writer: NdWriter,
rows: impl Stream<Item = Result<NdRecordBatch>>,
) -> Result<Dataset> {
if let Err(error) = Box::pin(writer.write(rows)).await {
writer.abort();
return Err(error);
}
Box::pin(writer.finish()).await
}
pub(crate) fn table_defaults(dataset: &Dataset, options: &WriteOptions) -> Result<ColumnOptions> {
let stored = column_defaults(dataset)?;
match &options.column_defaults {
Some(defaults) if *defaults != stored => Err(Error::InvalidArgument(
"the table has other column defaults, use set_column_defaults to change them"
.to_string(),
)),
_ => Ok(stored),
}
}
pub(crate) fn table_has_stats(dataset: &Dataset) -> bool {
has_stats(&Schema::from(dataset.schema()))
}
pub(crate) fn prepare_batch(
mut batch: RecordBatch,
defaults: &ColumnOptions,
options: &WriteOptions,
stats: bool,
table: Option<&Schema>,
) -> Result<RecordBatch> {
if let Some(table) = table {
batch = cast_to(batch, table, options.casting)?;
}
if stats {
batch = add_stats(&batch)?;
}
let mut metadata = batch.schema().metadata().clone();
metadata.extend(defaults.to_table_metadata());
add_format(&mut metadata);
let schema = with_column_options(&batch.schema(), defaults, &options.column_options)
.with_metadata(metadata);
conform_batch(&batch, &Arc::new(schema))
}
pub(crate) async fn next_group(
rows: &mut (impl Stream<Item = Result<NdRecordBatch>> + Unpin),
options: &WriteOptions,
) -> Result<Option<Vec<NdRecordBatch>>> {
let mut group = Vec::new();
let mut values = 0;
while group.len() < options.max_rows_per_group.max(1) && values < options.max_values_per_group {
let Some(row) = rows.next().await else {
break;
};
let row = row?;
values += row.columns().iter().map(|c| c.len()).sum::<usize>();
group.push(row);
}
Ok((!group.is_empty()).then_some(group))
}