1use std::collections::{HashMap, HashSet};
19use std::sync::Arc;
20
21use async_trait::async_trait;
22use uuid::Uuid;
23
24use crate::error::Result;
25use crate::spec::{DataFile, ManifestEntry, ManifestFile, Operation};
26use crate::table::Table;
27use crate::transaction::snapshot::{
28 DefaultManifestProcess, SnapshotProduceOperation, SnapshotProducer,
29};
30use crate::transaction::{ActionCommit, TransactionAction};
31
32pub struct FastAppendAction {
34 check_duplicate: bool,
35 commit_uuid: Option<Uuid>,
37 snapshot_properties: HashMap<String, String>,
38 added_data_files: Vec<DataFile>,
39}
40
41impl FastAppendAction {
42 pub(crate) fn new() -> Self {
43 Self {
44 check_duplicate: true,
45 commit_uuid: None,
46 snapshot_properties: HashMap::default(),
47 added_data_files: vec![],
48 }
49 }
50
51 pub fn with_check_duplicate(mut self, v: bool) -> Self {
53 self.check_duplicate = v;
54 self
55 }
56
57 pub fn add_data_files(mut self, data_files: impl IntoIterator<Item = DataFile>) -> Self {
59 self.added_data_files.extend(data_files);
60 self
61 }
62
63 pub fn set_commit_uuid(mut self, commit_uuid: Uuid) -> Self {
65 self.commit_uuid = Some(commit_uuid);
66 self
67 }
68
69 pub fn set_snapshot_properties(mut self, snapshot_properties: HashMap<String, String>) -> Self {
71 self.snapshot_properties = snapshot_properties;
72 self
73 }
74
75 fn dedupe_added_files(&self) -> Vec<DataFile> {
79 let mut seen = HashSet::with_capacity(self.added_data_files.len());
80 self.added_data_files
81 .iter()
82 .filter(|data_file| seen.insert(data_file.file_path.as_str()))
83 .cloned()
84 .collect()
85 }
86}
87
88#[async_trait]
89impl TransactionAction for FastAppendAction {
90 async fn commit(self: Arc<Self>, table: &Table) -> Result<ActionCommit> {
91 let snapshot_producer = SnapshotProducer::new(
92 table,
93 self.commit_uuid.unwrap_or_else(Uuid::now_v7),
94 self.snapshot_properties.clone(),
95 self.dedupe_added_files(),
96 );
97
98 snapshot_producer.validate_added_data_files()?;
100
101 if self.check_duplicate {
103 snapshot_producer.validate_duplicate_files().await?;
104 }
105
106 snapshot_producer
107 .commit(FastAppendOperation, DefaultManifestProcess)
108 .await
109 }
110}
111
112struct FastAppendOperation;
113
114impl SnapshotProduceOperation for FastAppendOperation {
115 fn operation(&self) -> Operation {
116 Operation::Append
117 }
118
119 async fn delete_entries(
120 &self,
121 _snapshot_produce: &SnapshotProducer<'_>,
122 ) -> Result<Vec<ManifestEntry>> {
123 Ok(vec![])
124 }
125
126 async fn existing_manifest(
127 &self,
128 snapshot_produce: &SnapshotProducer<'_>,
129 ) -> Result<Vec<ManifestFile>> {
130 let Some(snapshot) = snapshot_produce.table.metadata().current_snapshot() else {
131 return Ok(vec![]);
132 };
133
134 let manifest_list = snapshot_produce
135 .table
136 .manifest_list_reader(snapshot)
137 .load()
138 .await?;
139
140 Ok(manifest_list
141 .entries()
142 .iter()
143 .filter(|entry| {
144 entry.has_added_files() || entry.has_existing_files() || entry.has_deleted_files()
148 })
149 .cloned()
150 .collect())
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use std::collections::HashMap;
157 use std::fs;
158 use std::sync::Arc;
159
160 use minijinja::{AutoEscape, Environment, Value, context};
161 use tempfile::TempDir;
162 use uuid::Uuid;
163
164 use crate::encryption::kms::MemoryKeyManagementClient;
165 use crate::encryption::{SensitiveBytes, StandardKeyMetadata};
166 use crate::io::FileIO;
167 use crate::spec::{
168 DataContentType, DataFile, DataFileBuilder, DataFileFormat, Literal, MAIN_BRANCH,
169 ManifestEntry, ManifestListWriter, ManifestStatus, ManifestWriterBuilder, SnapshotRef,
170 Struct, TableMetadata,
171 };
172 use crate::table::Table;
173 use crate::test_utils::{make_encrypted_table, test_runtime};
174 use crate::transaction::tests::make_v2_minimal_table;
175 use crate::transaction::{Transaction, TransactionAction};
176 use crate::{TableIdent, TableRequirement, TableUpdate};
177
178 fn render_template(template: &str, ctx: Value) -> String {
179 let mut env = Environment::new();
180 env.set_auto_escape_callback(|_| AutoEscape::None);
181 env.render_str(template, ctx).unwrap()
182 }
183
184 async fn make_table_with_delete_only_manifest() -> (Table, TempDir, String) {
191 let tmp_dir = TempDir::new().unwrap();
192 let table_location = tmp_dir.path().join("table1");
193 let manifest_list_location = table_location.join("metadata/manifests_list_1.avro");
194 let table_metadata_location = table_location.join("metadata/v1.json");
195
196 let file_io = FileIO::new_with_fs();
197
198 let template = fs::read_to_string(format!(
199 "{}/testdata/example_table_metadata_v2.json",
200 env!("CARGO_MANIFEST_DIR")
201 ))
202 .unwrap();
203 let metadata_json = render_template(&template, context! {
205 table_location => &table_location,
206 manifest_list_1_location => &manifest_list_location,
207 manifest_list_2_location => &manifest_list_location,
208 table_metadata_1_location => &table_metadata_location,
209 });
210 let table_metadata = serde_json::from_str::<TableMetadata>(&metadata_json).unwrap();
211
212 let table = Table::builder()
213 .metadata(table_metadata)
214 .identifier(TableIdent::from_strs(["db", "table1"]).unwrap())
215 .file_io(file_io)
216 .metadata_location(table_metadata_location.to_str().unwrap())
217 .runtime(test_runtime())
218 .build()
219 .unwrap();
220
221 let current_snapshot = table.metadata().current_snapshot().unwrap();
222 let schema = current_snapshot.schema(table.metadata()).unwrap();
223 let partition_spec = table.metadata().default_partition_spec();
224
225 let next_manifest_file = |location: &str| {
226 table
227 .file_io()
228 .new_output(format!(
229 "{}/metadata/manifest_{}.avro",
230 location,
231 Uuid::new_v4()
232 ))
233 .unwrap()
234 };
235 let table_location_str = table_location.to_str().unwrap().to_string();
236
237 let mut data_writer = ManifestWriterBuilder::new(
239 next_manifest_file(&table_location_str),
240 Some(current_snapshot.snapshot_id()),
241 schema.clone(),
242 partition_spec.as_ref().clone(),
243 )
244 .build_v2_data();
245 data_writer
246 .add_entry(
247 ManifestEntry::builder()
248 .status(ManifestStatus::Added)
249 .data_file(
250 DataFileBuilder::default()
251 .partition_spec_id(0)
252 .content(DataContentType::Data)
253 .file_path(format!("{table_location_str}/data.parquet"))
254 .file_format(DataFileFormat::Parquet)
255 .file_size_in_bytes(100)
256 .record_count(1)
257 .partition(Struct::from_iter([Some(Literal::long(100))]))
258 .build()
259 .unwrap(),
260 )
261 .build(),
262 )
263 .unwrap();
264 let data_manifest = data_writer.write_manifest_file().await.unwrap();
265
266 let mut delete_writer = ManifestWriterBuilder::new(
268 next_manifest_file(&table_location_str),
269 Some(current_snapshot.snapshot_id()),
270 schema.clone(),
271 partition_spec.as_ref().clone(),
272 )
273 .build_v2_data();
274 delete_writer
275 .add_delete_entry(
276 ManifestEntry::builder()
277 .status(ManifestStatus::Deleted)
278 .sequence_number(0)
279 .file_sequence_number(0)
280 .data_file(
281 DataFileBuilder::default()
282 .partition_spec_id(0)
283 .content(DataContentType::Data)
284 .file_path(format!("{table_location_str}/removed.parquet"))
285 .file_format(DataFileFormat::Parquet)
286 .file_size_in_bytes(100)
287 .record_count(1)
288 .partition(Struct::from_iter([Some(Literal::long(100))]))
289 .build()
290 .unwrap(),
291 )
292 .build(),
293 )
294 .unwrap();
295 let delete_manifest = delete_writer.write_manifest_file().await.unwrap();
296 let delete_manifest_path = delete_manifest.manifest_path.clone();
297
298 assert!(delete_manifest.has_deleted_files());
300 assert!(!delete_manifest.has_added_files());
301 assert!(!delete_manifest.has_existing_files());
302
303 let mut manifest_list_writer = ManifestListWriter::v2(
304 table
305 .file_io()
306 .new_output(current_snapshot.manifest_list())
307 .unwrap()
308 .writer()
309 .await
310 .unwrap(),
311 current_snapshot.snapshot_id(),
312 current_snapshot.parent_snapshot_id(),
313 current_snapshot.sequence_number(),
314 );
315 manifest_list_writer
316 .add_manifests(vec![data_manifest, delete_manifest].into_iter())
317 .unwrap();
318 manifest_list_writer.close().await.unwrap();
319
320 (table, tmp_dir, delete_manifest_path)
321 }
322
323 #[tokio::test]
327 async fn test_fast_append_preserves_delete_only_manifest() {
328 let (table, _tmp_dir, delete_manifest_path) = make_table_with_delete_only_manifest().await;
329
330 let new_file = DataFileBuilder::default()
332 .content(DataContentType::Data)
333 .file_path(format!("{}/appended.parquet", table.metadata().location()))
334 .file_format(DataFileFormat::Parquet)
335 .file_size_in_bytes(100)
336 .record_count(1)
337 .partition_spec_id(table.metadata().default_partition_spec_id())
338 .partition(Struct::from_iter([Some(Literal::long(100))]))
339 .build()
340 .unwrap();
341
342 let tx = Transaction::new(&table);
343 let action = tx.fast_append().add_data_files(vec![new_file]);
344 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
345 let updates = action_commit.take_updates();
346
347 let new_snapshot: SnapshotRef = if let TableUpdate::AddSnapshot { snapshot } = &updates[0] {
348 SnapshotRef::new(snapshot.clone())
349 } else {
350 unreachable!("first update of a fast append should be AddSnapshot")
351 };
352
353 let manifest_list = table
354 .manifest_list_reader(&new_snapshot)
355 .load()
356 .await
357 .unwrap();
358
359 assert!(
360 manifest_list
361 .entries()
362 .iter()
363 .any(|m| m.manifest_path == delete_manifest_path),
364 "delete-only manifest {delete_manifest_path} was dropped from the new snapshot's \
365 manifest list; the files it removed would reappear as live data"
366 );
367 }
368
369 async fn committed_data_files(table: &Table, updates: &[TableUpdate]) -> Vec<DataFile> {
371 let TableUpdate::AddSnapshot { snapshot } = &updates[0] else {
372 unreachable!("first update is always AddSnapshot")
373 };
374 let manifest_list = table
375 .manifest_list_reader(&SnapshotRef::new(snapshot.clone()))
376 .load()
377 .await
378 .unwrap();
379 assert_eq!(1, manifest_list.entries().len());
380 table
381 .manifest_reader()
382 .read(&manifest_list.entries()[0])
383 .await
384 .unwrap()
385 .entries()
386 .iter()
387 .map(|entry| entry.data_file().clone())
388 .collect()
389 }
390
391 #[tokio::test]
392 async fn test_fast_append_writes_encrypted_manifest() {
393 let table = make_encrypted_table().await;
394 assert!(
395 table.encryption_manager().is_some(),
396 "fixture table should have an EncryptionManager"
397 );
398
399 let new_file = DataFileBuilder::default()
400 .content(DataContentType::Data)
401 .file_path("memory:///table/data/00000.parquet".to_string())
402 .file_format(DataFileFormat::Parquet)
403 .partition(Struct::empty())
404 .record_count(100)
405 .file_size_in_bytes(4096)
406 .partition_spec_id(table.metadata().default_partition_spec_id())
407 .build()
408 .unwrap();
409
410 let tx = Transaction::new(&table);
411 let action = tx.fast_append().add_data_files(vec![new_file]);
412 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
413 let updates = action_commit.take_updates();
414
415 let new_snapshot: SnapshotRef = updates
416 .iter()
417 .find_map(|u| match u {
418 TableUpdate::AddSnapshot { snapshot } => Some(SnapshotRef::new(snapshot.clone())),
419 _ => None,
420 })
421 .expect("a fast append should emit an AddSnapshot update");
422
423 let manifest_list = table
424 .manifest_list_reader(&new_snapshot)
425 .load()
426 .await
427 .unwrap();
428 let manifest_file = manifest_list
429 .entries()
430 .iter()
431 .find(|m| m.added_files_count.unwrap_or(0) > 0)
432 .expect("new snapshot should carry the appended data manifest");
433
434 let key_metadata_bytes = manifest_file
436 .key_metadata
437 .as_ref()
438 .expect("encrypted manifest must record key metadata");
439 StandardKeyMetadata::decode(key_metadata_bytes)
440 .expect("recorded key metadata must decode as StandardKeyMetadata");
441
442 let manifest = table.manifest_reader().read(manifest_file).await.unwrap();
447 assert_eq!(manifest.entries().len(), 1);
448 assert_eq!(
449 manifest.entries()[0].data_file().file_path(),
450 "memory:///table/data/00000.parquet"
451 );
452 }
453
454 #[tokio::test]
455 async fn test_empty_data_append_action() {
456 let table = make_v2_minimal_table();
457 let tx = Transaction::new(&table);
458 let action = tx.fast_append().add_data_files(vec![]);
459 assert!(Arc::new(action).commit(&table).await.is_err());
460 }
461
462 #[tokio::test]
466 async fn test_fast_append_honors_write_metadata_path() {
467 let base = make_v2_minimal_table();
468 let metadata_root = format!("{}/custom-meta", base.metadata().location());
469 let metadata = base
470 .metadata()
471 .clone()
472 .into_builder(None)
473 .set_properties(HashMap::from([(
474 "write.metadata.path".to_string(),
475 metadata_root.clone(),
476 )]))
477 .unwrap()
478 .build()
479 .unwrap()
480 .metadata;
481 let table = base.with_metadata(Arc::new(metadata));
482
483 let data_file = DataFileBuilder::default()
484 .content(DataContentType::Data)
485 .file_path(format!("{}/data/1.parquet", table.metadata().location()))
486 .file_format(DataFileFormat::Parquet)
487 .file_size_in_bytes(100)
488 .record_count(1)
489 .partition_spec_id(table.metadata().default_partition_spec_id())
490 .partition(Struct::from_iter([Some(Literal::long(300))]))
491 .build()
492 .unwrap();
493
494 let tx = Transaction::new(&table);
495 let action = tx.fast_append().add_data_files(vec![data_file]);
496 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
497 let updates = action_commit.take_updates();
498
499 let new_snapshot: SnapshotRef = if let TableUpdate::AddSnapshot { snapshot } = &updates[0] {
500 SnapshotRef::new(snapshot.clone())
501 } else {
502 unreachable!("first update of a fast append should be AddSnapshot")
503 };
504
505 let prefix = format!("{metadata_root}/");
506
507 assert!(
509 new_snapshot.manifest_list().starts_with(prefix.as_str()),
510 "manifest list {} not under configured write.metadata.path {metadata_root}",
511 new_snapshot.manifest_list()
512 );
513
514 let manifest_list = table
516 .manifest_list_reader(&new_snapshot)
517 .load()
518 .await
519 .unwrap();
520 assert!(
521 !manifest_list.entries().is_empty(),
522 "expected at least one manifest entry"
523 );
524 for entry in manifest_list.entries() {
525 assert!(
526 entry.manifest_path.starts_with(prefix.as_str()),
527 "manifest {} not under configured write.metadata.path {metadata_root}",
528 entry.manifest_path
529 );
530 }
531 }
532
533 #[tokio::test]
534 async fn test_set_snapshot_properties() {
535 let table = make_v2_minimal_table();
536 let tx = Transaction::new(&table);
537
538 let mut snapshot_properties = HashMap::new();
539 snapshot_properties.insert("key".to_string(), "val".to_string());
540
541 let data_file = DataFileBuilder::default()
542 .content(DataContentType::Data)
543 .file_path("test/1.parquet".to_string())
544 .file_format(DataFileFormat::Parquet)
545 .file_size_in_bytes(100)
546 .record_count(1)
547 .partition_spec_id(table.metadata().default_partition_spec_id())
548 .partition(Struct::from_iter([Some(Literal::long(300))]))
549 .build()
550 .unwrap();
551
552 let action = tx
553 .fast_append()
554 .set_snapshot_properties(snapshot_properties)
555 .add_data_files(vec![data_file]);
556 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
557 let updates = action_commit.take_updates();
558
559 let new_snapshot = if let TableUpdate::AddSnapshot { snapshot } = &updates[0] {
561 snapshot
562 } else {
563 unreachable!()
564 };
565 assert_eq!(
566 new_snapshot
567 .summary()
568 .additional_properties
569 .get("key")
570 .unwrap(),
571 "val"
572 );
573 }
574
575 const FIXTURE_MASTER_KEY_ID: &str = "master-1";
577 const FIXTURE_MASTER_KEY_BYTES: [u8; 16] = [
578 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
579 0x0f,
580 ];
581
582 async fn make_v3_encrypted_table() -> Table {
583 let json = fs::read_to_string(format!(
584 "{}/testdata/table_metadata/TableMetadataV3ValidEncryption.json",
585 env!("CARGO_MANIFEST_DIR")
586 ))
587 .unwrap();
588 let metadata = serde_json::from_str::<TableMetadata>(&json).unwrap();
589
590 let kms = MemoryKeyManagementClient::new();
591 kms.add_master_key_bytes(
592 FIXTURE_MASTER_KEY_ID,
593 SensitiveBytes::new(FIXTURE_MASTER_KEY_BYTES),
594 )
595 .unwrap();
596
597 let file_io = FileIO::new_with_memory();
598
599 let manifest_list_bytes = fs::read(format!(
600 "{}/testdata/manifests_lists/manifest-list-v3-encrypted.avro",
601 env!("CARGO_MANIFEST_DIR")
602 ))
603 .unwrap();
604 let parent_manifest_list = metadata.current_snapshot().unwrap().manifest_list();
605 file_io
606 .new_output(parent_manifest_list)
607 .unwrap()
608 .write(manifest_list_bytes.into())
609 .await
610 .unwrap();
611
612 Table::builder()
613 .metadata(metadata)
614 .metadata_location("memory:///table/metadata/v1.json")
615 .identifier(TableIdent::from_strs(["ns1", "enc"]).unwrap())
616 .file_io(file_io)
617 .kms_client(Arc::new(kms))
618 .runtime(test_runtime())
619 .build()
620 .unwrap()
621 }
622
623 #[tokio::test]
624 async fn test_commit_with_encryption_adds_keys_and_records_snapshot_key_id() {
625 let table = make_v3_encrypted_table().await;
626
627 let data_file = DataFileBuilder::default()
628 .content(DataContentType::Data)
629 .file_path("test/1.parquet".to_string())
630 .file_format(DataFileFormat::Parquet)
631 .file_size_in_bytes(100)
632 .record_count(1)
633 .partition_spec_id(table.metadata().default_partition_spec_id())
634 .partition(Struct::empty())
635 .build()
636 .unwrap();
637
638 let tx = Transaction::new(&table);
639 let action = tx.fast_append().add_data_files(vec![data_file]);
640 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
641 let updates = action_commit.take_updates();
642
643 let added_key_ids: Vec<String> = updates
644 .iter()
645 .filter_map(|u| match u {
646 TableUpdate::AddEncryptionKey { encryption_key } => {
647 Some(encryption_key.key_id().to_string())
648 }
649 _ => None,
650 })
651 .collect();
652 assert!(!added_key_ids.is_empty(), "got {updates:?}");
653
654 let new_snapshot = updates
656 .iter()
657 .find_map(|u| match u {
658 TableUpdate::AddSnapshot { snapshot } => Some(snapshot),
659 _ => None,
660 })
661 .expect("commit should add a snapshot");
662
663 let snapshot_key_id = new_snapshot
664 .encryption_key_id()
665 .expect("encrypted snapshot should record its manifest-list key id");
666 assert!(
667 added_key_ids.iter().any(|id| id == snapshot_key_id),
668 "snapshot key id {snapshot_key_id} not in added keys {added_key_ids:?}"
669 );
670
671 let new_snapshot_ref: SnapshotRef = Arc::new(new_snapshot.clone());
672 let manifest_list = table
673 .manifest_list_reader(&new_snapshot_ref)
674 .load()
675 .await
676 .expect("newly written encrypted manifest list should decrypt and parse");
677 assert_eq!(
678 manifest_list.entries().len(),
679 1,
680 "append should record exactly the one new data manifest"
681 );
682 }
683
684 #[tokio::test]
685 async fn test_snapshot_properties_cannot_override_computed_metrics() {
686 let table = make_v2_minimal_table();
691 let tx = Transaction::new(&table);
692
693 let mut snapshot_properties = HashMap::new();
694 snapshot_properties.insert("added-data-files".to_string(), "9999".to_string());
697 snapshot_properties.insert("added-records".to_string(), "not-a-number".to_string());
698
699 let data_file = DataFileBuilder::default()
700 .content(DataContentType::Data)
701 .file_path("test/1.parquet".to_string())
702 .file_format(DataFileFormat::Parquet)
703 .file_size_in_bytes(100)
704 .record_count(1)
705 .partition_spec_id(table.metadata().default_partition_spec_id())
706 .partition(Struct::from_iter([Some(Literal::long(300))]))
707 .build()
708 .unwrap();
709
710 let action = tx
711 .fast_append()
712 .set_snapshot_properties(snapshot_properties)
713 .add_data_files(vec![data_file]);
714 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
716 let updates = action_commit.take_updates();
717
718 let new_snapshot = if let TableUpdate::AddSnapshot { snapshot } = &updates[0] {
719 snapshot
720 } else {
721 unreachable!()
722 };
723 let props = &new_snapshot.summary().additional_properties;
724
725 assert_eq!(
727 props.get("added-data-files").unwrap(),
728 "1",
729 "computed added-data-files must override the user-supplied value"
730 );
731 assert_eq!(
732 props.get("added-records").unwrap(),
733 "1",
734 "computed added-records must override the user-supplied non-integer value"
735 );
736 }
737
738 #[tokio::test]
739 async fn test_append_snapshot_properties() {
740 let table = make_v2_minimal_table();
741 let tx = Transaction::new(&table);
742
743 let mut snapshot_properties = HashMap::new();
744 snapshot_properties.insert("key".to_string(), "val".to_string());
745
746 let action = tx
747 .fast_append()
748 .set_snapshot_properties(snapshot_properties);
749 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
750 let updates = action_commit.take_updates();
751
752 let new_snapshot = if let TableUpdate::AddSnapshot { snapshot } = &updates[0] {
754 snapshot
755 } else {
756 unreachable!()
757 };
758 assert_eq!(
759 new_snapshot
760 .summary()
761 .additional_properties
762 .get("key")
763 .unwrap(),
764 "val"
765 );
766 }
767
768 #[tokio::test]
769 async fn test_fast_append_file_with_incompatible_partition_value() {
770 let table = make_v2_minimal_table();
771 let tx = Transaction::new(&table);
772 let action = tx.fast_append();
773
774 let data_file = DataFileBuilder::default()
776 .content(DataContentType::Data)
777 .file_path("test/3.parquet".to_string())
778 .file_format(DataFileFormat::Parquet)
779 .file_size_in_bytes(100)
780 .record_count(1)
781 .partition_spec_id(table.metadata().default_partition_spec_id())
782 .partition(Struct::from_iter([Some(Literal::string("test"))]))
783 .build()
784 .unwrap();
785
786 let action = action.add_data_files(vec![data_file.clone()]);
787
788 assert!(Arc::new(action).commit(&table).await.is_err());
789 }
790
791 #[tokio::test]
792 async fn test_fast_append_dedupes_intra_batch_duplicate_paths() {
793 let table = make_v2_minimal_table();
794 let tx = Transaction::new(&table);
795
796 let make_file = |size: u64, records: u64| {
797 DataFileBuilder::default()
798 .content(DataContentType::Data)
799 .file_path("test/dup.parquet".to_string())
800 .file_format(DataFileFormat::Parquet)
801 .file_size_in_bytes(size)
802 .record_count(records)
803 .partition_spec_id(table.metadata().default_partition_spec_id())
804 .partition(Struct::from_iter([Some(Literal::long(1))]))
805 .build()
806 .unwrap()
807 };
808
809 let action = tx.fast_append().add_data_files(vec![
811 make_file(100, 10),
812 make_file(200, 20),
813 make_file(300, 30),
814 ]);
815 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
816 let files = committed_data_files(&table, &action_commit.take_updates()).await;
817 assert_eq!(1, files.len());
818 assert_eq!(100, files[0].file_size_in_bytes());
819 }
820
821 #[tokio::test]
822 async fn test_fast_append_dedupes_regardless_of_check_duplicate_flag() {
823 let table = make_v2_minimal_table();
824 let tx = Transaction::new(&table);
825
826 let make_file = || {
827 DataFileBuilder::default()
828 .content(DataContentType::Data)
829 .file_path("test/dup.parquet".to_string())
830 .file_format(DataFileFormat::Parquet)
831 .file_size_in_bytes(100)
832 .record_count(10)
833 .partition_spec_id(table.metadata().default_partition_spec_id())
834 .partition(Struct::from_iter([Some(Literal::long(1))]))
835 .build()
836 .unwrap()
837 };
838
839 let action = tx
841 .fast_append()
842 .with_check_duplicate(false)
843 .add_data_files(vec![make_file(), make_file()]);
844 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
845 let files = committed_data_files(&table, &action_commit.take_updates()).await;
846 assert_eq!(1, files.len());
847 }
848
849 #[tokio::test]
850 async fn test_fast_append() {
851 let table = make_v2_minimal_table();
852 let tx = Transaction::new(&table);
853 let action = tx.fast_append();
854
855 let data_file = DataFileBuilder::default()
856 .content(DataContentType::Data)
857 .file_path("test/3.parquet".to_string())
858 .file_format(DataFileFormat::Parquet)
859 .file_size_in_bytes(100)
860 .record_count(1)
861 .partition_spec_id(table.metadata().default_partition_spec_id())
862 .partition(Struct::from_iter([Some(Literal::long(300))]))
863 .build()
864 .unwrap();
865
866 let action = action.add_data_files(vec![data_file.clone()]);
867 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
868 let updates = action_commit.take_updates();
869 let requirements = action_commit.take_requirements();
870
871 assert!(
873 matches!((&updates[0],&updates[1]), (TableUpdate::AddSnapshot { snapshot },TableUpdate::SetSnapshotRef { reference,ref_name }) if snapshot.snapshot_id() == reference.snapshot_id && ref_name == MAIN_BRANCH)
874 );
875 assert_eq!(
876 vec![
877 TableRequirement::UuidMatch {
878 uuid: table.metadata().uuid()
879 },
880 TableRequirement::RefSnapshotIdMatch {
881 r#ref: MAIN_BRANCH.to_string(),
882 snapshot_id: table.metadata().current_snapshot_id
883 }
884 ],
885 requirements
886 );
887
888 let new_snapshot: SnapshotRef = if let TableUpdate::AddSnapshot { snapshot } = &updates[0] {
890 SnapshotRef::new(snapshot.clone())
891 } else {
892 unreachable!()
893 };
894 let manifest_list = table
895 .manifest_list_reader(&new_snapshot)
896 .load()
897 .await
898 .unwrap();
899 assert_eq!(1, manifest_list.entries().len());
900 assert_eq!(
901 manifest_list.entries()[0].sequence_number,
902 new_snapshot.sequence_number()
903 );
904
905 let manifest = table
907 .manifest_reader()
908 .read(&manifest_list.entries()[0])
909 .await
910 .unwrap();
911 assert_eq!(1, manifest.entries().len());
912 assert_eq!(
913 new_snapshot.sequence_number(),
914 manifest.entries()[0]
915 .sequence_number()
916 .expect("Inherit sequence number by load manifest")
917 );
918
919 assert_eq!(
920 new_snapshot.snapshot_id(),
921 manifest.entries()[0].snapshot_id().unwrap()
922 );
923 assert_eq!(data_file, *manifest.entries()[0].data_file());
924 }
925}