use crate::bucketing::BucketingFunction;
use crate::client::table::partition_getter::{PartitionGetter, get_physical_path};
use crate::client::{WriteRecord, WriteResultFuture, WriterClient};
use crate::error::Error::{IllegalArgument, UnexpectedError};
use crate::error::Result;
use crate::metadata::{PhysicalTablePath, TableInfo, TablePath};
use crate::record::prepare_append_record_batch;
use crate::row::encode::{KeyEncoder, KeyEncoderFactory};
use crate::row::{ColumnarRow, InternalRow};
use arrow::array::{RecordBatch, UInt32Array};
use bytes::Bytes;
use parking_lot::Mutex;
use std::collections::HashMap;
use std::sync::Arc;
pub struct TableAppend {
table_path: Arc<TablePath>,
table_info: Arc<TableInfo>,
writer_client: Arc<WriterClient>,
}
impl TableAppend {
pub(super) fn new(
table_path: TablePath,
table_info: Arc<TableInfo>,
writer_client: Arc<WriterClient>,
) -> Self {
Self {
table_path: Arc::new(table_path),
table_info,
writer_client,
}
}
pub fn create_writer(&self) -> Result<AppendWriter> {
let partition_getter = if self.table_info.is_partitioned() {
Some(PartitionGetter::new(
self.table_info.row_type(),
Arc::clone(self.table_info.get_partition_keys()),
)?)
} else {
None
};
let bucket_router = if self.table_info.has_bucket_key() {
let data_lake_format = self.table_info.table_config.get_datalake_format()?;
let encoder = KeyEncoderFactory::of_bucket_key_encoder(
self.table_info.row_type(),
self.table_info.get_bucket_keys(),
&data_lake_format,
)?;
Some(BucketRouter {
encoder: Mutex::new(encoder),
bucketing: <dyn BucketingFunction>::of(data_lake_format.as_ref()),
})
} else {
None
};
Ok(AppendWriter {
table_path: Arc::clone(&self.table_path),
partition_getter,
writer_client: self.writer_client.clone(),
table_info: Arc::clone(&self.table_info),
bucket_router,
})
}
}
struct BucketRouter {
encoder: Mutex<Box<dyn KeyEncoder>>,
bucketing: Box<dyn BucketingFunction>,
}
impl BucketRouter {
fn encode_key(&self, row: &dyn InternalRow) -> Result<Bytes> {
self.encoder.lock().encode_key(row)
}
fn bucket_of(&self, row: &dyn InternalRow, num_buckets: i32) -> Result<(i32, Bytes)> {
let key = self.encode_key(row)?;
let bucket = self.bucketing.bucketing(&key, num_buckets)?;
Ok((bucket, key))
}
}
pub struct AppendWriter {
table_path: Arc<TablePath>,
partition_getter: Option<PartitionGetter>,
writer_client: Arc<WriterClient>,
table_info: Arc<TableInfo>,
bucket_router: Option<BucketRouter>,
}
impl AppendWriter {
fn check_field_count<R: InternalRow>(&self, row: &R) -> Result<()> {
let expected = self.table_info.get_row_type().fields().len();
if row.get_field_count() != expected {
return Err(IllegalArgument {
message: format!(
"The field count of the row does not match the table schema. \
Expected: {}, Actual: {}",
expected,
row.get_field_count()
),
});
}
Ok(())
}
pub fn append<R: InternalRow>(&self, row: &R) -> Result<WriteResultFuture> {
self.check_field_count(row)?;
let physical_table_path = Arc::new(get_physical_path(
&self.table_path,
self.partition_getter.as_ref(),
row,
)?);
let bucket_key = match &self.bucket_router {
Some(router) => Some(router.encode_key(row)?),
None => None,
};
let record = WriteRecord::for_append(
Arc::clone(&self.table_info),
physical_table_path,
self.table_info.schema_id,
row,
)
.with_bucket_key(bucket_key);
let result_handle = self.writer_client.send(&record)?;
Ok(WriteResultFuture::new(result_handle))
}
pub fn append_arrow_batch(&self, batch: RecordBatch) -> Result<WriteResultFuture> {
if batch.num_rows() == 0 {
return Ok(WriteResultFuture::join(Vec::new()));
}
let batch = prepare_append_record_batch(&batch, self.table_info.row_type())?;
let physical_table_path = if self.partition_getter.is_some() {
let first_row = ColumnarRow::new(
Arc::new(batch.clone()),
Arc::new(self.table_info.row_type.clone()),
0,
None,
)?;
Arc::new(get_physical_path(
&self.table_path,
self.partition_getter.as_ref(),
&first_row,
)?)
} else {
Arc::new(PhysicalTablePath::of(Arc::clone(&self.table_path)))
};
let Some(router) = self.bucket_router.as_ref() else {
return self.send_arrow_batch(batch, physical_table_path, None);
};
let num_buckets = self.table_info.get_num_buckets();
let batch_arc = Arc::new(batch.clone());
let row_type = Arc::new(self.table_info.row_type.clone());
let mut groups: HashMap<i32, (Vec<u32>, Bytes)> = HashMap::new();
let mut row = ColumnarRow::new(Arc::clone(&batch_arc), Arc::clone(&row_type), 0, None)?;
for i in 0..batch.num_rows() {
row.set_row_id(i);
let (bucket, key) = router.bucket_of(&row, num_buckets)?;
groups
.entry(bucket)
.or_insert_with(|| (Vec::new(), key))
.0
.push(i as u32);
}
if groups.len() == 1 {
let (_, (_, rep_key)) = groups.into_iter().next().unwrap();
return self.send_arrow_batch(batch, physical_table_path, Some(rep_key));
}
let mut handles = Vec::with_capacity(groups.len());
for (_bucket, (indices, rep_key)) in groups {
let sub_batch = take_rows(&batch, &indices)?;
let record = WriteRecord::for_append_record_batch(
Arc::clone(&self.table_info),
Arc::clone(&physical_table_path),
self.table_info.schema_id,
sub_batch,
)
.with_bucket_key(Some(rep_key));
handles.push(self.writer_client.send(&record)?);
}
Ok(WriteResultFuture::join(handles))
}
fn send_arrow_batch(
&self,
batch: RecordBatch,
physical_table_path: Arc<PhysicalTablePath>,
bucket_key: Option<Bytes>,
) -> Result<WriteResultFuture> {
let record = WriteRecord::for_append_record_batch(
Arc::clone(&self.table_info),
physical_table_path,
self.table_info.schema_id,
batch,
)
.with_bucket_key(bucket_key);
let result_handle = self.writer_client.send(&record)?;
Ok(WriteResultFuture::new(result_handle))
}
pub async fn flush(&self) -> Result<()> {
self.writer_client.flush().await
}
}
fn take_rows(batch: &RecordBatch, indices: &[u32]) -> Result<RecordBatch> {
let idx = UInt32Array::from(indices.to_vec());
let columns = batch
.columns()
.iter()
.map(|column| arrow::compute::take(column, &idx, None))
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| UnexpectedError {
message: format!("Failed to split arrow batch by bucket: {e}"),
source: None,
})?;
RecordBatch::try_new(batch.schema(), columns).map_err(|e| UnexpectedError {
message: format!("Failed to rebuild split arrow batch: {e}"),
source: None,
})
}