iceberg/transaction/
action.rs1use std::mem::take;
19
20use as_any::AsAny;
21use async_trait::async_trait;
22
23use crate::table::Table;
24use crate::transaction::Transaction;
25use crate::{Result, TableRequirement, TableUpdate};
26
27pub(crate) type TransactionActionEntry = Box<dyn DynActionEntry>;
29
30#[async_trait]
40pub(crate) trait TransactionAction: Clone + Send + Sync + 'static {
41 type State: Send + Sync + 'static;
45
46 fn new_state(&self) -> Self::State;
51
52 async fn commit(&self, state: &mut Self::State, table: &Table) -> Result<ActionCommit>;
68
69 #[allow(dead_code)]
79 async fn cleanup(self, state: Self::State, table: &Table, status: CommitStatus);
80}
81
82#[async_trait]
91pub(crate) trait StatelessAction: Clone + Send + Sync + 'static {
92 async fn commit(&self, table: &Table) -> Result<ActionCommit>;
96}
97
98#[async_trait]
99impl<T: StatelessAction> TransactionAction for T {
100 type State = ();
101
102 fn new_state(&self) {}
103
104 async fn commit(&self, _state: &mut (), table: &Table) -> Result<ActionCommit> {
105 StatelessAction::commit(self, table).await
106 }
107
108 async fn cleanup(self, _state: (), _table: &Table, _status: CommitStatus) {}
109}
110
111#[allow(dead_code)]
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub(crate) enum CommitStatus {
121 Committed,
124 Failed,
127 Unknown,
135}
136
137pub(crate) struct ActionEntry<A: TransactionAction> {
143 action: A,
144 state: A::State,
145}
146
147impl<A: TransactionAction> ActionEntry<A> {
148 fn new(action: A) -> Self {
149 let state = action.new_state();
150 Self { action, state }
151 }
152
153 #[allow(dead_code)]
155 pub(crate) fn action(&self) -> &A {
156 &self.action
157 }
158}
159
160#[async_trait]
163pub(crate) trait DynActionEntry: AsAny + Send + Sync {
164 async fn commit(&mut self, table: &Table) -> Result<ActionCommit>;
166
167 #[allow(dead_code)]
171 async fn cleanup(self: Box<Self>, table: &Table, status: CommitStatus);
172
173 #[allow(dead_code)]
178 fn clone_with_new_state(&self) -> TransactionActionEntry;
179}
180
181#[async_trait]
182impl<A: TransactionAction> DynActionEntry for ActionEntry<A> {
183 async fn commit(&mut self, table: &Table) -> Result<ActionCommit> {
184 self.action.commit(&mut self.state, table).await
185 }
186
187 async fn cleanup(self: Box<Self>, table: &Table, status: CommitStatus) {
188 let ActionEntry { action, state } = *self;
189 action.cleanup(state, table, status).await
190 }
191
192 fn clone_with_new_state(&self) -> TransactionActionEntry {
193 Box::new(ActionEntry::new(self.action.clone()))
194 }
195}
196
197pub trait ApplyTransactionAction {
202 fn apply(self, tx: Transaction) -> Result<Transaction>;
212}
213
214impl<T: TransactionAction> ApplyTransactionAction for T {
215 fn apply(self, mut tx: Transaction) -> Result<Transaction>
216 where Self: Sized {
217 tx.actions.push(Box::new(ActionEntry::new(self)));
218 Ok(tx)
219 }
220}
221
222pub struct ActionCommit {
227 updates: Vec<TableUpdate>,
228 requirements: Vec<TableRequirement>,
229}
230
231impl ActionCommit {
232 pub fn new(updates: Vec<TableUpdate>, requirements: Vec<TableRequirement>) -> Self {
234 Self {
235 updates,
236 requirements,
237 }
238 }
239
240 pub fn take_updates(&mut self) -> Vec<TableUpdate> {
242 take(&mut self.updates)
243 }
244
245 pub fn take_requirements(&mut self) -> Vec<TableRequirement> {
247 take(&mut self.requirements)
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use std::str::FromStr;
254
255 use as_any::Downcast;
256 use async_trait::async_trait;
257 use uuid::Uuid;
258
259 use crate::table::Table;
260 use crate::test_utils::commit_with_fresh_state;
261 use crate::transaction::Transaction;
262 use crate::transaction::action::{
263 ActionCommit, ActionEntry, ApplyTransactionAction, CommitStatus, DynActionEntry,
264 StatelessAction, TransactionAction,
265 };
266 use crate::transaction::tests::make_v2_table;
267 use crate::{Result, TableRequirement, TableUpdate};
268
269 #[derive(Clone)]
270 struct TestAction;
271
272 #[async_trait]
273 impl TransactionAction for TestAction {
274 type State = ();
275
276 fn new_state(&self) -> Self::State {}
277
278 async fn commit(&self, _state: &mut (), _table: &Table) -> Result<ActionCommit> {
279 Ok(ActionCommit::new(
280 vec![TableUpdate::SetLocation {
281 location: String::from("s3://bucket/prefix/table/"),
282 }],
283 vec![TableRequirement::UuidMatch {
284 uuid: Uuid::from_str("9c12d441-03fe-4693-9a96-a0705ddf69c1")?,
285 }],
286 ))
287 }
288
289 async fn cleanup(self, _state: (), _table: &Table, _status: CommitStatus) {}
290 }
291
292 #[derive(Clone)]
293 struct CountingAction;
294
295 #[async_trait]
296 impl TransactionAction for CountingAction {
297 type State = u32;
298
299 fn new_state(&self) -> Self::State {
300 0
301 }
302
303 async fn commit(&self, state: &mut u32, _table: &Table) -> Result<ActionCommit> {
304 *state += 1;
305 Ok(ActionCommit::new(vec![], vec![]))
306 }
307
308 async fn cleanup(self, _state: u32, _table: &Table, _status: CommitStatus) {}
309 }
310
311 #[derive(Clone)]
312 struct StatelessTestAction;
313
314 #[async_trait]
315 impl StatelessAction for StatelessTestAction {
316 async fn commit(&self, _table: &Table) -> Result<ActionCommit> {
317 Ok(ActionCommit::new(
318 vec![TableUpdate::SetLocation {
319 location: String::from("s3://bucket/prefix/stateless/"),
320 }],
321 vec![],
322 ))
323 }
324 }
325
326 #[tokio::test]
327 async fn test_commit_transaction_action() {
328 let table = make_v2_table();
329 let action = TestAction;
330
331 let mut action_commit = commit_with_fresh_state(action, &table).await.unwrap();
332
333 let updates = action_commit.take_updates();
334 let requirements = action_commit.take_requirements();
335
336 assert_eq!(updates[0], TableUpdate::SetLocation {
337 location: String::from("s3://bucket/prefix/table/")
338 });
339 assert_eq!(requirements[0], TableRequirement::UuidMatch {
340 uuid: Uuid::from_str("9c12d441-03fe-4693-9a96-a0705ddf69c1").unwrap()
341 });
342 }
343
344 #[test]
345 fn test_apply_transaction_action() {
346 let table = make_v2_table();
347 let action = TestAction;
348 let tx = Transaction::new(&table);
349
350 let updated_tx = action.apply(tx).unwrap();
351 assert_eq!(updated_tx.actions.len(), 1);
353
354 (*updated_tx.actions[0])
355 .downcast_ref::<ActionEntry<TestAction>>()
356 .expect("TestAction was not applied to Transaction!");
357 }
358
359 #[tokio::test]
360 async fn test_entry_retains_state_across_commits() {
361 let table = make_v2_table();
362 let mut tx = CountingAction.apply(Transaction::new(&table)).unwrap();
363
364 let entry = &mut tx.actions[0];
365 entry.commit(&table).await.unwrap();
366 entry.commit(&table).await.unwrap();
367
368 let entry = (**entry)
369 .downcast_ref::<ActionEntry<CountingAction>>()
370 .unwrap();
371 assert_eq!(entry.state, 2);
372 }
373
374 #[tokio::test]
375 async fn test_clone_with_new_state_resets_state() {
376 let table = make_v2_table();
377 let mut tx = CountingAction.apply(Transaction::new(&table)).unwrap();
378 tx.actions[0].commit(&table).await.unwrap();
379
380 let cloned = tx.actions[0].clone_with_new_state();
381
382 let original = (*tx.actions[0])
383 .downcast_ref::<ActionEntry<CountingAction>>()
384 .unwrap();
385 let cloned = (*cloned)
386 .downcast_ref::<ActionEntry<CountingAction>>()
387 .unwrap();
388 assert_eq!(original.state, 1);
389 assert_eq!(cloned.state, 0);
390 }
391
392 #[tokio::test]
393 async fn test_stateless_action_blanket_impl() {
394 let table = make_v2_table();
395
396 let mut commit = commit_with_fresh_state(StatelessTestAction, &table)
398 .await
399 .unwrap();
400 assert_eq!(commit.take_updates(), vec![TableUpdate::SetLocation {
401 location: String::from("s3://bucket/prefix/stateless/"),
402 }]);
403
404 let tx = StatelessTestAction.apply(Transaction::new(&table)).unwrap();
406 assert_eq!(tx.actions.len(), 1);
407
408 let entry: Box<ActionEntry<StatelessTestAction>> =
410 Box::new(ActionEntry::new(StatelessTestAction));
411 entry.cleanup(&table, CommitStatus::Committed).await;
412 }
413
414 #[test]
415 fn test_action_commit() {
416 let location = String::from("s3://bucket/prefix/table/");
418 let uuid = Uuid::new_v4();
419 let updates = vec![TableUpdate::SetLocation { location }];
420 let requirements = vec![TableRequirement::UuidMatch { uuid }];
421
422 let mut action_commit = ActionCommit::new(updates.clone(), requirements.clone());
423
424 let taken_updates = action_commit.take_updates();
425 let taken_requirements = action_commit.take_requirements();
426
427 assert_eq!(taken_updates, updates);
429 assert_eq!(taken_requirements, requirements);
430
431 assert!(action_commit.take_updates().is_empty());
432 assert!(action_commit.take_requirements().is_empty());
433 }
434}