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