use std::collections::{HashMap, HashSet};
use arrow_array::RecordBatch;
use parquet::file::metadata::{ParquetMetaData, ParquetMetaDataWriter};
use crate::commit::{Change, CommitInfo, commit};
use crate::error::WriteResult;
use crate::file::{FileStats, FileWriter, writer_properties};
use crate::host::{Host, Upload, uuid_v7};
use crate::iceberg::DataFile;
use crate::maintain::{MaintainOptions, MaintainReport, maintain};
use crate::rows::{latest_of_sorted, sort_and_split};
use crate::summary;
use crate::table::TableState;
pub const PART_BYTES: usize = 8 << 20;
#[derive(Clone, Debug, Default)]
pub struct AppendOptions {
pub batch_id: Option<String>,
pub properties: HashMap<String, String>,
pub maintain: Option<bool>,
pub max_compact_bytes: Option<u64>,
}
#[derive(Clone, Debug, Default)]
pub struct AppendReport {
pub rows: usize,
pub files: usize,
pub bytes: u64,
pub commit: CommitInfo,
pub maintenance: Option<Result<MaintainReport, String>>,
}
pub async fn append<H: Host>(
host: &H,
table: TableState,
batches: &[RecordBatch],
opts: &AppendOptions,
) -> WriteResult<(TableState, AppendReport)> {
let now_us = host.now_ms().saturating_mul(1000);
let partitions = sort_and_split(&table.schema, &table.contract, batches, now_us)?;
let rows = partitions.iter().map(|(_, b)| b.num_rows()).sum();
let touched: HashSet<Option<i32>> = partitions.iter().map(|(p, _)| *p).collect();
let latest = latest_of_sorted(&table.schema, &table.contract, &partitions)?;
let mut files = vec![];
let mut footers = vec![];
let result = async {
for (partition, batch) in &partitions {
for (file, footer) in write_files(host, &table, *partition, batch).await? {
footers.push((file.path.clone(), footer));
files.push(file);
}
}
Ok::<_, crate::error::WriteError>(())
}
.await;
let paths: Vec<String> = files.iter().map(|f: &DataFile| f.path.clone()).collect();
if let Err(e) = result {
let _ = host.delete(&paths).await;
return Err(e);
}
if files.is_empty() {
let info = CommitInfo { version: table.version, ..CommitInfo::default() };
return Ok((table, AppendReport { commit: info, ..AppendReport::default() }));
}
let (n_files, bytes) = (files.len(), files.iter().map(|f| f.file_size).sum());
let mut props = opts.properties.clone();
if let Some(id) = &opts.batch_id {
props.insert(summary::BATCH_ID.into(), id.clone());
}
let committed = commit(host, table, &Change::Append { files, latest }, &props).await;
if !matches!(&committed, Ok((_, info)) if !info.duplicate) {
let _ = host.delete(&paths).await;
}
let (mut table, info) = committed?;
if info.duplicate {
return Ok((table, AppendReport { commit: info, ..AppendReport::default() }));
}
table.footers.extend(footers);
let _ = table.write_head(host).await;
let mut report = AppendReport { rows, files: n_files, bytes, commit: info, maintenance: None };
if !opts.maintain.unwrap_or(table.contract.auto_maintain) {
return Ok((table, report));
}
let mut maintain_opts = MaintainOptions::inline(&table, touched, host.now_ms());
maintain_opts.max_bytes = opts.max_compact_bytes;
match maintain(host, table.clone(), &maintain_opts).await {
Ok((maintained, done)) => {
report.maintenance = (!done.is_noop()).then_some(Ok(done));
Ok((maintained, report))
}
Err(e) => {
report.maintenance = Some(Err(e.to_string()));
Ok((table, report))
}
}
}
pub async fn write_files<H: Host>(
host: &H,
table: &TableState,
partition: Option<i32>,
rows: &RecordBatch,
) -> WriteResult<Vec<(DataFile, Vec<u8>)>> {
let mut writer = PartitionFiles::new(host, table, partition)?;
writer.write(rows).await?;
writer.finish().await
}
pub struct PartitionFiles<'a, H: Host> {
host: &'a H,
table: &'a TableState,
partition: Option<i32>,
open: Option<OpenFile<H::Upload>>,
done: Vec<(DataFile, Vec<u8>)>,
}
impl<H: Host> std::fmt::Debug for PartitionFiles<'_, H> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PartitionFiles").field("partition", &self.partition).finish_non_exhaustive()
}
}
struct OpenFile<U> {
uri: String,
writer: FileWriter,
pending: Pending,
upload: Option<U>,
}
#[derive(Default)]
struct Pending {
chunks: std::collections::VecDeque<Vec<u8>>,
len: usize,
}
impl Pending {
fn extend(&mut self, chunks: Vec<Vec<u8>>) {
for c in chunks {
self.len += c.len();
self.chunks.push_back(c);
}
}
fn take(&mut self, n: usize) -> Vec<u8> {
let n = n.min(self.len);
let mut part = Vec::with_capacity(n);
while part.len() < n {
let Some(mut chunk) = self.chunks.pop_front() else { break };
let want = n - part.len();
if chunk.len() > want {
let rest = chunk.split_off(want);
self.chunks.push_front(rest);
}
part.extend_from_slice(&chunk);
}
self.len -= part.len();
part
}
}
impl<'a, H: Host> PartitionFiles<'a, H> {
pub fn new(host: &'a H, table: &'a TableState, partition: Option<i32>) -> WriteResult<Self> {
Ok(Self { host, table, partition, open: None, done: vec![] })
}
fn dir(&self) -> String {
let c = &self.table.contract;
match (self.partition, &c.time_column, c.chunk) {
(Some(v), Some(time_column), Some(chunk)) => {
format!("{}/data/{time_column}_{chunk}={}", self.table.location, chunk.label(v))
}
_ => format!("{}/data", self.table.location),
}
}
pub async fn write(&mut self, batch: &RecordBatch) -> WriteResult<()> {
if batch.num_rows() == 0 {
return Ok(());
}
let mut file = if let Some(f) = self.open.take() {
f
} else {
let page_rows = self.host.data_page_rows();
let props = writer_properties(&self.table.schema, &self.table.contract, &created_by(), page_rows)?;
OpenFile {
uri: format!("{}/{}.parquet", self.dir(), uuid_v7(self.host)),
writer: FileWriter::new(self.table.schema.clone(), props)?
.with_max_row_group_bytes(self.host.max_row_group_bytes()),
pending: Pending::default(),
upload: None,
}
};
file.writer.write(batch)?;
file.pending.extend(file.writer.take_chunks());
while file.pending.len >= PART_BYTES {
let mut upload = match file.upload.take() {
Some(u) => u,
None => self.host.upload(&file.uri).await?,
};
upload.put_part(file.pending.take(PART_BYTES)).await?;
file.upload = Some(upload);
}
if file.writer.size() >= self.table.contract.target_file_size {
self.close(file).await?;
} else {
self.open = Some(file);
}
Ok(())
}
async fn close(&mut self, file: OpenFile<H::Upload>) -> WriteResult<()> {
let OpenFile { uri, writer, mut pending, upload } = file;
let (rest, stats) = writer.finish_chunks()?;
pending.extend(rest);
if let Some(mut u) = upload {
while pending.len > 0 {
u.put_part(pending.take(PART_BYTES)).await?;
}
u.complete().await?;
} else {
let all = pending.len;
self.host.put(&uri, pending.take(all)).await?;
}
let footer = footer_bytes(&stats.metadata)?;
self.done.push((data_file(self.table, uri, self.partition, &stats), footer));
Ok(())
}
pub async fn finish(mut self) -> WriteResult<Vec<(DataFile, Vec<u8>)>> {
if let Some(file) = self.open.take() {
self.close(file).await?;
}
Ok(self.done)
}
}
fn created_by() -> String {
format!("timelake {}", env!("CARGO_PKG_VERSION"))
}
pub fn data_file(table: &TableState, uri: String, partition: Option<i32>, stats: &FileStats) -> DataFile {
DataFile {
path: uri,
partition,
record_count: stats.record_count,
file_size: stats.file_size,
column_sizes: stats.column_sizes.clone(),
value_counts: stats.value_counts.clone(),
null_counts: stats.null_counts.clone(),
nan_counts: stats.nan_counts.clone(),
lower_bounds: stats.lower_bounds.clone(),
upper_bounds: stats.upper_bounds.clone(),
split_offsets: Some(stats.split_offsets.clone()),
sort_order_id: (!table.contract.sort_columns().is_empty()).then(|| table.metadata.sort_order_id()),
}
}
pub fn footer_bytes(metadata: &ParquetMetaData) -> WriteResult<Vec<u8>> {
let mut buf = vec![];
ParquetMetaDataWriter::new(&mut buf, metadata).finish()?;
Ok(buf)
}