iceberg/spec/manifest_list/
manifest_file.rs1use std::str::FromStr;
19
20use serde_derive::{Deserialize, Serialize};
21
22use super::ByteBuf;
23use crate::error::Result;
24use crate::{Error, ErrorKind};
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(Error::new(
143 ErrorKind::DataInvalid,
144 format!("Invalid manifest content type: {s}"),
145 )),
146 }
147 }
148}
149
150impl std::fmt::Display for ManifestContentType {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 match self {
153 ManifestContentType::Data => write!(f, "data"),
154 ManifestContentType::Deletes => write!(f, "deletes"),
155 }
156 }
157}
158
159impl TryFrom<i32> for ManifestContentType {
160 type Error = Error;
161
162 fn try_from(value: i32) -> std::result::Result<Self, Self::Error> {
163 match value {
164 0 => Ok(ManifestContentType::Data),
165 1 => Ok(ManifestContentType::Deletes),
166 _ => Err(Error::new(
167 ErrorKind::DataInvalid,
168 format!("Invalid manifest content type. Expected 0 or 1, got {value}"),
169 )),
170 }
171 }
172}
173
174#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default, Hash)]
178pub struct FieldSummary {
179 pub contains_null: bool,
184 pub contains_nan: Option<bool>,
188 pub lower_bound: Option<ByteBuf>,
192 pub upper_bound: Option<ByteBuf>,
196}
197
198#[cfg(test)]
199mod test {
200 use std::sync::Arc;
201
202 use super::{ManifestContentType, ManifestFile};
203 use crate::ErrorKind;
204 use crate::encryption::{EncryptedOutputFile, StandardKeyMetadata};
205 use crate::io::FileIO;
206 use crate::spec::{
207 DataContentType, DataFileBuilder, DataFileFormat, ManifestEntry, ManifestReader,
208 ManifestStatus, ManifestWriterBuilder, NestedField, PartitionSpec, PrimitiveType, Schema,
209 SchemaRef, Type,
210 };
211
212 #[test]
213 fn test_manifest_content_type_default() {
214 assert_eq!(ManifestContentType::default(), ManifestContentType::Data);
215 }
216
217 #[test]
218 fn test_manifest_content_type_default_value() {
219 assert_eq!(ManifestContentType::default() as i32, 0);
220 }
221
222 fn test_schema() -> SchemaRef {
224 Arc::new(
225 Schema::builder()
226 .with_fields(vec![Arc::new(NestedField::optional(
227 1,
228 "id",
229 Type::Primitive(PrimitiveType::Long),
230 ))])
231 .build()
232 .unwrap(),
233 )
234 }
235
236 async fn write_manifest(io: &FileIO, path: &str) -> ManifestFile {
239 let schema = test_schema();
240 let partition_spec = PartitionSpec::builder(schema.clone())
241 .with_spec_id(0)
242 .build()
243 .unwrap();
244
245 let output_file = io.new_output(path).unwrap();
246 let mut writer = ManifestWriterBuilder::new(output_file, Some(1), schema, partition_spec)
247 .build_v3_data();
248
249 writer
250 .add_entry(data_entry(ManifestStatus::Added, 100, None))
251 .unwrap();
252
253 writer.write_manifest_file().await.unwrap()
254 }
255
256 async fn write_encrypted_manifest(
259 io: &FileIO,
260 path: &str,
261 key_metadata: StandardKeyMetadata,
262 ) -> ManifestFile {
263 let schema = test_schema();
264 let partition_spec = PartitionSpec::builder(schema.clone())
265 .with_spec_id(0)
266 .build()
267 .unwrap();
268
269 let output_file = io.new_output(path).unwrap();
270 let encrypted_output = EncryptedOutputFile::new(output_file, key_metadata);
271
272 let mut writer = ManifestWriterBuilder::new_from_encrypted(
273 encrypted_output,
274 Some(1),
275 schema,
276 partition_spec,
277 )
278 .expect("Expected a valid writer")
279 .build_v3_data();
280
281 writer
282 .add_entry(data_entry(ManifestStatus::Added, 100, None))
283 .unwrap();
284
285 writer.write_manifest_file().await.unwrap()
286 }
287
288 #[tokio::test]
289 async fn test_load_manifest_decrypts_when_key_metadata_present() {
290 let key_metadata = StandardKeyMetadata::try_new(b"0123456789abcdef")
291 .unwrap()
292 .with_aad_prefix(b"test-aad-prefix!");
293 let encoded_key_metadata = key_metadata.encode().unwrap().to_vec();
294
295 let io = FileIO::new_with_memory();
296 let path = "memory:///test/encrypted_manifest.avro";
297 let manifest_file = write_encrypted_manifest(&io, path, key_metadata).await;
298 assert_eq!(manifest_file.key_metadata, Some(encoded_key_metadata));
299
300 let manifest = ManifestReader::new(io).read(&manifest_file).await.unwrap();
301 assert_eq!(manifest.entries().len(), 1);
302 assert_eq!(
303 manifest.entries()[0].file_path(),
304 "s3://bucket/table/data/00000.parquet"
305 );
306 assert_eq!(manifest.entries()[0].data_file.record_count, 100);
307 }
308
309 #[tokio::test]
310 async fn test_load_manifest_fails_with_wrong_key() {
311 let key_metadata = StandardKeyMetadata::try_new(b"0123456789abcdef")
312 .unwrap()
313 .with_aad_prefix(b"test-aad-prefix!");
314
315 let io = FileIO::new_with_memory();
316 let path = "memory:///test/wrong_key_manifest.avro";
317 let mut manifest_file = write_encrypted_manifest(&io, path, key_metadata).await;
318
319 let wrong_key_metadata = StandardKeyMetadata::try_new(b"fedcba9876543210")
324 .unwrap()
325 .with_aad_prefix(b"test-aad-prefix!");
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 manifest_file.key_metadata = Some(wrong_aad_metadata.encode().unwrap().to_vec());
352
353 let err = ManifestReader::new(io)
354 .read(&manifest_file)
355 .await
356 .expect_err("read must fail when decrypting with the wrong AAD prefix");
357 assert_eq!(err.kind(), ErrorKind::Unexpected);
358 }
359
360 fn data_entry(
363 status: ManifestStatus,
364 record_count: u64,
365 first_row_id: Option<i64>,
366 ) -> ManifestEntry {
367 let data_file = DataFileBuilder::default()
368 .content(DataContentType::Data)
369 .file_path("s3://bucket/table/data/00000.parquet".to_string())
370 .file_format(DataFileFormat::Parquet)
371 .file_size_in_bytes(4096)
372 .record_count(record_count)
373 .first_row_id(first_row_id)
374 .build()
375 .unwrap();
376
377 ManifestEntry::builder()
378 .status(status)
379 .data_file(data_file)
380 .build()
381 }
382
383 #[tokio::test]
384 async fn test_load_manifest_reads_written_entries() {
385 let io = FileIO::new_with_memory();
386 let path = "memory:///test/plaintext_manifest.avro";
387 let manifest_file = write_manifest(&io, path).await;
388 assert_eq!(manifest_file.key_metadata, None);
389
390 let manifest = ManifestReader::new(io).read(&manifest_file).await.unwrap();
391 assert_eq!(manifest.entries().len(), 1);
392 assert_eq!(
393 manifest.entries()[0].file_path(),
394 "s3://bucket/table/data/00000.parquet"
395 );
396 assert_eq!(manifest.entries()[0].data_file.record_count, 100);
397 }
398
399 #[tokio::test]
404 async fn test_load_manifest_assigns_first_row_ids() {
405 let io = FileIO::new_with_memory();
406 let path = "memory:///test/first_row_id_manifest.avro";
407 let mut manifest_file = write_manifest(&io, path).await;
408
409 manifest_file.first_row_id = Some(1000);
411
412 let manifest = ManifestReader::new(io).read(&manifest_file).await.unwrap();
413 assert_eq!(manifest.entries().len(), 1);
414 assert_eq!(manifest.entries()[0].data_file().first_row_id(), Some(1000));
415 }
416}