iceberg/transaction/
row_delta.rs1use std::collections::HashMap;
19use std::sync::Arc;
20
21use async_trait::async_trait;
22use uuid::Uuid;
23
24use crate::error::Result;
25use crate::spec::{DataContentType, DataFile, ManifestEntry, ManifestFile, Operation};
26use crate::table::Table;
27use crate::transaction::snapshot::{
28 DefaultManifestProcess, SnapshotProduceOperation, SnapshotProducer,
29};
30use crate::transaction::{ActionCommit, TransactionAction};
31use crate::{Error, ErrorKind};
32
33pub struct RowDeltaAction {
56 check_duplicate: bool,
57 commit_uuid: Option<Uuid>,
59 snapshot_properties: HashMap<String, String>,
60 added_data_files: Vec<DataFile>,
61 added_delete_files: Vec<DataFile>,
62}
63
64impl Default for RowDeltaAction {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl RowDeltaAction {
71 pub fn new() -> Self {
73 Self {
74 check_duplicate: true,
75 commit_uuid: None,
76 snapshot_properties: HashMap::default(),
77 added_data_files: vec![],
78 added_delete_files: vec![],
79 }
80 }
81
82 pub fn with_check_duplicate(mut self, v: bool) -> Self {
84 self.check_duplicate = v;
85 self
86 }
87
88 pub fn add_data_files(mut self, data_files: impl IntoIterator<Item = DataFile>) -> Self {
90 self.added_data_files.extend(data_files);
91 self
92 }
93
94 pub fn add_delete_files(mut self, delete_files: impl IntoIterator<Item = DataFile>) -> Self {
99 self.added_delete_files.extend(delete_files);
100 self
101 }
102
103 pub fn set_commit_uuid(mut self, commit_uuid: Uuid) -> Self {
105 self.commit_uuid = Some(commit_uuid);
106 self
107 }
108
109 pub fn set_snapshot_properties(mut self, snapshot_properties: HashMap<String, String>) -> Self {
111 self.snapshot_properties = snapshot_properties;
112 self
113 }
114
115 fn validate_delete_files(delete_files: &[DataFile]) -> Result<()> {
117 for delete_file in delete_files {
118 match delete_file.content_type() {
119 DataContentType::PositionDeletes | DataContentType::EqualityDeletes => {
120 }
122 DataContentType::Data => {
123 return Err(Error::new(
124 ErrorKind::DataInvalid,
125 format!(
126 "File {} has content type Data but was added as a delete file. Use add_data_files() instead.",
127 delete_file.file_path()
128 ),
129 ));
130 }
131 }
132
133 if delete_file.content_type() == DataContentType::EqualityDeletes
135 && delete_file.equality_ids().is_none()
136 {
137 return Err(Error::new(
138 ErrorKind::DataInvalid,
139 format!(
140 "Equality delete file {} must have equality_ids set",
141 delete_file.file_path()
142 ),
143 ));
144 }
145 }
146 Ok(())
147 }
148}
149
150#[async_trait]
151impl TransactionAction for RowDeltaAction {
152 async fn commit(self: Arc<Self>, table: &Table) -> Result<ActionCommit> {
153 Self::validate_delete_files(&self.added_delete_files)?;
155
156 let snapshot_producer = SnapshotProducer::new(
157 table,
158 self.commit_uuid.unwrap_or_else(Uuid::now_v7),
159 self.snapshot_properties.clone(),
160 self.added_data_files.clone(),
161 self.added_delete_files.clone(),
162 );
163
164 if !self.added_data_files.is_empty() {
166 snapshot_producer.validate_added_data_files(&self.added_data_files)?;
167 }
168
169 if !self.added_delete_files.is_empty() {
171 snapshot_producer.validate_added_data_files(&self.added_delete_files)?;
172 }
173
174 if self.check_duplicate {
176 snapshot_producer.validate_duplicate_files().await?;
177 }
178
179 snapshot_producer
180 .commit(RowDeltaOperation, DefaultManifestProcess)
181 .await
182 }
183}
184
185struct RowDeltaOperation;
186
187impl SnapshotProduceOperation for RowDeltaOperation {
188 fn operation(&self) -> Operation {
189 Operation::Append
190 }
191
192 async fn delete_entries(
193 &self,
194 _snapshot_produce: &SnapshotProducer<'_>,
195 ) -> Result<Vec<ManifestEntry>> {
196 Ok(vec![])
197 }
198
199 async fn existing_manifest(
200 &self,
201 snapshot_produce: &SnapshotProducer<'_>,
202 ) -> Result<Vec<ManifestFile>> {
203 let Some(snapshot) = snapshot_produce.table.metadata().current_snapshot() else {
204 return Ok(vec![]);
205 };
206
207 let manifest_list = snapshot_produce
208 .table
209 .manifest_list_reader(snapshot)
210 .load()
211 .await?;
212
213 Ok(manifest_list
216 .entries()
217 .iter()
218 .filter(|entry| {
219 entry.has_added_files() || entry.has_existing_files() || entry.has_deleted_files()
220 })
221 .cloned()
222 .collect())
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use std::collections::HashMap;
229 use std::sync::Arc;
230
231 use crate::spec::{
232 DataContentType, DataFileBuilder, DataFileFormat, Literal, MAIN_BRANCH, SnapshotRef, Struct,
233 };
234 use crate::transaction::tests::make_v2_minimal_table;
235 use crate::transaction::{Transaction, TransactionAction};
236 use crate::{TableRequirement, TableUpdate};
237
238 #[tokio::test]
239 async fn test_row_delta_with_data_and_deletes() {
240 let table = make_v2_minimal_table();
241 let tx = Transaction::new(&table);
242
243 let data_file = DataFileBuilder::default()
244 .content(DataContentType::Data)
245 .file_path("test/data-1.parquet".to_string())
246 .file_format(DataFileFormat::Parquet)
247 .file_size_in_bytes(100)
248 .record_count(10)
249 .partition_spec_id(table.metadata().default_partition_spec_id())
250 .partition(Struct::from_iter([Some(Literal::long(100))]))
251 .build()
252 .unwrap();
253
254 let delete_file = DataFileBuilder::default()
255 .content(DataContentType::EqualityDeletes)
256 .file_path("test/delete-1.parquet".to_string())
257 .file_format(DataFileFormat::Parquet)
258 .file_size_in_bytes(50)
259 .record_count(5)
260 .partition_spec_id(table.metadata().default_partition_spec_id())
261 .partition(Struct::from_iter([Some(Literal::long(100))]))
262 .equality_ids(Some(vec![1])) .build()
264 .unwrap();
265
266 let action = tx
267 .row_delta()
268 .add_data_files(vec![data_file.clone()])
269 .add_delete_files(vec![delete_file.clone()]);
270
271 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
272 let updates = action_commit.take_updates();
273 let requirements = action_commit.take_requirements();
274
275 assert!(
277 matches!((&updates[0],&updates[1]), (TableUpdate::AddSnapshot { snapshot },TableUpdate::SetSnapshotRef { reference,ref_name }) if snapshot.snapshot_id() == reference.snapshot_id && ref_name == MAIN_BRANCH)
278 );
279 assert_eq!(
280 vec![
281 TableRequirement::UuidMatch {
282 uuid: table.metadata().uuid()
283 },
284 TableRequirement::RefSnapshotIdMatch {
285 r#ref: MAIN_BRANCH.to_string(),
286 snapshot_id: table.metadata().current_snapshot_id
287 }
288 ],
289 requirements
290 );
291
292 let new_snapshot: SnapshotRef = if let TableUpdate::AddSnapshot { snapshot } = &updates[0] {
294 SnapshotRef::new(snapshot.clone())
295 } else {
296 unreachable!()
297 };
298 let manifest_list = table
299 .manifest_list_reader(&new_snapshot)
300 .load()
301 .await
302 .unwrap();
303
304 assert_eq!(2, manifest_list.entries().len());
306 }
307
308 #[tokio::test]
309 async fn test_row_delta_rejects_data_file_as_delete() {
310 let table = make_v2_minimal_table();
311 let tx = Transaction::new(&table);
312
313 let data_file = DataFileBuilder::default()
314 .content(DataContentType::Data)
315 .file_path("test/data-1.parquet".to_string())
316 .file_format(DataFileFormat::Parquet)
317 .file_size_in_bytes(100)
318 .record_count(10)
319 .partition_spec_id(table.metadata().default_partition_spec_id())
320 .partition(Struct::from_iter([Some(Literal::long(100))]))
321 .build()
322 .unwrap();
323
324 let action = tx.row_delta().add_delete_files(vec![data_file]);
326
327 let result = Arc::new(action).commit(&table).await;
328 assert!(result.is_err());
329 let err = result.err().unwrap();
330 assert!(
331 err.to_string()
332 .contains("has content type Data but was added as a delete file")
333 );
334 }
335
336 #[tokio::test]
337 async fn test_row_delta_rejects_equality_delete_without_ids() {
338 let table = make_v2_minimal_table();
339 let tx = Transaction::new(&table);
340
341 let delete_file = DataFileBuilder::default()
342 .content(DataContentType::EqualityDeletes)
343 .file_path("test/delete-1.parquet".to_string())
344 .file_format(DataFileFormat::Parquet)
345 .file_size_in_bytes(50)
346 .record_count(5)
347 .partition_spec_id(table.metadata().default_partition_spec_id())
348 .partition(Struct::from_iter([Some(Literal::long(100))]))
349 .build()
351 .unwrap();
352
353 let action = tx.row_delta().add_delete_files(vec![delete_file]);
354
355 let result = Arc::new(action).commit(&table).await;
356 assert!(result.is_err());
357 let err = result.err().unwrap();
358 assert!(err.to_string().contains("must have equality_ids set"));
359 }
360
361 #[tokio::test]
362 async fn test_row_delta_with_only_deletes() {
363 let table = make_v2_minimal_table();
364 let tx = Transaction::new(&table);
365
366 let delete_file = DataFileBuilder::default()
367 .content(DataContentType::PositionDeletes)
368 .file_path("test/delete-1.parquet".to_string())
369 .file_format(DataFileFormat::Parquet)
370 .file_size_in_bytes(50)
371 .record_count(5)
372 .partition_spec_id(table.metadata().default_partition_spec_id())
373 .partition(Struct::from_iter([Some(Literal::long(100))]))
374 .build()
375 .unwrap();
376
377 let action = tx.row_delta().add_delete_files(vec![delete_file]);
378
379 let result = Arc::new(action).commit(&table).await;
380 assert!(result.is_ok());
381 }
382
383 #[tokio::test]
384 async fn test_row_delta_with_snapshot_properties() {
385 let table = make_v2_minimal_table();
386 let tx = Transaction::new(&table);
387
388 let mut snapshot_properties = HashMap::new();
389 snapshot_properties.insert("custom-key".to_string(), "custom-value".to_string());
390
391 let data_file = DataFileBuilder::default()
392 .content(DataContentType::Data)
393 .file_path("test/data-1.parquet".to_string())
394 .file_format(DataFileFormat::Parquet)
395 .file_size_in_bytes(100)
396 .record_count(10)
397 .partition_spec_id(table.metadata().default_partition_spec_id())
398 .partition(Struct::from_iter([Some(Literal::long(100))]))
399 .build()
400 .unwrap();
401
402 let action = tx
403 .row_delta()
404 .set_snapshot_properties(snapshot_properties)
405 .add_data_files(vec![data_file]);
406
407 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
408 let updates = action_commit.take_updates();
409
410 let new_snapshot = if let TableUpdate::AddSnapshot { snapshot } = &updates[0] {
411 snapshot
412 } else {
413 unreachable!()
414 };
415 assert_eq!(
416 new_snapshot
417 .summary()
418 .additional_properties
419 .get("custom-key")
420 .unwrap(),
421 "custom-value"
422 );
423 }
424}