1use std::cmp::min;
19use std::future::Future;
20use std::pin::Pin;
21
22use apache_avro::{Writer as AvroWriter, to_value};
23use bytes::Bytes;
24use itertools::Itertools;
25use serde_json::to_vec;
26
27use super::{
28 Datum, FormatVersion, ManifestContentType, PartitionSpec, PrimitiveType,
29 UNASSIGNED_SEQUENCE_NUMBER,
30};
31use crate::encryption::EncryptedOutputFile;
32use crate::error::Result;
33use crate::io::{FileWrite, OutputFile};
34use crate::spec::manifest::_serde::{ManifestEntryV1, ManifestEntryV2};
35use crate::spec::manifest::{manifest_schema_v1, manifest_schema_v2};
36use crate::spec::{
37 DataContentType, DataFile, FieldSummary, ManifestEntry, ManifestFile, ManifestMetadata,
38 ManifestStatus, PrimitiveLiteral, SchemaRef, StructType, Type,
39};
40use crate::{Error, ErrorKind};
41
42const UNASSIGNED_SNAPSHOT_ID: i64 = -1;
45
46type WriterFuture = Pin<Box<dyn Future<Output = Result<Box<dyn FileWrite>>> + Send>>;
47
48pub struct ManifestWriterBuilder {
50 writer_future: WriterFuture,
51 location: String,
52 snapshot_id: Option<i64>,
53 key_metadata: Option<Vec<u8>>,
54 schema: SchemaRef,
55 partition_spec: PartitionSpec,
56}
57
58impl ManifestWriterBuilder {
59 pub fn new(
61 output: OutputFile,
62 snapshot_id: Option<i64>,
63 schema: SchemaRef,
64 partition_spec: PartitionSpec,
65 ) -> Self {
66 let location = output.location().to_owned();
67 Self {
68 writer_future: Box::pin(async move { output.writer().await }),
69 location,
70 snapshot_id,
71 key_metadata: None,
72 schema,
73 partition_spec,
74 }
75 }
76
77 pub fn new_from_encrypted(
81 encrypted_output: EncryptedOutputFile,
82 snapshot_id: Option<i64>,
83 schema: SchemaRef,
84 partition_spec: PartitionSpec,
85 ) -> Result<Self> {
86 let location = encrypted_output.location().to_owned();
87 let key_metadata = Some(encrypted_output.key_metadata().encode()?.to_vec());
88 Ok(Self {
89 writer_future: Box::pin(async move { encrypted_output.writer().await }),
90 location,
91 snapshot_id,
92 key_metadata,
93 schema,
94 partition_spec,
95 })
96 }
97
98 pub fn build_v1(self) -> ManifestWriter {
100 let metadata = ManifestMetadata::builder()
101 .schema_id(self.schema.schema_id())
102 .schema(self.schema)
103 .partition_spec(self.partition_spec)
104 .format_version(FormatVersion::V1)
105 .content(ManifestContentType::Data)
106 .build();
107 ManifestWriter::new(
108 self.writer_future,
109 self.location,
110 self.snapshot_id,
111 self.key_metadata,
112 metadata,
113 None,
114 )
115 }
116
117 pub fn build_v2_data(self) -> ManifestWriter {
119 let metadata = ManifestMetadata::builder()
120 .schema_id(self.schema.schema_id())
121 .schema(self.schema)
122 .partition_spec(self.partition_spec)
123 .format_version(FormatVersion::V2)
124 .content(ManifestContentType::Data)
125 .build();
126
127 ManifestWriter::new(
128 self.writer_future,
129 self.location,
130 self.snapshot_id,
131 self.key_metadata,
132 metadata,
133 None,
134 )
135 }
136
137 pub fn build_v2_deletes(self) -> ManifestWriter {
139 let metadata = ManifestMetadata::builder()
140 .schema_id(self.schema.schema_id())
141 .schema(self.schema)
142 .partition_spec(self.partition_spec)
143 .format_version(FormatVersion::V2)
144 .content(ManifestContentType::Deletes)
145 .build();
146 ManifestWriter::new(
147 self.writer_future,
148 self.location,
149 self.snapshot_id,
150 self.key_metadata,
151 metadata,
152 None,
153 )
154 }
155
156 pub fn build_v3_data(self) -> ManifestWriter {
158 let metadata = ManifestMetadata::builder()
159 .schema_id(self.schema.schema_id())
160 .schema(self.schema)
161 .partition_spec(self.partition_spec)
162 .format_version(FormatVersion::V3)
163 .content(ManifestContentType::Data)
164 .build();
165 ManifestWriter::new(
166 self.writer_future,
167 self.location,
168 self.snapshot_id,
169 self.key_metadata,
170 metadata,
171 None,
174 )
175 }
176
177 pub fn build_v3_deletes(self) -> ManifestWriter {
179 let metadata = ManifestMetadata::builder()
180 .schema_id(self.schema.schema_id())
181 .schema(self.schema)
182 .partition_spec(self.partition_spec)
183 .format_version(FormatVersion::V3)
184 .content(ManifestContentType::Deletes)
185 .build();
186 ManifestWriter::new(
187 self.writer_future,
188 self.location,
189 self.snapshot_id,
190 self.key_metadata,
191 metadata,
192 None,
193 )
194 }
195}
196
197pub struct ManifestWriter {
199 writer_future: WriterFuture,
200 location: String,
201
202 snapshot_id: Option<i64>,
203
204 added_files: u32,
205 added_rows: u64,
206 existing_files: u32,
207 existing_rows: u64,
208 deleted_files: u32,
209 deleted_rows: u64,
210 first_row_id: Option<u64>,
211
212 min_seq_num: Option<i64>,
213
214 key_metadata: Option<Vec<u8>>,
215
216 manifest_entries: Vec<ManifestEntry>,
217
218 metadata: ManifestMetadata,
219}
220
221impl ManifestWriter {
222 pub(crate) fn new(
224 writer_future: WriterFuture,
225 location: String,
226 snapshot_id: Option<i64>,
227 key_metadata: Option<Vec<u8>>,
228 metadata: ManifestMetadata,
229 first_row_id: Option<u64>,
230 ) -> Self {
231 Self {
232 writer_future,
233 location,
234 snapshot_id,
235 added_files: 0,
236 added_rows: 0,
237 existing_files: 0,
238 existing_rows: 0,
239 deleted_files: 0,
240 deleted_rows: 0,
241 first_row_id,
242 min_seq_num: None,
243 key_metadata,
244 manifest_entries: Vec::new(),
245 metadata,
246 }
247 }
248
249 fn construct_partition_summaries(
250 &mut self,
251 partition_type: &StructType,
252 ) -> Result<Vec<FieldSummary>> {
253 let mut field_stats: Vec<_> = partition_type
254 .fields()
255 .iter()
256 .map(|f| PartitionFieldStats::new(f.field_type.as_primitive_type().unwrap().clone()))
257 .collect();
258 for partition in self.manifest_entries.iter().map(|e| &e.data_file.partition) {
259 for (literal, stat) in partition.iter().zip_eq(field_stats.iter_mut()) {
260 let primitive_literal = literal.map(|v| v.as_primitive_literal().unwrap());
261 stat.update(primitive_literal)?;
262 }
263 }
264 Ok(field_stats.into_iter().map(|stat| stat.finish()).collect())
265 }
266
267 fn check_data_file(&self, data_file: &DataFile) -> Result<()> {
268 match self.metadata.content {
269 ManifestContentType::Data => {
270 if data_file.content != DataContentType::Data {
271 return Err(Error::new(
272 ErrorKind::DataInvalid,
273 format!(
274 "Date file at path {} with manifest content type `data`, should have DataContentType `Data`, but has `{:?}`",
275 data_file.file_path(),
276 data_file.content
277 ),
278 ));
279 }
280 }
281 ManifestContentType::Deletes => {
282 if data_file.content != DataContentType::EqualityDeletes
283 && data_file.content != DataContentType::PositionDeletes
284 {
285 return Err(Error::new(
286 ErrorKind::DataInvalid,
287 format!(
288 "Date file at path {} with manifest content type `deletes`, should have DataContentType `Data`, but has `{:?}`",
289 data_file.file_path(),
290 data_file.content
291 ),
292 ));
293 }
294 }
295 }
296 Ok(())
297 }
298
299 pub(crate) fn add_entry(&mut self, mut entry: ManifestEntry) -> Result<()> {
305 self.check_data_file(&entry.data_file)?;
306 if entry.sequence_number().is_some_and(|n| n >= 0) {
307 entry.status = ManifestStatus::Added;
308 entry.snapshot_id = self.snapshot_id;
309 entry.file_sequence_number = None;
310 } else {
311 entry.status = ManifestStatus::Added;
312 entry.snapshot_id = self.snapshot_id;
313 entry.sequence_number = None;
314 entry.file_sequence_number = None;
315 };
316 self.add_entry_inner(entry)?;
317 Ok(())
318 }
319
320 pub fn add_file(&mut self, data_file: DataFile, sequence_number: i64) -> Result<()> {
324 self.check_data_file(&data_file)?;
325 let entry = ManifestEntry {
326 status: ManifestStatus::Added,
327 snapshot_id: self.snapshot_id,
328 sequence_number: (sequence_number >= 0).then_some(sequence_number),
329 file_sequence_number: None,
330 data_file,
331 };
332 self.add_entry_inner(entry)?;
333 Ok(())
334 }
335
336 #[allow(dead_code)]
343 pub(crate) fn add_delete_entry(&mut self, mut entry: ManifestEntry) -> Result<()> {
344 self.check_data_file(&entry.data_file)?;
345 entry.status = ManifestStatus::Deleted;
346 entry.snapshot_id = self.snapshot_id;
347 self.add_entry_inner(entry)?;
348 Ok(())
349 }
350
351 pub fn add_delete_file(
355 &mut self,
356 data_file: DataFile,
357 sequence_number: i64,
358 file_sequence_number: Option<i64>,
359 ) -> Result<()> {
360 self.check_data_file(&data_file)?;
361 let entry = ManifestEntry {
362 status: ManifestStatus::Deleted,
363 snapshot_id: self.snapshot_id,
364 sequence_number: Some(sequence_number),
365 file_sequence_number,
366 data_file,
367 };
368 self.add_entry_inner(entry)?;
369 Ok(())
370 }
371
372 #[allow(dead_code)]
378 pub(crate) fn add_existing_entry(&mut self, mut entry: ManifestEntry) -> Result<()> {
379 self.check_data_file(&entry.data_file)?;
380 entry.status = ManifestStatus::Existing;
381 self.add_entry_inner(entry)?;
382 Ok(())
383 }
384
385 pub fn add_existing_file(
388 &mut self,
389 data_file: DataFile,
390 snapshot_id: i64,
391 sequence_number: i64,
392 file_sequence_number: Option<i64>,
393 ) -> Result<()> {
394 self.check_data_file(&data_file)?;
395 let entry = ManifestEntry {
396 status: ManifestStatus::Existing,
397 snapshot_id: Some(snapshot_id),
398 sequence_number: Some(sequence_number),
399 file_sequence_number,
400 data_file,
401 };
402 self.add_entry_inner(entry)?;
403 Ok(())
404 }
405
406 fn add_entry_inner(&mut self, entry: ManifestEntry) -> Result<()> {
407 if (entry.status == ManifestStatus::Deleted || entry.status == ManifestStatus::Existing)
409 && (entry.sequence_number.is_none() || entry.file_sequence_number.is_none())
410 {
411 return Err(Error::new(
412 ErrorKind::DataInvalid,
413 "Manifest entry with status Existing or Deleted should have sequence number",
414 ));
415 }
416
417 match entry.status {
419 ManifestStatus::Added => {
420 self.added_files += 1;
421 self.added_rows += entry.data_file.record_count;
422 }
423 ManifestStatus::Deleted => {
424 self.deleted_files += 1;
425 self.deleted_rows += entry.data_file.record_count;
426 }
427 ManifestStatus::Existing => {
428 self.existing_files += 1;
429 self.existing_rows += entry.data_file.record_count;
430 }
431 }
432 if entry.is_alive()
433 && let Some(seq_num) = entry.sequence_number
434 {
435 self.min_seq_num = Some(self.min_seq_num.map_or(seq_num, |v| min(v, seq_num)));
436 }
437 self.manifest_entries.push(entry);
438 Ok(())
439 }
440
441 pub async fn write_manifest_file(mut self) -> Result<ManifestFile> {
443 let partition_type = self
445 .metadata
446 .partition_spec
447 .partition_type(&self.metadata.schema)?;
448 let partition_struct_type = Type::Struct(partition_type.clone());
451 let table_schema = &self.metadata.schema;
452 let avro_schema = match self.metadata.format_version {
453 FormatVersion::V1 => manifest_schema_v1(&partition_type)?,
454 FormatVersion::V2 | FormatVersion::V3 => manifest_schema_v2(&partition_type)?,
456 };
457 let mut avro_writer = AvroWriter::new(&avro_schema, Vec::new());
458 avro_writer.add_user_metadata(
459 "schema".to_string(),
460 to_vec(table_schema).map_err(|err| {
461 Error::new(ErrorKind::DataInvalid, "Fail to serialize table schema")
462 .with_source(err)
463 })?,
464 )?;
465 avro_writer.add_user_metadata(
466 "schema-id".to_string(),
467 table_schema.schema_id().to_string(),
468 )?;
469 avro_writer.add_user_metadata(
470 "partition-spec".to_string(),
471 to_vec(&self.metadata.partition_spec.fields()).map_err(|err| {
472 Error::new(ErrorKind::DataInvalid, "Fail to serialize partition spec")
473 .with_source(err)
474 })?,
475 )?;
476 avro_writer.add_user_metadata(
477 "partition-spec-id".to_string(),
478 self.metadata.partition_spec.spec_id().to_string(),
479 )?;
480 avro_writer.add_user_metadata(
481 "format-version".to_string(),
482 (self.metadata.format_version as u8).to_string(),
483 )?;
484 match self.metadata.format_version {
485 FormatVersion::V1 => {}
486 FormatVersion::V2 | FormatVersion::V3 => {
487 avro_writer
488 .add_user_metadata("content".to_string(), self.metadata.content.to_string())?;
489 }
490 }
491
492 let partition_summary = self.construct_partition_summaries(&partition_type)?;
493 for entry in std::mem::take(&mut self.manifest_entries) {
495 let value = match self.metadata.format_version {
496 FormatVersion::V1 => {
497 to_value(ManifestEntryV1::try_from(entry, &partition_struct_type)?)?
498 .resolve(&avro_schema)?
499 }
500 FormatVersion::V2 | FormatVersion::V3 => {
502 to_value(ManifestEntryV2::try_from(entry, &partition_struct_type)?)?
503 .resolve(&avro_schema)?
504 }
505 };
506
507 avro_writer.append(value)?;
508 }
509
510 let content = avro_writer.into_inner()?;
511 let length = content.len();
512 let mut writer = self.writer_future.await?;
513 writer.write(Bytes::from(content)).await?;
514 writer.close().await?;
515
516 Ok(ManifestFile {
517 manifest_path: self.location,
518 manifest_length: length as i64,
519 partition_spec_id: self.metadata.partition_spec.spec_id(),
520 content: self.metadata.content,
521 sequence_number: UNASSIGNED_SEQUENCE_NUMBER,
524 min_sequence_number: self.min_seq_num.unwrap_or(UNASSIGNED_SEQUENCE_NUMBER),
525 added_snapshot_id: self.snapshot_id.unwrap_or(UNASSIGNED_SNAPSHOT_ID),
526 added_files_count: Some(self.added_files),
527 existing_files_count: Some(self.existing_files),
528 deleted_files_count: Some(self.deleted_files),
529 added_rows_count: Some(self.added_rows),
530 existing_rows_count: Some(self.existing_rows),
531 deleted_rows_count: Some(self.deleted_rows),
532 partitions: Some(partition_summary),
533 key_metadata: self.key_metadata,
534 first_row_id: self.first_row_id,
535 })
536 }
537}
538
539struct PartitionFieldStats {
540 partition_type: PrimitiveType,
541
542 contains_null: bool,
543 contains_nan: Option<bool>,
544 lower_bound: Option<Datum>,
545 upper_bound: Option<Datum>,
546}
547
548impl PartitionFieldStats {
549 pub(crate) fn new(partition_type: PrimitiveType) -> Self {
550 Self {
551 partition_type,
552 contains_null: false,
553 contains_nan: Some(false),
554 upper_bound: None,
555 lower_bound: None,
556 }
557 }
558
559 pub(crate) fn update(&mut self, value: Option<PrimitiveLiteral>) -> Result<()> {
560 let Some(value) = value else {
561 self.contains_null = true;
562 return Ok(());
563 };
564 if !self.partition_type.compatible(&value) {
565 return Err(Error::new(
566 ErrorKind::DataInvalid,
567 "value is not compatible with type",
568 ));
569 }
570 let value = Datum::new(self.partition_type.clone(), value);
571
572 if value.is_nan() {
573 self.contains_nan = Some(true);
574 return Ok(());
575 }
576
577 self.lower_bound = Some(self.lower_bound.take().map_or(value.clone(), |original| {
578 if value < original {
579 value.clone()
580 } else {
581 original
582 }
583 }));
584 self.upper_bound = Some(self.upper_bound.take().map_or(value.clone(), |original| {
585 if value > original { value } else { original }
586 }));
587
588 Ok(())
589 }
590
591 pub(crate) fn finish(self) -> FieldSummary {
592 FieldSummary {
593 contains_null: self.contains_null,
594 contains_nan: self.contains_nan,
595 upper_bound: self.upper_bound.map(|v| v.to_bytes().unwrap()),
596 lower_bound: self.lower_bound.map(|v| v.to_bytes().unwrap()),
597 }
598 }
599}
600
601#[cfg(test)]
602mod tests {
603 use std::collections::HashMap;
604 use std::fs;
605 use std::sync::Arc;
606
607 use tempfile::TempDir;
608
609 use super::*;
610 use crate::io::FileIO;
611 use crate::spec::{DataFileFormat, Manifest, NestedField, PrimitiveType, Schema, Struct, Type};
612
613 #[tokio::test]
614 async fn test_add_delete_existing() {
615 let schema = Arc::new(
616 Schema::builder()
617 .with_fields(vec![
618 Arc::new(NestedField::optional(
619 1,
620 "id",
621 Type::Primitive(PrimitiveType::Int),
622 )),
623 Arc::new(NestedField::optional(
624 2,
625 "name",
626 Type::Primitive(PrimitiveType::String),
627 )),
628 ])
629 .build()
630 .unwrap(),
631 );
632 let metadata = ManifestMetadata {
633 schema_id: 0,
634 schema: schema.clone(),
635 partition_spec: PartitionSpec::builder(schema)
636 .with_spec_id(0)
637 .build()
638 .unwrap(),
639 content: ManifestContentType::Data,
640 format_version: FormatVersion::V2,
641 };
642 let mut entries = vec![
643 ManifestEntry {
644 status: ManifestStatus::Added,
645 snapshot_id: None,
646 sequence_number: Some(1),
647 file_sequence_number: Some(1),
648 data_file: DataFile {
649 content: DataContentType::Data,
650 file_path: "s3a://icebergdata/demo/s1/t1/data/00000-0-ba56fbfa-f2ff-40c9-bb27-565ad6dc2be8-00000.parquet".to_string(),
651 file_format: DataFileFormat::Parquet,
652 partition: Struct::empty(),
653 record_count: 1,
654 file_size_in_bytes: 5442,
655 column_sizes: HashMap::from([(1, 61), (2, 73)]),
656 value_counts: HashMap::from([(1, 1), (2, 1)]),
657 null_value_counts: HashMap::from([(1, 0), (2, 0)]),
658 nan_value_counts: HashMap::new(),
659 lower_bounds: HashMap::new(),
660 upper_bounds: HashMap::new(),
661 key_metadata: Some(Vec::new()),
662 split_offsets: Some(vec![4]),
663 equality_ids: None,
664 sort_order_id: None,
665 partition_spec_id: 0,
666 first_row_id: None,
667 referenced_data_file: None,
668 content_offset: None,
669 content_size_in_bytes: None,
670 },
671 },
672 ManifestEntry {
673 status: ManifestStatus::Deleted,
674 snapshot_id: Some(1),
675 sequence_number: Some(1),
676 file_sequence_number: Some(1),
677 data_file: DataFile {
678 content: DataContentType::Data,
679 file_path: "s3a://icebergdata/demo/s1/t1/data/00000-0-ba56fbfa-f2ff-40c9-bb27-565ad6dc2be8-00000.parquet".to_string(),
680 file_format: DataFileFormat::Parquet,
681 partition: Struct::empty(),
682 record_count: 1,
683 file_size_in_bytes: 5442,
684 column_sizes: HashMap::from([(1, 61), (2, 73)]),
685 value_counts: HashMap::from([(1, 1), (2, 1)]),
686 null_value_counts: HashMap::from([(1, 0), (2, 0)]),
687 nan_value_counts: HashMap::new(),
688 lower_bounds: HashMap::new(),
689 upper_bounds: HashMap::new(),
690 key_metadata: Some(Vec::new()),
691 split_offsets: Some(vec![4]),
692 equality_ids: None,
693 sort_order_id: None,
694 partition_spec_id: 0,
695 first_row_id: None,
696 referenced_data_file: None,
697 content_offset: None,
698 content_size_in_bytes: None,
699 },
700 },
701 ManifestEntry {
702 status: ManifestStatus::Existing,
703 snapshot_id: Some(1),
704 sequence_number: Some(1),
705 file_sequence_number: Some(1),
706 data_file: DataFile {
707 content: DataContentType::Data,
708 file_path: "s3a://icebergdata/demo/s1/t1/data/00000-0-ba56fbfa-f2ff-40c9-bb27-565ad6dc2be8-00000.parquet".to_string(),
709 file_format: DataFileFormat::Parquet,
710 partition: Struct::empty(),
711 record_count: 1,
712 file_size_in_bytes: 5442,
713 column_sizes: HashMap::from([(1, 61), (2, 73)]),
714 value_counts: HashMap::from([(1, 1), (2, 1)]),
715 null_value_counts: HashMap::from([(1, 0), (2, 0)]),
716 nan_value_counts: HashMap::new(),
717 lower_bounds: HashMap::new(),
718 upper_bounds: HashMap::new(),
719 key_metadata: Some(Vec::new()),
720 split_offsets: Some(vec![4]),
721 equality_ids: None,
722 sort_order_id: None,
723 partition_spec_id: 0,
724 first_row_id: None,
725 referenced_data_file: None,
726 content_offset: None,
727 content_size_in_bytes: None,
728 },
729 },
730 ];
731
732 let tmp_dir = TempDir::new().unwrap();
734 let path = tmp_dir.path().join("test_manifest.avro");
735 let io = FileIO::new_with_fs();
736 let output_file = io.new_output(path.to_str().unwrap()).unwrap();
737 let mut writer = ManifestWriterBuilder::new(
738 output_file,
739 Some(3),
740 metadata.schema.clone(),
741 metadata.partition_spec.clone(),
742 )
743 .build_v2_data();
744 writer.add_entry(entries[0].clone()).unwrap();
745 writer.add_delete_entry(entries[1].clone()).unwrap();
746 writer.add_existing_entry(entries[2].clone()).unwrap();
747 writer.write_manifest_file().await.unwrap();
748
749 let actual_manifest =
751 Manifest::parse_avro(fs::read(path).expect("read_file must succeed").as_slice())
752 .unwrap();
753
754 entries[0].snapshot_id = Some(3);
756 entries[1].snapshot_id = Some(3);
757 entries[0].file_sequence_number = None;
759 assert_eq!(actual_manifest, Manifest::new(metadata, entries));
760 }
761
762 #[tokio::test]
763 async fn test_v3_delete_manifest_delete_file_roundtrip() {
764 let schema = Arc::new(
765 Schema::builder()
766 .with_fields(vec![
767 Arc::new(NestedField::optional(
768 1,
769 "id",
770 Type::Primitive(PrimitiveType::Long),
771 )),
772 Arc::new(NestedField::optional(
773 2,
774 "data",
775 Type::Primitive(PrimitiveType::String),
776 )),
777 ])
778 .build()
779 .unwrap(),
780 );
781
782 let partition_spec = PartitionSpec::builder(schema.clone())
783 .with_spec_id(0)
784 .build()
785 .unwrap();
786
787 let delete_entry = ManifestEntry {
789 status: ManifestStatus::Added,
790 snapshot_id: None,
791 sequence_number: None,
792 file_sequence_number: None,
793 data_file: DataFile {
794 content: DataContentType::PositionDeletes,
795 file_path: "s3://bucket/table/data/delete-00000.parquet".to_string(),
796 file_format: DataFileFormat::Parquet,
797 partition: Struct::empty(),
798 record_count: 10,
799 file_size_in_bytes: 1024,
800 column_sizes: HashMap::new(),
801 value_counts: HashMap::new(),
802 null_value_counts: HashMap::new(),
803 nan_value_counts: HashMap::new(),
804 lower_bounds: HashMap::new(),
805 upper_bounds: HashMap::new(),
806 key_metadata: None,
807 split_offsets: None,
808 equality_ids: None,
809 sort_order_id: None,
810 partition_spec_id: 0,
811 first_row_id: None,
812 referenced_data_file: None,
813 content_offset: None,
814 content_size_in_bytes: None,
815 },
816 };
817
818 let tmp_dir = TempDir::new().unwrap();
820 let path = tmp_dir.path().join("v3_delete_manifest.avro");
821 let io = FileIO::new_with_fs();
822 let output_file = io.new_output(path.to_str().unwrap()).unwrap();
823
824 let mut writer = ManifestWriterBuilder::new(
825 output_file,
826 Some(1),
827 schema.clone(),
828 partition_spec.clone(),
829 )
830 .build_v3_deletes();
831
832 writer.add_entry(delete_entry).unwrap();
833 let manifest_file = writer.write_manifest_file().await.unwrap();
834
835 assert_eq!(manifest_file.content, ManifestContentType::Deletes);
837
838 let actual_manifest =
840 Manifest::parse_avro(fs::read(&path).expect("read_file must succeed").as_slice())
841 .unwrap();
842
843 assert_eq!(
845 actual_manifest.metadata().content,
846 ManifestContentType::Deletes,
847 );
848 }
849}