use std::sync::Arc;
use arrow_schema::{DataType, Schema, SchemaRef};
use futures::future::BoxFuture;
use uuid::Uuid;
use crate::client::Connection;
use crate::registration::register;
use crate::scandal::RecordBatchStream;
use crate::scandal::types::FragmentReceipt;
use crate::scandal_backend::{AttemptWrite, RowEdit, reader_stream};
use crate::{Blob, BlobPack, BlobStream, Error, IntoArrowStream, Project, Table, TableIdentity};
pub const ROW_ID_COLUMN: &str = "s:row_id";
pub struct TableWrite {
base: Option<Table>,
connection: Arc<Connection>,
write: AttemptWrite,
}
impl Project {
pub async fn table_writer(&self, schema: SchemaRef) -> Result<TableWrite, Error> {
let write = self
.client
.connection
.tables()?
.begin_create(self.id.as_str(), &schema)
.await?;
Ok(TableWrite {
base: None,
connection: Arc::clone(&self.client.connection),
write,
})
}
}
impl Table {
pub async fn writer(&self) -> Result<TableWrite, Error> {
let write = self
.connection
.tables()?
.begin_write(self.identity(), false)
.await?;
Ok(TableWrite {
base: Some(self.clone()),
connection: Arc::clone(&self.connection),
write,
})
}
}
impl TableWrite {
pub fn attempt(&self) -> Uuid {
self.write.attempt()
}
pub async fn upload_blob(&self, media_type: &str, input: BlobStream) -> Result<Blob, Error> {
self.write.upload_blob(media_type, input).await
}
pub fn blob_pack(&self) -> BlobPack<'_> {
BlobPack::new(&self.write)
}
pub async fn append(&mut self, input: impl IntoArrowStream) -> Result<(), Error> {
self.write.append(input.into_write_input()?).await
}
pub async fn patch_rows(&mut self, rows: impl IntoArrowStream) -> Result<(), Error> {
self.edit_input(RowEdit::Patch, rows).await
}
pub async fn delete_rows(&mut self, ids: impl IntoArrowStream) -> Result<(), Error> {
self.edit_input(RowEdit::Delete, ids).await
}
pub async fn finish(self) -> Result<Table, Error> {
let result = register(&self.connection.arbor, self.write.prepare().await?).await?;
match self.base {
Some(base) => base.successor(result),
None => Ok(Table::new(
self.connection,
TableIdentity {
resource_id: result.resource_id,
version_id: result.version_id,
},
)),
}
}
async fn edit_input(
&mut self,
edit: RowEdit,
input: impl IntoArrowStream,
) -> Result<(), Error> {
let reader = input.into_arrow_stream()?;
self.edit_rows(edit, reader.schema(), reader_stream(reader))
.await
}
pub(crate) async fn edit_rows(
&mut self,
edit: RowEdit,
schema: SchemaRef,
input: RecordBatchStream,
) -> Result<(), Error> {
let receipts = self.start_edit(edit, schema, input)?.await?;
self.write.add_receipts(receipts);
Ok(())
}
pub(crate) fn start_edit(
&mut self,
edit: RowEdit,
schema: SchemaRef,
input: RecordBatchStream,
) -> Result<BoxFuture<'static, Result<Vec<FragmentReceipt>, Error>>, Error> {
if self.base.is_none() {
return Err(Error::invalid("a new table has no rows to patch or delete"));
}
check_edit_schema(edit, &schema)?;
self.write.edit_base()?;
Ok(self.write.edit_rows(edit, ROW_ID_COLUMN, schema, input))
}
pub(crate) fn add_receipts(&mut self, receipts: Vec<FragmentReceipt>) {
self.write.add_receipts(receipts);
}
}
pub(crate) fn check_edit_schema(edit: RowEdit, schema: &Schema) -> Result<(), Error> {
let row_ids = schema.field_with_name(ROW_ID_COLUMN).map_err(|_| {
Error::invalid(format!(
"rows that address existing rows need a `{ROW_ID_COLUMN}` column"
))
})?;
if row_ids.data_type() != &DataType::UInt64 {
return Err(Error::invalid(format!(
"`{ROW_ID_COLUMN}` holds uint64 row IDs, not {}",
row_ids.data_type()
)));
}
let values = schema.fields().len() - 1;
match edit {
RowEdit::Delete if values > 0 => Err(Error::invalid(format!(
"deletions hold only the `{ROW_ID_COLUMN}` column"
))),
RowEdit::Patch if values == 0 => Err(Error::invalid(
"patched rows hold at least one column besides their row IDs",
)),
_ => Ok(()),
}
}
#[cfg(test)]
mod tests {
use arrow_schema::Field;
use super::*;
fn schema(fields: &[(&str, DataType)]) -> Schema {
Schema::new(
fields
.iter()
.map(|(name, data_type)| Field::new(*name, data_type.clone(), false))
.collect::<Vec<_>>(),
)
}
#[test]
fn edit_rows_name_their_targets_and_carry_the_columns_their_edit_needs() {
let ids = schema(&[(ROW_ID_COLUMN, DataType::UInt64)]);
let values = schema(&[(ROW_ID_COLUMN, DataType::UInt64), ("label", DataType::Utf8)]);
assert!(check_edit_schema(RowEdit::Delete, &ids).is_ok());
assert!(check_edit_schema(RowEdit::Delete, &values).is_err());
assert!(check_edit_schema(RowEdit::Patch, &values).is_ok());
assert!(check_edit_schema(RowEdit::Patch, &ids).is_err());
let signed = schema(&[(ROW_ID_COLUMN, DataType::Int64)]);
assert!(check_edit_schema(RowEdit::Delete, &signed).is_err());
assert!(check_edit_schema(RowEdit::Patch, &schema(&[("label", DataType::Utf8)])).is_err());
}
}