iceberg/spec/manifest_list/
manifest_file.rs1use std::str::FromStr;
19
20use serde_derive::{Deserialize, Serialize};
21
22use super::ByteBuf;
23use crate::Error;
24use crate::error::{Result, invalid_data};
25
26#[derive(Debug, PartialEq, Clone, Eq, Hash)]
28pub struct ManifestFile {
29 pub manifest_path: String,
33 pub manifest_length: i64,
37 pub partition_spec_id: i32,
42 pub content: ManifestContentType,
47 pub sequence_number: i64,
52 pub min_sequence_number: i64,
57 pub added_snapshot_id: i64,
61 pub added_files_count: Option<u32>,
66 pub existing_files_count: Option<u32>,
71 pub deleted_files_count: Option<u32>,
76 pub added_rows_count: Option<u64>,
81 pub existing_rows_count: Option<u64>,
86 pub deleted_rows_count: Option<u64>,
91 pub partitions: Option<Vec<FieldSummary>>,
98 pub key_metadata: Option<Vec<u8>>,
102 pub first_row_id: Option<u64>,
106}
107
108impl ManifestFile {
109 pub fn has_added_files(&self) -> bool {
111 self.added_files_count.map(|c| c > 0).unwrap_or(true)
112 }
113
114 pub fn has_deleted_files(&self) -> bool {
116 self.deleted_files_count.map(|c| c > 0).unwrap_or(true)
117 }
118
119 pub fn has_existing_files(&self) -> bool {
121 self.existing_files_count.map(|c| c > 0).unwrap_or(true)
122 }
123}
124
125#[derive(Debug, PartialEq, Clone, Copy, Eq, Hash, Default)]
127pub enum ManifestContentType {
128 #[default]
130 Data = 0,
131 Deletes = 1,
133}
134
135impl FromStr for ManifestContentType {
136 type Err = Error;
137
138 fn from_str(s: &str) -> Result<Self> {
139 match s {
140 "data" => Ok(ManifestContentType::Data),
141 "deletes" => Ok(ManifestContentType::Deletes),
142 _ => Err(invalid_data!("Invalid manifest content type: {s}")),
143 }
144 }
145}
146
147impl std::fmt::Display for ManifestContentType {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 match self {
150 ManifestContentType::Data => write!(f, "data"),
151 ManifestContentType::Deletes => write!(f, "deletes"),
152 }
153 }
154}
155
156impl TryFrom<i32> for ManifestContentType {
157 type Error = Error;
158
159 fn try_from(value: i32) -> std::result::Result<Self, Self::Error> {
160 match value {
161 0 => Ok(ManifestContentType::Data),
162 1 => Ok(ManifestContentType::Deletes),
163 _ => Err(invalid_data!(
164 "Invalid manifest content type. Expected 0 or 1, got {value}"
165 )),
166 }
167 }
168}
169
170#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default, Hash)]
174pub struct FieldSummary {
175 pub contains_null: bool,
180 pub contains_nan: Option<bool>,
184 pub lower_bound: Option<ByteBuf>,
188 pub upper_bound: Option<ByteBuf>,
192}
193
194#[cfg(test)]
195mod test {
196 use std::sync::Arc;
197
198 use super::{ManifestContentType, ManifestFile};
199 use crate::ErrorKind;
200 use crate::encryption::{EncryptedOutputFile, StandardKeyMetadata};
201 use crate::io::FileIO;
202 use crate::spec::{
203 DataContentType, DataFileBuilder, DataFileFormat, ManifestEntry, ManifestReader,
204 ManifestStatus, ManifestWriterBuilder, NestedField, PartitionSpec, PrimitiveType, Schema,
205 SchemaRef, Type,
206 };
207
208 #[test]
209 fn test_manifest_content_type_default() {
210 assert_eq!(ManifestContentType::default(), ManifestContentType::Data);
211 }
212
213 #[test]
214 fn test_manifest_content_type_default_value() {
215 assert_eq!(ManifestContentType::default() as i32, 0);
216 }
217
218 fn test_schema() -> SchemaRef {
220 Arc::new(
221 Schema::builder()
222 .with_fields(vec![Arc::new(NestedField::optional(
223 1,
224 "id",
225 Type::Primitive(PrimitiveType::Long),
226 ))])
227 .build()
228 .unwrap(),
229 )
230 }
231
232 async fn write_manifest(io: &FileIO, path: &str) -> ManifestFile {
235 let schema = test_schema();
236 let partition_spec = PartitionSpec::builder(schema.clone())
237 .with_spec_id(0)
238 .build()
239 .unwrap();
240
241 let output_file = io.new_output(path).unwrap();
242 let mut writer = ManifestWriterBuilder::new(output_file, Some(1), schema, partition_spec)
243 .build_v3_data();
244
245 writer
246 .add_entry(data_entry(ManifestStatus::Added, 100, None))
247 .unwrap();
248
249 writer.write_manifest_file().await.unwrap()
250 }
251
252 async fn write_encrypted_manifest(
255 io: &FileIO,
256 path: &str,
257 key_metadata: StandardKeyMetadata,
258 ) -> ManifestFile {
259 let schema = test_schema();
260 let partition_spec = PartitionSpec::builder(schema.clone())
261 .with_spec_id(0)
262 .build()
263 .unwrap();
264
265 let output_file = io.new_output(path).unwrap();
266 let encrypted_output = EncryptedOutputFile::new(output_file, key_metadata);
267
268 let mut writer = ManifestWriterBuilder::new_from_encrypted(
269 encrypted_output,
270 Some(1),
271 schema,
272 partition_spec,
273 )
274 .expect("Expected a valid writer")
275 .build_v3_data();
276
277 writer
278 .add_entry(data_entry(ManifestStatus::Added, 100, None))
279 .unwrap();
280
281 writer.write_manifest_file().await.unwrap()
282 }
283
284 #[tokio::test]
285 async fn test_load_manifest_decrypts_when_key_metadata_present() {
286 let key_metadata = StandardKeyMetadata::try_new(b"0123456789abcdef")
287 .unwrap()
288 .with_aad_prefix(b"test-aad-prefix!");
289 let io = FileIO::new_with_memory();
290 let path = "memory:///test/encrypted_manifest.avro";
291 let manifest_file = write_encrypted_manifest(&io, path, key_metadata.clone()).await;
292 let size = io.new_input(path).unwrap().metadata().await.unwrap().size;
293 assert_eq!(manifest_file.manifest_length, size as i64);
294 assert_eq!(
295 StandardKeyMetadata::decode(manifest_file.key_metadata.as_ref().unwrap()).unwrap(),
296 key_metadata.with_file_length(size)
297 );
298
299 let manifest = ManifestReader::new(io).read(&manifest_file).await.unwrap();
300 assert_eq!(manifest.entries().len(), 1);
301 assert_eq!(
302 manifest.entries()[0].file_path(),
303 "s3://bucket/table/data/00000.parquet"
304 );
305 assert_eq!(manifest.entries()[0].data_file.record_count, 100);
306 }
307
308 #[tokio::test]
309 async fn test_load_manifest_fails_with_wrong_key() {
310 let key_metadata = StandardKeyMetadata::try_new(b"0123456789abcdef")
311 .unwrap()
312 .with_aad_prefix(b"test-aad-prefix!");
313
314 let io = FileIO::new_with_memory();
315 let path = "memory:///test/wrong_key_manifest.avro";
316 let mut manifest_file = write_encrypted_manifest(&io, path, key_metadata).await;
317
318 let wrong_key_metadata = StandardKeyMetadata::try_new(b"fedcba9876543210")
323 .unwrap()
324 .with_aad_prefix(b"test-aad-prefix!")
325 .with_file_length(manifest_file.manifest_length as u64);
326 manifest_file.key_metadata = Some(wrong_key_metadata.encode().unwrap().to_vec());
327
328 let err = ManifestReader::new(io)
329 .read(&manifest_file)
330 .await
331 .expect_err("read must fail when decrypting with the wrong key");
332 assert_eq!(err.kind(), ErrorKind::Unexpected);
333 }
334
335 #[tokio::test]
336 async fn test_load_manifest_fails_with_wrong_aad() {
337 let key_metadata = StandardKeyMetadata::try_new(b"0123456789abcdef")
338 .unwrap()
339 .with_aad_prefix(b"test-aad-prefix!");
340
341 let io = FileIO::new_with_memory();
342 let path = "memory:///test/wrong_aad_manifest.avro";
343 let mut manifest_file = write_encrypted_manifest(&io, path, key_metadata).await;
344
345 let wrong_aad_metadata = StandardKeyMetadata::try_new(b"0123456789abcdef")
349 .unwrap()
350 .with_aad_prefix(b"wrong-aad-prefix")
351 .with_file_length(manifest_file.manifest_length as u64);
352 manifest_file.key_metadata = Some(wrong_aad_metadata.encode().unwrap().to_vec());
353
354 let err = ManifestReader::new(io)
355 .read(&manifest_file)
356 .await
357 .expect_err("read must fail when decrypting with the wrong AAD prefix");
358 assert_eq!(err.kind(), ErrorKind::Unexpected);
359 }
360
361 fn data_entry(
364 status: ManifestStatus,
365 record_count: u64,
366 first_row_id: Option<i64>,
367 ) -> ManifestEntry {
368 let data_file = DataFileBuilder::default()
369 .content(DataContentType::Data)
370 .file_path("s3://bucket/table/data/00000.parquet".to_string())
371 .file_format(DataFileFormat::Parquet)
372 .file_size_in_bytes(4096)
373 .record_count(record_count)
374 .first_row_id(first_row_id)
375 .build()
376 .unwrap();
377
378 ManifestEntry::builder()
379 .status(status)
380 .data_file(data_file)
381 .build()
382 }
383
384 #[tokio::test]
385 async fn test_load_manifest_reads_written_entries() {
386 let io = FileIO::new_with_memory();
387 let path = "memory:///test/plaintext_manifest.avro";
388 let manifest_file = write_manifest(&io, path).await;
389 assert_eq!(manifest_file.key_metadata, None);
390
391 let manifest = ManifestReader::new(io).read(&manifest_file).await.unwrap();
392 assert_eq!(manifest.entries().len(), 1);
393 assert_eq!(
394 manifest.entries()[0].file_path(),
395 "s3://bucket/table/data/00000.parquet"
396 );
397 assert_eq!(manifest.entries()[0].data_file.record_count, 100);
398 }
399
400 #[tokio::test]
405 async fn test_load_manifest_assigns_first_row_ids() {
406 let io = FileIO::new_with_memory();
407 let path = "memory:///test/first_row_id_manifest.avro";
408 let mut manifest_file = write_manifest(&io, path).await;
409
410 manifest_file.first_row_id = Some(1000);
412
413 let manifest = ManifestReader::new(io).read(&manifest_file).await.unwrap();
414 assert_eq!(manifest.entries().len(), 1);
415 assert_eq!(manifest.entries()[0].data_file().first_row_id(), Some(1000));
416 }
417}