1use std::sync::Arc;
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use arrow_array::RecordBatch;
7use arrow_schema::{DataType, FieldRef, SchemaRef};
8use futures::{StreamExt as _, TryStreamExt as _, future, stream};
9
10use crate::registration::register;
11use crate::scandal::api::{
12 AddColumn, ArrowDataType, ArrowField, ColumnPath as WireColumnPath, MoveColumn, RenameColumn,
13 SchemaChange as WireSchemaChange, WidenType,
14};
15use crate::scandal_backend::{RowEdit, input_error};
16use crate::table_write::ROW_ID_COLUMN;
17use crate::{BatchStream, Error, Expression, NamedExpression, ScanOptions, Table};
18
19#[derive(Clone, Debug)]
21pub struct UpdatedRows {
22 pub table: Table,
24 pub rows: u64,
26}
27
28#[derive(Clone, Debug, Eq, Hash, PartialEq)]
30pub struct ColumnPath(Vec<String>);
31
32impl ColumnPath {
33 pub fn nested(segments: impl IntoIterator<Item = impl Into<String>>) -> Self {
35 Self(segments.into_iter().map(Into::into).collect())
36 }
37
38 pub fn segments(&self) -> &[String] {
40 &self.0
41 }
42
43 fn into_wire(self) -> Result<Vec<String>, Error> {
44 if self.0.is_empty() || self.0.iter().any(String::is_empty) {
45 return Err(Error::invalid("a column path has nonempty segments"));
46 }
47 Ok(self.0)
48 }
49}
50
51impl From<&str> for ColumnPath {
52 fn from(column: &str) -> Self {
53 Self(vec![column.to_owned()])
54 }
55}
56
57impl From<String> for ColumnPath {
58 fn from(column: String) -> Self {
59 Self(vec![column])
60 }
61}
62
63#[derive(Clone, Debug, Eq, PartialEq)]
68pub enum SchemaChange {
69 AddColumn {
71 field: FieldRef,
72 after: Option<ColumnPath>,
73 },
74 DropColumn { path: ColumnPath },
76 RenameColumn { path: ColumnPath, name: String },
78 SetNullable { path: ColumnPath },
80 WidenType {
83 path: ColumnPath,
84 data_type: DataType,
85 },
86 MoveColumn {
88 path: ColumnPath,
89 after: Option<ColumnPath>,
90 },
91}
92
93impl SchemaChange {
94 pub(crate) fn into_wire(self) -> Result<WireSchemaChange, Error> {
96 Ok(match self {
97 Self::AddColumn { field, after } => {
98 if !field.is_nullable() {
99 return Err(Error::invalid(format!(
100 "added column `{}` must be nullable",
101 field.name()
102 )));
103 }
104 WireSchemaChange::AddColumn(AddColumn {
105 field: ArrowField(serde_json::to_value(field.as_ref()).map_err(Error::arrow)?),
106 after: after.map(ColumnPath::into_wire).transpose()?,
107 })
108 }
109 Self::DropColumn { path } => WireSchemaChange::DropColumn(WireColumnPath {
110 path: path.into_wire()?,
111 }),
112 Self::RenameColumn { path, name } => WireSchemaChange::RenameColumn(RenameColumn {
113 path: path.into_wire()?,
114 name,
115 }),
116 Self::SetNullable { path } => WireSchemaChange::SetNullable(WireColumnPath {
117 path: path.into_wire()?,
118 }),
119 Self::WidenType { path, data_type } => WireSchemaChange::WidenType(WidenType {
120 path: path.into_wire()?,
121 data_type: ArrowDataType(serde_json::to_value(&data_type).map_err(Error::arrow)?),
122 }),
123 Self::MoveColumn { path, after } => WireSchemaChange::MoveColumn(MoveColumn {
124 path: path.into_wire()?,
125 after: after.map(ColumnPath::into_wire).transpose()?,
126 }),
127 })
128 }
129}
130
131impl Table {
132 pub async fn schema(&self) -> Result<SchemaRef, Error> {
134 Ok(self.scan(ScanOptions::default()).await?.schema())
135 }
136
137 pub async fn alter_schema(
141 &self,
142 changes: impl IntoIterator<Item = SchemaChange>,
143 ) -> Result<Table, Error> {
144 let changes = changes
145 .into_iter()
146 .map(SchemaChange::into_wire)
147 .collect::<Result<Vec<_>, _>>()?;
148 if changes.is_empty() {
149 return Ok(self.clone());
150 }
151 let write = self
152 .connection
153 .tables()?
154 .begin_edit(self.identity(), changes)
155 .await?;
156 let result = register(&self.connection.arbor, write.prepare().await?).await?;
157 self.successor(result)
158 }
159
160 pub async fn patch_rows(
167 &self,
168 filter: Option<Expression>,
169 set: Vec<NamedExpression>,
170 ) -> Result<UpdatedRows, Error> {
171 self.update_rows(filter, set).await
172 }
173
174 async fn update_rows(
176 &self,
177 filter: Option<Expression>,
178 set: Vec<NamedExpression>,
179 ) -> Result<UpdatedRows, Error> {
180 if set.is_empty() {
181 return Err(Error::invalid("an update assigns at least one column"));
182 }
183 let scan = self
184 .scan(ScanOptions {
185 projection: Some(set),
186 filter,
187 row_id_column: Some(ROW_ID_COLUMN.to_owned()),
188 ..Default::default()
189 })
190 .await?;
191 let schema = scan.schema();
192 let mut batches = scan.into_stream();
193 let Some(first) = first_rows(&mut batches).await? else {
194 return Ok(UpdatedRows {
195 table: self.clone(),
196 rows: 0,
197 });
198 };
199 let rows = Arc::new(AtomicU64::new(first.num_rows() as u64));
200 let counted = Arc::clone(&rows);
201 let input = stream::once(future::ready(Ok(first)))
202 .chain(batches.inspect_ok(move |batch| {
203 counted.fetch_add(batch.num_rows() as u64, Ordering::Relaxed);
204 }))
205 .map_err(input_error)
206 .boxed();
207 let mut write = self.writer().await?;
208 write.edit_rows(RowEdit::Patch, schema, input).await?;
209 Ok(UpdatedRows {
210 table: write.finish().await?,
211 rows: rows.load(Ordering::Relaxed),
212 })
213 }
214}
215
216pub(crate) async fn first_rows(batches: &mut BatchStream) -> Result<Option<RecordBatch>, Error> {
218 while let Some(batch) = batches.next_batch().await {
219 let batch = batch?;
220 if batch.num_rows() > 0 {
221 return Ok(Some(batch));
222 }
223 }
224 Ok(None)
225}
226
227#[cfg(test)]
228mod tests {
229 use arrow_schema::Field;
230 use serde_json::json;
231
232 use super::*;
233
234 #[test]
235 fn schema_changes_name_paths_and_carry_arrow_json() {
236 let added = SchemaChange::AddColumn {
237 field: Arc::new(Field::new("score", DataType::Float32, true)),
238 after: Some("label".into()),
239 }
240 .into_wire()
241 .unwrap();
242 let WireSchemaChange::AddColumn(added) = added else {
243 panic!("an added column stays an added column");
244 };
245 assert_eq!(added.after, Some(vec!["label".to_owned()]));
246 assert_eq!(added.field.0["name"], json!("score"));
247 assert_eq!(added.field.0["nullable"], json!(true));
248
249 let widened = SchemaChange::WidenType {
250 path: ColumnPath::nested(["pose", "x"]),
251 data_type: DataType::Float64,
252 }
253 .into_wire()
254 .unwrap();
255 assert_eq!(
256 widened,
257 WireSchemaChange::WidenType(WidenType {
258 path: vec!["pose".to_owned(), "x".to_owned()],
259 data_type: ArrowDataType(json!("Float64")),
260 })
261 );
262
263 let required = SchemaChange::AddColumn {
264 field: Arc::new(Field::new("score", DataType::Float32, false)),
265 after: None,
266 };
267 assert!(required.into_wire().is_err());
268 let unnamed = SchemaChange::DropColumn {
269 path: ColumnPath::nested(Vec::<String>::new()),
270 };
271 assert!(unnamed.into_wire().is_err());
272 }
273}