1mod _serde;
19
20mod data_file;
21pub use data_file::*;
22mod entry;
23pub use entry::*;
24mod metadata;
25pub use metadata::*;
26mod writer;
27use std::sync::Arc;
28
29use apache_avro::{Reader as AvroReader, from_value};
30pub use writer::*;
31
32use super::{
33 Datum, FormatVersion, ManifestContentType, PartitionSpec, PrimitiveType, Schema, Struct,
34 UNASSIGNED_SEQUENCE_NUMBER,
35};
36use crate::error::Result;
37use crate::{Error, ErrorKind};
38
39#[derive(Debug, PartialEq, Eq, Clone)]
41pub struct Manifest {
42 metadata: ManifestMetadata,
43 entries: Vec<ManifestEntryRef>,
44}
45
46impl Manifest {
47 pub(crate) fn try_from_avro_bytes(bs: &[u8]) -> Result<(ManifestMetadata, Vec<ManifestEntry>)> {
49 let reader = AvroReader::new(bs)?;
50
51 let meta = reader.user_metadata();
53 let metadata = ManifestMetadata::parse(meta)?;
54
55 let partition_type = metadata.partition_spec.partition_type(&metadata.schema)?;
57
58 let entries = match metadata.format_version {
59 FormatVersion::V1 => {
60 let schema = manifest_schema_v1(&partition_type)?;
61 let reader = AvroReader::with_schema(&schema, bs)?;
62 reader
63 .into_iter()
64 .map(|value| {
65 from_value::<_serde::ManifestEntryV1>(&value?)?.try_into(
66 metadata.partition_spec.spec_id(),
67 &partition_type,
68 &metadata.schema,
69 )
70 })
71 .collect::<Result<Vec<_>>>()?
72 }
73 FormatVersion::V2 | FormatVersion::V3 => {
75 let schema = manifest_schema_v2(&partition_type)?;
76 let reader = AvroReader::with_schema(&schema, bs)?;
77 reader
78 .into_iter()
79 .map(|value| {
80 from_value::<_serde::ManifestEntryV2>(&value?)?.try_into(
81 metadata.partition_spec.spec_id(),
82 &partition_type,
83 &metadata.schema,
84 )
85 })
86 .collect::<Result<Vec<_>>>()?
87 }
88 };
89
90 Ok((metadata, entries))
91 }
92
93 pub fn parse_avro(bs: &[u8]) -> Result<Self> {
95 let (metadata, entries) = Self::try_from_avro_bytes(bs)?;
96 Ok(Self::new(metadata, entries))
97 }
98
99 pub fn entries(&self) -> &[ManifestEntryRef] {
101 &self.entries
102 }
103
104 pub fn metadata(&self) -> &ManifestMetadata {
106 &self.metadata
107 }
108
109 pub fn into_parts(self) -> (Vec<ManifestEntryRef>, ManifestMetadata) {
111 let Self { entries, metadata } = self;
112 (entries, metadata)
113 }
114
115 pub fn new(metadata: ManifestMetadata, entries: Vec<ManifestEntry>) -> Self {
117 Self {
118 metadata,
119 entries: entries.into_iter().map(Arc::new).collect(),
120 }
121 }
122}
123
124pub fn serialize_data_file_to_json(
126 data_file: DataFile,
127 partition_type: &super::StructType,
128 format_version: FormatVersion,
129) -> Result<String> {
130 let serde = _serde::DataFileSerde::try_from(data_file, partition_type, format_version)?;
131 serde_json::to_string(&serde).map_err(|e| {
132 Error::new(
133 ErrorKind::DataInvalid,
134 "Failed to serialize DataFile to JSON!".to_string(),
135 )
136 .with_source(e)
137 })
138}
139
140pub fn deserialize_data_file_from_json(
142 json: &str,
143 partition_spec_id: i32,
144 partition_type: &super::StructType,
145 schema: &Schema,
146) -> Result<DataFile> {
147 let serde = serde_json::from_str::<_serde::DataFileSerde>(json).map_err(|e| {
148 Error::new(
149 ErrorKind::DataInvalid,
150 "Failed to deserialize JSON to DataFile!".to_string(),
151 )
152 .with_source(e)
153 })?;
154
155 serde.try_into(partition_spec_id, partition_type, schema)
156}
157
158#[cfg(test)]
159mod tests {
160 use std::collections::HashMap;
161 use std::fs;
162 use std::sync::Arc;
163
164 use apache_avro::{Codec, Writer, to_value};
165 use serde_json::{Value, to_vec};
166 use tempfile::TempDir;
167
168 use super::*;
169 use crate::io::FileIO;
170 use crate::spec::{Literal, NestedField, PrimitiveType, Struct, Transform, Type};
171
172 #[tokio::test]
173 async fn test_parse_manifest_v2_unpartition() {
174 let schema = Arc::new(
175 Schema::builder()
176 .with_fields(vec![
177 Arc::new(NestedField::optional(
179 1,
180 "id",
181 Type::Primitive(PrimitiveType::Long),
182 )),
183 Arc::new(NestedField::optional(
184 2,
185 "v_int",
186 Type::Primitive(PrimitiveType::Int),
187 )),
188 Arc::new(NestedField::optional(
189 3,
190 "v_long",
191 Type::Primitive(PrimitiveType::Long),
192 )),
193 Arc::new(NestedField::optional(
194 4,
195 "v_float",
196 Type::Primitive(PrimitiveType::Float),
197 )),
198 Arc::new(NestedField::optional(
199 5,
200 "v_double",
201 Type::Primitive(PrimitiveType::Double),
202 )),
203 Arc::new(NestedField::optional(
204 6,
205 "v_varchar",
206 Type::Primitive(PrimitiveType::String),
207 )),
208 Arc::new(NestedField::optional(
209 7,
210 "v_bool",
211 Type::Primitive(PrimitiveType::Boolean),
212 )),
213 Arc::new(NestedField::optional(
214 8,
215 "v_date",
216 Type::Primitive(PrimitiveType::Date),
217 )),
218 Arc::new(NestedField::optional(
219 9,
220 "v_timestamp",
221 Type::Primitive(PrimitiveType::Timestamptz),
222 )),
223 Arc::new(NestedField::optional(
224 10,
225 "v_decimal",
226 Type::Primitive(PrimitiveType::Decimal {
227 precision: 36,
228 scale: 10,
229 }),
230 )),
231 Arc::new(NestedField::optional(
232 11,
233 "v_ts_ntz",
234 Type::Primitive(PrimitiveType::Timestamp),
235 )),
236 Arc::new(NestedField::optional(
237 12,
238 "v_ts_ns_ntz",
239 Type::Primitive(PrimitiveType::TimestampNs),
240 )),
241 ])
242 .build()
243 .unwrap(),
244 );
245 let metadata = ManifestMetadata {
246 schema_id: 0,
247 schema: schema.clone(),
248 partition_spec: PartitionSpec::builder(schema)
249 .with_spec_id(0)
250 .build()
251 .unwrap(),
252 content: ManifestContentType::Data,
253 format_version: FormatVersion::V2,
254 };
255 let mut entries = vec![
256 ManifestEntry {
257 status: ManifestStatus::Added,
258 snapshot_id: None,
259 sequence_number: None,
260 file_sequence_number: None,
261 data_file: DataFile {content:DataContentType::Data,file_path:"s3a://icebergdata/demo/s1/t1/data/00000-0-ba56fbfa-f2ff-40c9-bb27-565ad6dc2be8-00000.parquet".to_string(),file_format:DataFileFormat::Parquet,partition:Struct::empty(),record_count:1,file_size_in_bytes:5442,column_sizes:HashMap::from([(0,73),(6,34),(2,73),(7,61),(3,61),(5,62),(9,79),(10,73),(1,61),(4,73),(8,73)]),value_counts:HashMap::from([(4,1),(5,1),(2,1),(0,1),(3,1),(6,1),(8,1),(1,1),(10,1),(7,1),(9,1)]),null_value_counts:HashMap::from([(1,0),(6,0),(2,0),(8,0),(0,0),(3,0),(5,0),(9,0),(7,0),(4,0),(10,0)]),nan_value_counts:HashMap::new(),lower_bounds:HashMap::new(),upper_bounds:HashMap::new(),key_metadata:None,split_offsets:Some(vec![4]),equality_ids:Some(Vec::new()),sort_order_id:None, partition_spec_id: 0,first_row_id: None,referenced_data_file: None,content_offset: None,content_size_in_bytes: None }
262 }
263 ];
264
265 let tmp_dir = TempDir::new().unwrap();
267 let path = tmp_dir.path().join("test_manifest.avro");
268 let io = FileIO::new_with_fs();
269 let output_file = io.new_output(path.to_str().unwrap()).unwrap();
270 let mut writer = ManifestWriterBuilder::new(
271 output_file,
272 Some(1),
273 metadata.schema.clone(),
274 metadata.partition_spec.clone(),
275 )
276 .build_v2_data();
277 for entry in &entries {
278 writer.add_entry(entry.clone()).unwrap();
279 }
280 writer.write_manifest_file().await.unwrap();
281
282 let actual_manifest =
284 Manifest::parse_avro(fs::read(path).expect("read_file must succeed").as_slice())
285 .unwrap();
286 entries[0].snapshot_id = Some(1);
288 assert_eq!(actual_manifest, Manifest::new(metadata, entries));
289 }
290
291 #[test]
292 fn test_parse_snappy_manifest_v2() {
293 let schema = Arc::new(
294 Schema::builder()
295 .with_fields(vec![Arc::new(NestedField::optional(
296 1,
297 "id",
298 Type::Primitive(PrimitiveType::Long),
299 ))])
300 .build()
301 .unwrap(),
302 );
303 let partition_spec = PartitionSpec::builder(schema.clone())
304 .with_spec_id(0)
305 .build()
306 .unwrap();
307
308 for (manifest_content, file_content, file_path) in [
309 (
310 ManifestContentType::Data,
311 DataContentType::Data,
312 "s3://bucket/table/data/data.parquet",
313 ),
314 (
315 ManifestContentType::Deletes,
316 DataContentType::PositionDeletes,
317 "s3://bucket/table/data/delete.parquet",
318 ),
319 ] {
320 let metadata = ManifestMetadata {
321 schema_id: 0,
322 schema: schema.clone(),
323 partition_spec: partition_spec.clone(),
324 content: manifest_content,
325 format_version: FormatVersion::V2,
326 };
327 let entry = ManifestEntry {
328 status: ManifestStatus::Added,
329 snapshot_id: Some(1),
330 sequence_number: None,
331 file_sequence_number: None,
332 data_file: DataFile {
333 content: file_content,
334 file_path: file_path.to_string(),
335 file_format: DataFileFormat::Parquet,
336 partition: Struct::empty(),
337 record_count: 1,
338 file_size_in_bytes: 1024,
339 column_sizes: HashMap::new(),
340 value_counts: HashMap::new(),
341 null_value_counts: HashMap::new(),
342 nan_value_counts: HashMap::new(),
343 lower_bounds: HashMap::new(),
344 upper_bounds: HashMap::new(),
345 key_metadata: None,
346 split_offsets: None,
347 equality_ids: None,
348 sort_order_id: None,
349 partition_spec_id: 0,
350 first_row_id: None,
351 referenced_data_file: None,
352 content_offset: None,
353 content_size_in_bytes: None,
354 },
355 };
356
357 let partition_type = metadata
358 .partition_spec
359 .partition_type(&metadata.schema)
360 .unwrap();
361 let avro_schema = manifest_schema_v2(&partition_type).unwrap();
362 let mut writer = Writer::with_codec(&avro_schema, Vec::new(), Codec::Snappy);
363 writer
364 .add_user_metadata("schema".to_string(), to_vec(&metadata.schema).unwrap())
365 .unwrap();
366 writer
367 .add_user_metadata(
368 "schema-id".to_string(),
369 metadata.schema.schema_id().to_string(),
370 )
371 .unwrap();
372 writer
373 .add_user_metadata(
374 "partition-spec".to_string(),
375 to_vec(&metadata.partition_spec.fields()).unwrap(),
376 )
377 .unwrap();
378 writer
379 .add_user_metadata(
380 "partition-spec-id".to_string(),
381 metadata.partition_spec.spec_id().to_string(),
382 )
383 .unwrap();
384 writer
385 .add_user_metadata(
386 "format-version".to_string(),
387 (metadata.format_version as u8).to_string(),
388 )
389 .unwrap();
390 writer
391 .add_user_metadata("content".to_string(), metadata.content.to_string())
392 .unwrap();
393 let value = to_value(
394 _serde::ManifestEntryV2::try_from(entry.clone(), &partition_type).unwrap(),
395 )
396 .unwrap()
397 .resolve(&avro_schema)
398 .unwrap();
399 writer.append(value).unwrap();
400 let bs = writer.into_inner().unwrap();
401
402 let parsed_manifest = Manifest::parse_avro(&bs).unwrap();
403
404 assert_eq!(parsed_manifest, Manifest::new(metadata, vec![entry]));
405 }
406 }
407
408 #[tokio::test]
409 async fn test_parse_manifest_v2_partition() {
410 let schema = Arc::new(
411 Schema::builder()
412 .with_fields(vec![
413 Arc::new(NestedField::optional(
414 1,
415 "id",
416 Type::Primitive(PrimitiveType::Long),
417 )),
418 Arc::new(NestedField::optional(
419 2,
420 "v_int",
421 Type::Primitive(PrimitiveType::Int),
422 )),
423 Arc::new(NestedField::optional(
424 3,
425 "v_long",
426 Type::Primitive(PrimitiveType::Long),
427 )),
428 Arc::new(NestedField::optional(
429 4,
430 "v_float",
431 Type::Primitive(PrimitiveType::Float),
432 )),
433 Arc::new(NestedField::optional(
434 5,
435 "v_double",
436 Type::Primitive(PrimitiveType::Double),
437 )),
438 Arc::new(NestedField::optional(
439 6,
440 "v_varchar",
441 Type::Primitive(PrimitiveType::String),
442 )),
443 Arc::new(NestedField::optional(
444 7,
445 "v_bool",
446 Type::Primitive(PrimitiveType::Boolean),
447 )),
448 Arc::new(NestedField::optional(
449 8,
450 "v_date",
451 Type::Primitive(PrimitiveType::Date),
452 )),
453 Arc::new(NestedField::optional(
454 9,
455 "v_timestamp",
456 Type::Primitive(PrimitiveType::Timestamptz),
457 )),
458 Arc::new(NestedField::optional(
459 10,
460 "v_decimal",
461 Type::Primitive(PrimitiveType::Decimal {
462 precision: 36,
463 scale: 10,
464 }),
465 )),
466 Arc::new(NestedField::optional(
467 11,
468 "v_ts_ntz",
469 Type::Primitive(PrimitiveType::Timestamp),
470 )),
471 Arc::new(NestedField::optional(
472 12,
473 "v_ts_ns_ntz",
474 Type::Primitive(PrimitiveType::TimestampNs),
475 )),
476 ])
477 .build()
478 .unwrap(),
479 );
480 let metadata = ManifestMetadata {
481 schema_id: 0,
482 schema: schema.clone(),
483 partition_spec: PartitionSpec::builder(schema)
484 .with_spec_id(0)
485 .add_partition_field("v_int", "v_int", Transform::Identity)
486 .unwrap()
487 .add_partition_field("v_long", "v_long", Transform::Identity)
488 .unwrap()
489 .build()
490 .unwrap(),
491 content: ManifestContentType::Data,
492 format_version: FormatVersion::V2,
493 };
494 let mut entries = vec![ManifestEntry {
495 status: ManifestStatus::Added,
496 snapshot_id: None,
497 sequence_number: None,
498 file_sequence_number: None,
499 data_file: DataFile {
500 content: DataContentType::Data,
501 file_format: DataFileFormat::Parquet,
502 file_path: "s3a://icebergdata/demo/s1/t1/data/00000-0-378b56f5-5c52-4102-a2c2-f05f8a7cbe4a-00000.parquet".to_string(),
503 partition: Struct::from_iter(
504 vec![
505 Some(Literal::int(1)),
506 Some(Literal::long(1000)),
507 ]
508 .into_iter()
509 ),
510 record_count: 1,
511 file_size_in_bytes: 5442,
512 column_sizes: HashMap::from([
513 (0, 73),
514 (6, 34),
515 (2, 73),
516 (7, 61),
517 (3, 61),
518 (5, 62),
519 (9, 79),
520 (10, 73),
521 (1, 61),
522 (4, 73),
523 (8, 73)
524 ]),
525 value_counts: HashMap::from([
526 (4, 1),
527 (5, 1),
528 (2, 1),
529 (0, 1),
530 (3, 1),
531 (6, 1),
532 (8, 1),
533 (1, 1),
534 (10, 1),
535 (7, 1),
536 (9, 1)
537 ]),
538 null_value_counts: HashMap::from([
539 (1, 0),
540 (6, 0),
541 (2, 0),
542 (8, 0),
543 (0, 0),
544 (3, 0),
545 (5, 0),
546 (9, 0),
547 (7, 0),
548 (4, 0),
549 (10, 0)
550 ]),
551 nan_value_counts: HashMap::new(),
552 lower_bounds: HashMap::new(),
553 upper_bounds: HashMap::new(),
554 key_metadata: None,
555 split_offsets: Some(vec![4]),
556 equality_ids: Some(Vec::new()),
557 sort_order_id: None,
558 partition_spec_id: 0,
559 first_row_id: None,
560 referenced_data_file: None,
561 content_offset: None,
562 content_size_in_bytes: None,
563 },
564 }];
565
566 let tmp_dir = TempDir::new().unwrap();
568 let path = tmp_dir.path().join("test_manifest.avro");
569 let io = FileIO::new_with_fs();
570 let output_file = io.new_output(path.to_str().unwrap()).unwrap();
571 let mut writer = ManifestWriterBuilder::new(
572 output_file,
573 Some(2),
574 metadata.schema.clone(),
575 metadata.partition_spec.clone(),
576 )
577 .build_v2_data();
578 for entry in &entries {
579 writer.add_entry(entry.clone()).unwrap();
580 }
581 let manifest_file = writer.write_manifest_file().await.unwrap();
582 assert_eq!(manifest_file.sequence_number, UNASSIGNED_SEQUENCE_NUMBER);
583 assert_eq!(
584 manifest_file.min_sequence_number,
585 UNASSIGNED_SEQUENCE_NUMBER
586 );
587
588 let actual_manifest =
590 Manifest::parse_avro(fs::read(path).expect("read_file must succeed").as_slice())
591 .unwrap();
592 entries[0].snapshot_id = Some(2);
594 assert_eq!(actual_manifest, Manifest::new(metadata, entries));
595 }
596
597 #[tokio::test]
598 async fn test_parse_manifest_v1_unpartition() {
599 let schema = Arc::new(
600 Schema::builder()
601 .with_schema_id(1)
602 .with_fields(vec![
603 Arc::new(NestedField::optional(
604 1,
605 "id",
606 Type::Primitive(PrimitiveType::Int),
607 )),
608 Arc::new(NestedField::optional(
609 2,
610 "data",
611 Type::Primitive(PrimitiveType::String),
612 )),
613 Arc::new(NestedField::optional(
614 3,
615 "comment",
616 Type::Primitive(PrimitiveType::String),
617 )),
618 ])
619 .build()
620 .unwrap(),
621 );
622 let metadata = ManifestMetadata {
623 schema_id: 1,
624 schema: schema.clone(),
625 partition_spec: PartitionSpec::builder(schema)
626 .with_spec_id(0)
627 .build()
628 .unwrap(),
629 content: ManifestContentType::Data,
630 format_version: FormatVersion::V1,
631 };
632 let mut entries = vec![ManifestEntry {
633 status: ManifestStatus::Added,
634 snapshot_id: Some(0),
635 sequence_number: Some(0),
636 file_sequence_number: Some(0),
637 data_file: DataFile {
638 content: DataContentType::Data,
639 file_path: "s3://testbucket/iceberg_data/iceberg_ctl/iceberg_db/iceberg_tbl/data/00000-7-45268d71-54eb-476c-b42c-942d880c04a1-00001.parquet".to_string(),
640 file_format: DataFileFormat::Parquet,
641 partition: Struct::empty(),
642 record_count: 1,
643 file_size_in_bytes: 875,
644 column_sizes: HashMap::from([(1,47),(2,48),(3,52)]),
645 value_counts: HashMap::from([(1,1),(2,1),(3,1)]),
646 null_value_counts: HashMap::from([(1,0),(2,0),(3,0)]),
647 nan_value_counts: HashMap::new(),
648 lower_bounds: HashMap::from([(1,Datum::int(1)),(2,Datum::string("a")),(3,Datum::string("AC/DC"))]),
649 upper_bounds: HashMap::from([(1,Datum::int(1)),(2,Datum::string("a")),(3,Datum::string("AC/DC"))]),
650 key_metadata: None,
651 split_offsets: Some(vec![4]),
652 equality_ids: None,
653 sort_order_id: Some(0),
654 partition_spec_id: 0,
655 first_row_id: None,
656 referenced_data_file: None,
657 content_offset: None,
658 content_size_in_bytes: None,
659 }
660 }];
661
662 let tmp_dir = TempDir::new().unwrap();
664 let path = tmp_dir.path().join("test_manifest.avro");
665 let io = FileIO::new_with_fs();
666 let output_file = io.new_output(path.to_str().unwrap()).unwrap();
667 let mut writer = ManifestWriterBuilder::new(
668 output_file,
669 Some(3),
670 metadata.schema.clone(),
671 metadata.partition_spec.clone(),
672 )
673 .build_v1();
674 for entry in &entries {
675 writer.add_entry(entry.clone()).unwrap();
676 }
677 writer.write_manifest_file().await.unwrap();
678
679 let actual_manifest =
681 Manifest::parse_avro(fs::read(path).expect("read_file must succeed").as_slice())
682 .unwrap();
683 entries[0].snapshot_id = Some(3);
685 assert_eq!(actual_manifest, Manifest::new(metadata, entries));
686 }
687
688 #[tokio::test]
689 async fn test_parse_manifest_v1_partition() {
690 let schema = Arc::new(
691 Schema::builder()
692 .with_fields(vec![
693 Arc::new(NestedField::optional(
694 1,
695 "id",
696 Type::Primitive(PrimitiveType::Long),
697 )),
698 Arc::new(NestedField::optional(
699 2,
700 "data",
701 Type::Primitive(PrimitiveType::String),
702 )),
703 Arc::new(NestedField::optional(
704 3,
705 "category",
706 Type::Primitive(PrimitiveType::String),
707 )),
708 ])
709 .build()
710 .unwrap(),
711 );
712 let metadata = ManifestMetadata {
713 schema_id: 0,
714 schema: schema.clone(),
715 partition_spec: PartitionSpec::builder(schema)
716 .add_partition_field("category", "category", Transform::Identity)
717 .unwrap()
718 .build()
719 .unwrap(),
720 content: ManifestContentType::Data,
721 format_version: FormatVersion::V1,
722 };
723 let mut entries = vec![
724 ManifestEntry {
725 status: ManifestStatus::Added,
726 snapshot_id: Some(0),
727 sequence_number: Some(0),
728 file_sequence_number: Some(0),
729 data_file: DataFile {
730 content: DataContentType::Data,
731 file_path: "s3://testbucket/prod/db/sample/data/category=x/00010-1-d5c93668-1e52-41ac-92a6-bba590cbf249-00001.parquet".to_string(),
732 file_format: DataFileFormat::Parquet,
733 partition: Struct::from_iter(
734 vec![
735 Some(
736 Literal::string("x"),
737 ),
738 ]
739 .into_iter()
740 ),
741 record_count: 1,
742 file_size_in_bytes: 874,
743 column_sizes: HashMap::from([(1, 46), (2, 48), (3, 48)]),
744 value_counts: HashMap::from([(1, 1), (2, 1), (3, 1)]),
745 null_value_counts: HashMap::from([(1, 0), (2, 0), (3, 0)]),
746 nan_value_counts: HashMap::new(),
747 lower_bounds: HashMap::from([
748 (1, Datum::long(1)),
749 (2, Datum::string("a")),
750 (3, Datum::string("x"))
751 ]),
752 upper_bounds: HashMap::from([
753 (1, Datum::long(1)),
754 (2, Datum::string("a")),
755 (3, Datum::string("x"))
756 ]),
757 key_metadata: None,
758 split_offsets: Some(vec![4]),
759 equality_ids: None,
760 sort_order_id: Some(0),
761 partition_spec_id: 0,
762 first_row_id: None,
763 referenced_data_file: None,
764 content_offset: None,
765 content_size_in_bytes: None,
766 },
767 }
768 ];
769
770 let tmp_dir = TempDir::new().unwrap();
772 let path = tmp_dir.path().join("test_manifest.avro");
773 let io = FileIO::new_with_fs();
774 let output_file = io.new_output(path.to_str().unwrap()).unwrap();
775 let mut writer = ManifestWriterBuilder::new(
776 output_file,
777 Some(2),
778 metadata.schema.clone(),
779 metadata.partition_spec.clone(),
780 )
781 .build_v1();
782 for entry in &entries {
783 writer.add_entry(entry.clone()).unwrap();
784 }
785 let manifest_file = writer.write_manifest_file().await.unwrap();
786 let partitions = manifest_file.partitions.unwrap();
787 assert_eq!(partitions.len(), 1);
788 assert_eq!(
789 partitions[0].clone().lower_bound.unwrap(),
790 Datum::string("x").to_bytes().unwrap()
791 );
792 assert_eq!(
793 partitions[0].clone().upper_bound.unwrap(),
794 Datum::string("x").to_bytes().unwrap()
795 );
796
797 let actual_manifest =
799 Manifest::parse_avro(fs::read(path).expect("read_file must succeed").as_slice())
800 .unwrap();
801 entries[0].snapshot_id = Some(2);
803 assert_eq!(actual_manifest, Manifest::new(metadata, entries));
804 }
805
806 #[tokio::test]
807 async fn test_parse_manifest_with_schema_evolution() {
808 let schema = Arc::new(
809 Schema::builder()
810 .with_fields(vec![
811 Arc::new(NestedField::optional(
812 1,
813 "id",
814 Type::Primitive(PrimitiveType::Long),
815 )),
816 Arc::new(NestedField::optional(
817 2,
818 "v_int",
819 Type::Primitive(PrimitiveType::Int),
820 )),
821 ])
822 .build()
823 .unwrap(),
824 );
825 let metadata = ManifestMetadata {
826 schema_id: 0,
827 schema: schema.clone(),
828 partition_spec: PartitionSpec::builder(schema)
829 .with_spec_id(0)
830 .build()
831 .unwrap(),
832 content: ManifestContentType::Data,
833 format_version: FormatVersion::V2,
834 };
835 let entries = vec![ManifestEntry {
836 status: ManifestStatus::Added,
837 snapshot_id: None,
838 sequence_number: None,
839 file_sequence_number: None,
840 data_file: DataFile {
841 content: DataContentType::Data,
842 file_format: DataFileFormat::Parquet,
843 file_path: "s3a://icebergdata/demo/s1/t1/data/00000-0-378b56f5-5c52-4102-a2c2-f05f8a7cbe4a-00000.parquet".to_string(),
844 partition: Struct::empty(),
845 record_count: 1,
846 file_size_in_bytes: 5442,
847 column_sizes: HashMap::from([
848 (1, 61),
849 (2, 73),
850 (3, 61),
851 ]),
852 value_counts: HashMap::default(),
853 null_value_counts: HashMap::default(),
854 nan_value_counts: HashMap::new(),
855 lower_bounds: HashMap::from([
856 (1, Datum::long(1)),
857 (2, Datum::int(2)),
858 (3, Datum::string("x"))
859 ]),
860 upper_bounds: HashMap::from([
861 (1, Datum::long(1)),
862 (2, Datum::int(2)),
863 (3, Datum::string("x"))
864 ]),
865 key_metadata: None,
866 split_offsets: Some(vec![4]),
867 equality_ids: None,
868 sort_order_id: None,
869 partition_spec_id: 0,
870 first_row_id: None,
871 referenced_data_file: None,
872 content_offset: None,
873 content_size_in_bytes: None,
874 },
875 }];
876
877 let tmp_dir = TempDir::new().unwrap();
879 let path = tmp_dir.path().join("test_manifest.avro");
880 let io = FileIO::new_with_fs();
881 let output_file = io.new_output(path.to_str().unwrap()).unwrap();
882 let mut writer = ManifestWriterBuilder::new(
883 output_file,
884 Some(2),
885 metadata.schema.clone(),
886 metadata.partition_spec.clone(),
887 )
888 .build_v2_data();
889 for entry in &entries {
890 writer.add_entry(entry.clone()).unwrap();
891 }
892 writer.write_manifest_file().await.unwrap();
893
894 let actual_manifest =
896 Manifest::parse_avro(fs::read(path).expect("read_file must succeed").as_slice())
897 .unwrap();
898
899 let schema = Arc::new(
903 Schema::builder()
904 .with_fields(vec![
905 Arc::new(NestedField::optional(
906 1,
907 "id",
908 Type::Primitive(PrimitiveType::Long),
909 )),
910 Arc::new(NestedField::optional(
911 2,
912 "v_int",
913 Type::Primitive(PrimitiveType::Int),
914 )),
915 ])
916 .build()
917 .unwrap(),
918 );
919 let expected_manifest = Manifest {
920 metadata: ManifestMetadata {
921 schema_id: 0,
922 schema: schema.clone(),
923 partition_spec: PartitionSpec::builder(schema).with_spec_id(0).build().unwrap(),
924 content: ManifestContentType::Data,
925 format_version: FormatVersion::V2,
926 },
927 entries: vec![Arc::new(ManifestEntry {
928 status: ManifestStatus::Added,
929 snapshot_id: Some(2),
930 sequence_number: None,
931 file_sequence_number: None,
932 data_file: DataFile {
933 content: DataContentType::Data,
934 file_format: DataFileFormat::Parquet,
935 file_path: "s3a://icebergdata/demo/s1/t1/data/00000-0-378b56f5-5c52-4102-a2c2-f05f8a7cbe4a-00000.parquet".to_string(),
936 partition: Struct::empty(),
937 record_count: 1,
938 file_size_in_bytes: 5442,
939 column_sizes: HashMap::from([
940 (1, 61),
941 (2, 73),
942 (3, 61),
943 ]),
944 value_counts: HashMap::default(),
945 null_value_counts: HashMap::default(),
946 nan_value_counts: HashMap::new(),
947 lower_bounds: HashMap::from([
948 (1, Datum::long(1)),
949 (2, Datum::int(2)),
950 ]),
951 upper_bounds: HashMap::from([
952 (1, Datum::long(1)),
953 (2, Datum::int(2)),
954 ]),
955 key_metadata: None,
956 split_offsets: Some(vec![4]),
957 equality_ids: None,
958 sort_order_id: None,
959 partition_spec_id: 0,
960 first_row_id: None,
961 referenced_data_file: None,
962 content_offset: None,
963 content_size_in_bytes: None,
964 },
965 })],
966 };
967
968 assert_eq!(actual_manifest, expected_manifest);
969 }
970
971 #[tokio::test]
972 async fn test_manifest_summary() {
973 let schema = Arc::new(
974 Schema::builder()
975 .with_fields(vec![
976 Arc::new(NestedField::optional(
977 1,
978 "time",
979 Type::Primitive(PrimitiveType::Date),
980 )),
981 Arc::new(NestedField::optional(
982 2,
983 "v_float",
984 Type::Primitive(PrimitiveType::Float),
985 )),
986 Arc::new(NestedField::optional(
987 3,
988 "v_double",
989 Type::Primitive(PrimitiveType::Double),
990 )),
991 ])
992 .build()
993 .unwrap(),
994 );
995 let partition_spec = PartitionSpec::builder(schema.clone())
996 .with_spec_id(0)
997 .add_partition_field("time", "year_of_time", Transform::Year)
998 .unwrap()
999 .add_partition_field("v_float", "f", Transform::Identity)
1000 .unwrap()
1001 .add_partition_field("v_double", "d", Transform::Identity)
1002 .unwrap()
1003 .build()
1004 .unwrap();
1005 let metadata = ManifestMetadata {
1006 schema_id: 0,
1007 schema,
1008 partition_spec,
1009 content: ManifestContentType::Data,
1010 format_version: FormatVersion::V2,
1011 };
1012 let entries = vec![
1013 ManifestEntry {
1014 status: ManifestStatus::Added,
1015 snapshot_id: None,
1016 sequence_number: None,
1017 file_sequence_number: None,
1018 data_file: DataFile {
1019 content: DataContentType::Data,
1020 file_path: "s3a://icebergdata/demo/s1/t1/data/00000-0-ba56fbfa-f2ff-40c9-bb27-565ad6dc2be8-00000.parquet".to_string(),
1021 file_format: DataFileFormat::Parquet,
1022 partition: Struct::from_iter(
1023 vec![
1024 Some(Literal::int(2021)),
1025 Some(Literal::float(1.0)),
1026 Some(Literal::double(2.0)),
1027 ]
1028 ),
1029 record_count: 1,
1030 file_size_in_bytes: 5442,
1031 column_sizes: HashMap::from([(0,73),(6,34),(2,73),(7,61),(3,61),(5,62),(9,79),(10,73),(1,61),(4,73),(8,73)]),
1032 value_counts: HashMap::from([(4,1),(5,1),(2,1),(0,1),(3,1),(6,1),(8,1),(1,1),(10,1),(7,1),(9,1)]),
1033 null_value_counts: HashMap::from([(1,0),(6,0),(2,0),(8,0),(0,0),(3,0),(5,0),(9,0),(7,0),(4,0),(10,0)]),
1034 nan_value_counts: HashMap::new(),
1035 lower_bounds: HashMap::new(),
1036 upper_bounds: HashMap::new(),
1037 key_metadata: None,
1038 split_offsets: Some(vec![4]),
1039 equality_ids: None,
1040 sort_order_id: None,
1041 partition_spec_id: 0,
1042 first_row_id: None,
1043 referenced_data_file: None,
1044 content_offset: None,
1045 content_size_in_bytes: None,
1046 }
1047 },
1048 ManifestEntry {
1049 status: ManifestStatus::Added,
1050 snapshot_id: None,
1051 sequence_number: None,
1052 file_sequence_number: None,
1053 data_file: DataFile {
1054 content: DataContentType::Data,
1055 file_path: "s3a://icebergdata/demo/s1/t1/data/00000-0-ba56fbfa-f2ff-40c9-bb27-565ad6dc2be8-00000.parquet".to_string(),
1056 file_format: DataFileFormat::Parquet,
1057 partition: Struct::from_iter(
1058 vec![
1059 Some(Literal::int(1111)),
1060 Some(Literal::float(15.5)),
1061 Some(Literal::double(25.5)),
1062 ]
1063 ),
1064 record_count: 1,
1065 file_size_in_bytes: 5442,
1066 column_sizes: HashMap::from([(0,73),(6,34),(2,73),(7,61),(3,61),(5,62),(9,79),(10,73),(1,61),(4,73),(8,73)]),
1067 value_counts: HashMap::from([(4,1),(5,1),(2,1),(0,1),(3,1),(6,1),(8,1),(1,1),(10,1),(7,1),(9,1)]),
1068 null_value_counts: HashMap::from([(1,0),(6,0),(2,0),(8,0),(0,0),(3,0),(5,0),(9,0),(7,0),(4,0),(10,0)]),
1069 nan_value_counts: HashMap::new(),
1070 lower_bounds: HashMap::new(),
1071 upper_bounds: HashMap::new(),
1072 key_metadata: None,
1073 split_offsets: Some(vec![4]),
1074 equality_ids: None,
1075 sort_order_id: None,
1076 partition_spec_id: 0,
1077 first_row_id: None,
1078 referenced_data_file: None,
1079 content_offset: None,
1080 content_size_in_bytes: None,
1081 }
1082 },
1083 ManifestEntry {
1084 status: ManifestStatus::Added,
1085 snapshot_id: None,
1086 sequence_number: None,
1087 file_sequence_number: None,
1088 data_file: DataFile {
1089 content: DataContentType::Data,
1090 file_path: "s3a://icebergdata/demo/s1/t1/data/00000-0-ba56fbfa-f2ff-40c9-bb27-565ad6dc2be8-00000.parquet".to_string(),
1091 file_format: DataFileFormat::Parquet,
1092 partition: Struct::from_iter(
1093 vec![
1094 Some(Literal::int(1211)),
1095 Some(Literal::float(f32::NAN)),
1096 Some(Literal::double(1.0)),
1097 ]
1098 ),
1099 record_count: 1,
1100 file_size_in_bytes: 5442,
1101 column_sizes: HashMap::from([(0,73),(6,34),(2,73),(7,61),(3,61),(5,62),(9,79),(10,73),(1,61),(4,73),(8,73)]),
1102 value_counts: HashMap::from([(4,1),(5,1),(2,1),(0,1),(3,1),(6,1),(8,1),(1,1),(10,1),(7,1),(9,1)]),
1103 null_value_counts: HashMap::from([(1,0),(6,0),(2,0),(8,0),(0,0),(3,0),(5,0),(9,0),(7,0),(4,0),(10,0)]),
1104 nan_value_counts: HashMap::new(),
1105 lower_bounds: HashMap::new(),
1106 upper_bounds: HashMap::new(),
1107 key_metadata: None,
1108 split_offsets: Some(vec![4]),
1109 equality_ids: None,
1110 sort_order_id: None,
1111 partition_spec_id: 0,
1112 first_row_id: None,
1113 referenced_data_file: None,
1114 content_offset: None,
1115 content_size_in_bytes: None,
1116 }
1117 },
1118 ManifestEntry {
1119 status: ManifestStatus::Added,
1120 snapshot_id: None,
1121 sequence_number: None,
1122 file_sequence_number: None,
1123 data_file: DataFile {
1124 content: DataContentType::Data,
1125 file_path: "s3a://icebergdata/demo/s1/t1/data/00000-0-ba56fbfa-f2ff-40c9-bb27-565ad6dc2be8-00000.parquet".to_string(),
1126 file_format: DataFileFormat::Parquet,
1127 partition: Struct::from_iter(
1128 vec![
1129 Some(Literal::int(1111)),
1130 None,
1131 Some(Literal::double(11.0)),
1132 ]
1133 ),
1134 record_count: 1,
1135 file_size_in_bytes: 5442,
1136 column_sizes: HashMap::from([(0,73),(6,34),(2,73),(7,61),(3,61),(5,62),(9,79),(10,73),(1,61),(4,73),(8,73)]),
1137 value_counts: HashMap::from([(4,1),(5,1),(2,1),(0,1),(3,1),(6,1),(8,1),(1,1),(10,1),(7,1),(9,1)]),
1138 null_value_counts: HashMap::from([(1,0),(6,0),(2,0),(8,0),(0,0),(3,0),(5,0),(9,0),(7,0),(4,0),(10,0)]),
1139 nan_value_counts: HashMap::new(),
1140 lower_bounds: HashMap::new(),
1141 upper_bounds: HashMap::new(),
1142 key_metadata: None,
1143 split_offsets: Some(vec![4]),
1144 equality_ids: None,
1145 sort_order_id: None,
1146 partition_spec_id: 0,
1147 first_row_id: None,
1148 referenced_data_file: None,
1149 content_offset: None,
1150 content_size_in_bytes: None,
1151 }
1152 },
1153 ];
1154
1155 let tmp_dir = TempDir::new().unwrap();
1157 let path = tmp_dir.path().join("test_manifest.avro");
1158 let io = FileIO::new_with_fs();
1159 let output_file = io.new_output(path.to_str().unwrap()).unwrap();
1160 let mut writer = ManifestWriterBuilder::new(
1161 output_file,
1162 Some(1),
1163 metadata.schema.clone(),
1164 metadata.partition_spec.clone(),
1165 )
1166 .build_v2_data();
1167 for entry in &entries {
1168 writer.add_entry(entry.clone()).unwrap();
1169 }
1170 let res = writer.write_manifest_file().await.unwrap();
1171
1172 let partitions = res.partitions.unwrap();
1173
1174 assert_eq!(partitions.len(), 3);
1175 assert_eq!(
1176 partitions[0].clone().lower_bound.unwrap(),
1177 Datum::int(1111).to_bytes().unwrap()
1178 );
1179 assert_eq!(
1180 partitions[0].clone().upper_bound.unwrap(),
1181 Datum::int(2021).to_bytes().unwrap()
1182 );
1183 assert!(!partitions[0].clone().contains_null);
1184 assert_eq!(partitions[0].clone().contains_nan, Some(false));
1185
1186 assert_eq!(
1187 partitions[1].clone().lower_bound.unwrap(),
1188 Datum::float(1.0).to_bytes().unwrap()
1189 );
1190 assert_eq!(
1191 partitions[1].clone().upper_bound.unwrap(),
1192 Datum::float(15.5).to_bytes().unwrap()
1193 );
1194 assert!(partitions[1].clone().contains_null);
1195 assert_eq!(partitions[1].clone().contains_nan, Some(true));
1196
1197 assert_eq!(
1198 partitions[2].clone().lower_bound.unwrap(),
1199 Datum::double(1.0).to_bytes().unwrap()
1200 );
1201 assert_eq!(
1202 partitions[2].clone().upper_bound.unwrap(),
1203 Datum::double(25.5).to_bytes().unwrap()
1204 );
1205 assert!(!partitions[2].clone().contains_null);
1206 assert_eq!(partitions[2].clone().contains_nan, Some(false));
1207 }
1208
1209 #[test]
1210 fn test_data_file_serialization() {
1211 let schema = Schema::builder()
1213 .with_schema_id(1)
1214 .with_identifier_field_ids(vec![1])
1215 .with_fields(vec![
1216 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Long)).into(),
1217 NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
1218 ])
1219 .build()
1220 .unwrap();
1221
1222 let partition_spec = PartitionSpec::builder(schema.clone())
1224 .with_spec_id(1)
1225 .add_partition_field("id", "id_partition", Transform::Identity)
1226 .unwrap()
1227 .build()
1228 .unwrap();
1229
1230 let partition_type = partition_spec.partition_type(&schema).unwrap();
1232
1233 let data_files = vec![
1235 DataFileBuilder::default()
1236 .content(DataContentType::Data)
1237 .file_format(DataFileFormat::Parquet)
1238 .file_path("path/to/file1.parquet".to_string())
1239 .file_size_in_bytes(1024)
1240 .record_count(100)
1241 .partition_spec_id(1)
1242 .partition(Struct::empty())
1243 .column_sizes(HashMap::from([(1, 512), (2, 1024)]))
1244 .value_counts(HashMap::from([(1, 100), (2, 500)]))
1245 .null_value_counts(HashMap::from([(1, 0), (2, 1)]))
1246 .build()
1247 .unwrap(),
1248 DataFileBuilder::default()
1249 .content(DataContentType::Data)
1250 .file_format(DataFileFormat::Parquet)
1251 .file_path("path/to/file2.parquet".to_string())
1252 .file_size_in_bytes(2048)
1253 .record_count(200)
1254 .partition_spec_id(1)
1255 .partition(Struct::empty())
1256 .column_sizes(HashMap::from([(1, 1024), (2, 2048)]))
1257 .value_counts(HashMap::from([(1, 200), (2, 600)]))
1258 .null_value_counts(HashMap::from([(1, 10), (2, 999)]))
1259 .build()
1260 .unwrap(),
1261 ];
1262
1263 let serialized_files = data_files
1265 .clone()
1266 .into_iter()
1267 .map(|f| serialize_data_file_to_json(f, &partition_type, FormatVersion::V2).unwrap())
1268 .collect::<Vec<String>>();
1269
1270 assert_eq!(serialized_files.len(), 2);
1272 let pretty_json1: Value = serde_json::from_str(serialized_files.first().unwrap()).unwrap();
1273 let pretty_json2: Value = serde_json::from_str(serialized_files.get(1).unwrap()).unwrap();
1274 let expected_serialized_file1 = serde_json::json!({
1275 "content": 0,
1276 "file_path": "path/to/file1.parquet",
1277 "file_format": "PARQUET",
1278 "partition": {},
1279 "record_count": 100,
1280 "file_size_in_bytes": 1024,
1281 "column_sizes": [
1282 { "key": 1, "value": 512 },
1283 { "key": 2, "value": 1024 }
1284 ],
1285 "value_counts": [
1286 { "key": 1, "value": 100 },
1287 { "key": 2, "value": 500 }
1288 ],
1289 "null_value_counts": [
1290 { "key": 1, "value": 0 },
1291 { "key": 2, "value": 1 }
1292 ],
1293 "nan_value_counts": [],
1294 "lower_bounds": [],
1295 "upper_bounds": [],
1296 "key_metadata": null,
1297 "split_offsets": null,
1298 "equality_ids": null,
1299 "sort_order_id": null,
1300 "first_row_id": null,
1301 "referenced_data_file": null,
1302 "content_offset": null,
1303 "content_size_in_bytes": null
1304 });
1305 let expected_serialized_file2 = serde_json::json!({
1306 "content": 0,
1307 "file_path": "path/to/file2.parquet",
1308 "file_format": "PARQUET",
1309 "partition": {},
1310 "record_count": 200,
1311 "file_size_in_bytes": 2048,
1312 "column_sizes": [
1313 { "key": 1, "value": 1024 },
1314 { "key": 2, "value": 2048 }
1315 ],
1316 "value_counts": [
1317 { "key": 1, "value": 200 },
1318 { "key": 2, "value": 600 }
1319 ],
1320 "null_value_counts": [
1321 { "key": 1, "value": 10 },
1322 { "key": 2, "value": 999 }
1323 ],
1324 "nan_value_counts": [],
1325 "lower_bounds": [],
1326 "upper_bounds": [],
1327 "key_metadata": null,
1328 "split_offsets": null,
1329 "equality_ids": null,
1330 "sort_order_id": null,
1331 "first_row_id": null,
1332 "referenced_data_file": null,
1333 "content_offset": null,
1334 "content_size_in_bytes": null
1335 });
1336 assert_eq!(pretty_json1, expected_serialized_file1);
1337 assert_eq!(pretty_json2, expected_serialized_file2);
1338
1339 let deserialized_files: Vec<DataFile> = serialized_files
1341 .into_iter()
1342 .map(|json| {
1343 deserialize_data_file_from_json(
1344 &json,
1345 partition_spec.spec_id(),
1346 &partition_type,
1347 &schema,
1348 )
1349 .unwrap()
1350 })
1351 .collect();
1352
1353 assert_eq!(deserialized_files.len(), 2);
1355 let deserialized_data_file1 = deserialized_files.first().unwrap();
1356 let deserialized_data_file2 = deserialized_files.get(1).unwrap();
1357 let original_data_file1 = data_files.first().unwrap();
1358 let original_data_file2 = data_files.get(1).unwrap();
1359
1360 assert_eq!(deserialized_data_file1, original_data_file1);
1361 assert_eq!(deserialized_data_file2, original_data_file2);
1362 }
1363}