1use std::sync::Arc;
5
6use arrow_schema::{DataType, Schema, SchemaRef};
7use futures::future::BoxFuture;
8use uuid::Uuid;
9
10use crate::client::Connection;
11use crate::registration::register;
12use crate::scandal::RecordBatchStream;
13use crate::scandal::types::FragmentReceipt;
14use crate::scandal_backend::{AttemptWrite, RowEdit, reader_stream};
15use crate::{Blob, BlobPack, BlobStream, Error, IntoArrowStream, Project, Table, TableIdentity};
16
17pub const ROW_ID_COLUMN: &str = "s:row_id";
21
22pub struct TableWrite {
29 base: Option<Table>,
31 connection: Arc<Connection>,
32 write: AttemptWrite,
33}
34
35impl Project {
36 pub async fn table_writer(&self, schema: SchemaRef) -> Result<TableWrite, Error> {
38 let write = self
39 .client
40 .connection
41 .tables()?
42 .begin_create(self.id.as_str(), &schema)
43 .await?;
44 Ok(TableWrite {
45 base: None,
46 connection: Arc::clone(&self.client.connection),
47 write,
48 })
49 }
50}
51
52impl Table {
53 pub async fn writer(&self) -> Result<TableWrite, Error> {
56 let write = self
57 .connection
58 .tables()?
59 .begin_write(self.identity(), false)
60 .await?;
61 Ok(TableWrite {
62 base: Some(self.clone()),
63 connection: Arc::clone(&self.connection),
64 write,
65 })
66 }
67}
68
69impl TableWrite {
70 pub fn attempt(&self) -> Uuid {
72 self.write.attempt()
73 }
74
75 pub async fn upload_blob(&self, media_type: &str, input: BlobStream) -> Result<Blob, Error> {
79 self.write.upload_blob(media_type, input).await
80 }
81
82 pub fn blob_pack(&self) -> BlobPack<'_> {
85 BlobPack::new(&self.write)
86 }
87
88 pub async fn append(&mut self, input: impl IntoArrowStream) -> Result<(), Error> {
90 self.write.append(input.into_write_input()?).await
91 }
92
93 pub async fn patch_rows(&mut self, rows: impl IntoArrowStream) -> Result<(), Error> {
97 self.edit_input(RowEdit::Patch, rows).await
98 }
99
100 pub async fn delete_rows(&mut self, ids: impl IntoArrowStream) -> Result<(), Error> {
103 self.edit_input(RowEdit::Delete, ids).await
104 }
105
106 pub async fn finish(self) -> Result<Table, Error> {
108 let result = register(&self.connection.arbor, self.write.prepare().await?).await?;
109 match self.base {
110 Some(base) => base.successor(result),
111 None => Ok(Table::new(
112 self.connection,
113 TableIdentity {
114 resource_id: result.resource_id,
115 version_id: result.version_id,
116 },
117 )),
118 }
119 }
120
121 async fn edit_input(
122 &mut self,
123 edit: RowEdit,
124 input: impl IntoArrowStream,
125 ) -> Result<(), Error> {
126 let reader = input.into_arrow_stream()?;
127 self.edit_rows(edit, reader.schema(), reader_stream(reader))
128 .await
129 }
130
131 pub(crate) async fn edit_rows(
133 &mut self,
134 edit: RowEdit,
135 schema: SchemaRef,
136 input: RecordBatchStream,
137 ) -> Result<(), Error> {
138 let receipts = self.start_edit(edit, schema, input)?.await?;
139 self.write.add_receipts(receipts);
140 Ok(())
141 }
142
143 pub(crate) fn start_edit(
146 &mut self,
147 edit: RowEdit,
148 schema: SchemaRef,
149 input: RecordBatchStream,
150 ) -> Result<BoxFuture<'static, Result<Vec<FragmentReceipt>, Error>>, Error> {
151 if self.base.is_none() {
152 return Err(Error::invalid("a new table has no rows to patch or delete"));
153 }
154 check_edit_schema(edit, &schema)?;
155 self.write.edit_base()?;
156 Ok(self.write.edit_rows(edit, ROW_ID_COLUMN, schema, input))
157 }
158
159 pub(crate) fn add_receipts(&mut self, receipts: Vec<FragmentReceipt>) {
161 self.write.add_receipts(receipts);
162 }
163}
164
165pub(crate) fn check_edit_schema(edit: RowEdit, schema: &Schema) -> Result<(), Error> {
168 let row_ids = schema.field_with_name(ROW_ID_COLUMN).map_err(|_| {
169 Error::invalid(format!(
170 "rows that address existing rows need a `{ROW_ID_COLUMN}` column"
171 ))
172 })?;
173 if row_ids.data_type() != &DataType::UInt64 {
174 return Err(Error::invalid(format!(
175 "`{ROW_ID_COLUMN}` holds uint64 row IDs, not {}",
176 row_ids.data_type()
177 )));
178 }
179 let values = schema.fields().len() - 1;
180 match edit {
181 RowEdit::Delete if values > 0 => Err(Error::invalid(format!(
182 "deletions hold only the `{ROW_ID_COLUMN}` column"
183 ))),
184 RowEdit::Patch if values == 0 => Err(Error::invalid(
185 "patched rows hold at least one column besides their row IDs",
186 )),
187 _ => Ok(()),
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use arrow_schema::Field;
194
195 use super::*;
196
197 fn schema(fields: &[(&str, DataType)]) -> Schema {
198 Schema::new(
199 fields
200 .iter()
201 .map(|(name, data_type)| Field::new(*name, data_type.clone(), false))
202 .collect::<Vec<_>>(),
203 )
204 }
205
206 #[test]
207 fn edit_rows_name_their_targets_and_carry_the_columns_their_edit_needs() {
208 let ids = schema(&[(ROW_ID_COLUMN, DataType::UInt64)]);
209 let values = schema(&[(ROW_ID_COLUMN, DataType::UInt64), ("label", DataType::Utf8)]);
210 assert!(check_edit_schema(RowEdit::Delete, &ids).is_ok());
211 assert!(check_edit_schema(RowEdit::Delete, &values).is_err());
212 assert!(check_edit_schema(RowEdit::Patch, &values).is_ok());
213 assert!(check_edit_schema(RowEdit::Patch, &ids).is_err());
214 let signed = schema(&[(ROW_ID_COLUMN, DataType::Int64)]);
215 assert!(check_edit_schema(RowEdit::Delete, &signed).is_err());
216 assert!(check_edit_schema(RowEdit::Patch, &schema(&[("label", DataType::Utf8)])).is_err());
217 }
218}