Skip to main content

iceberg/spec/manifest/
reader.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use super::Manifest;
19use crate::encryption::{EncryptedInputFile, StandardKeyMetadata};
20use crate::error::{Result, invalid_data};
21use crate::io::FileIO;
22use crate::spec::{ManifestContentType, ManifestEntry, ManifestFile};
23
24/// Reads a manifest file referenced by a manifest list entry, transparently
25/// decrypting it when the entry records key metadata.
26pub struct ManifestReader {
27    file_io: FileIO,
28}
29
30impl ManifestReader {
31    /// Create a manifest reader.
32    pub(crate) fn new(file_io: FileIO) -> Self {
33        Self { file_io }
34    }
35
36    /// Read, decrypt, parse and return the manifest described by
37    /// `manifest_file`.
38    pub async fn read(self, manifest_file: &ManifestFile) -> Result<Manifest> {
39        let input_file = self.file_io.new_input(&manifest_file.manifest_path)?;
40        let key_metadata = manifest_file
41            .key_metadata
42            .as_deref()
43            .map(StandardKeyMetadata::decode)
44            .transpose()?;
45        let bytes = match key_metadata {
46            Some(key_metadata) => {
47                EncryptedInputFile::new(input_file, key_metadata)
48                    .read()
49                    .await?
50            }
51            None => input_file.read().await?,
52        };
53
54        let (metadata, mut entries) =
55            Manifest::try_from_avro_bytes(&bytes, Some(&manifest_file.manifest_path))?;
56
57        for entry in &mut entries {
58            entry.inherit_data(manifest_file);
59        }
60
61        self.assign_first_row_ids(manifest_file, &mut entries)?;
62
63        Ok(Manifest::new(metadata, entries))
64    }
65
66    /// Assigns `first_row_id` to the live data-file entries, following the
67    /// row-lineage inheritance rules in
68    /// <https://github.com/apache/iceberg/blob/main/format/spec.md#first-row-id-inheritance>.
69    ///
70    /// With a manifest-level `first_row_id`, each live entry lacking one is
71    /// assigned the running id, which then advances by that entry's record
72    /// count; entries that already carry a `first_row_id` keep it and do not
73    /// advance the counter. Without a manifest-level `first_row_id`, any
74    /// inherited per-entry value is cleared so callers never observe a stale id.
75    fn assign_first_row_ids(
76        &self,
77        manifest_file: &ManifestFile,
78        entries: &mut [ManifestEntry],
79    ) -> Result<()> {
80        // A `first_row_id` is only valid on data manifests. Delete files always
81        // have a null `first_row_id`, so there is nothing to assign or clear; a
82        // stray value on a delete manifest is a spec violation by the writer,
83        // which we surface without failing the read.
84        if manifest_file.content != ManifestContentType::Data {
85            if let Some(manifest_first_row_id) = manifest_file.first_row_id {
86                tracing::warn!(
87                    "Ignoring first_row_id {manifest_first_row_id} on delete manifest {}",
88                    manifest_file.manifest_path
89                );
90            }
91
92            return Ok(());
93        }
94
95        let Some(manifest_first_row_id) = manifest_file.first_row_id else {
96            // A data manifest with no manifest-level `first_row_id` predates row
97            // lineage; clear any per-entry value inherited from an earlier read.
98            for entry in entries {
99                entry.data_file.first_row_id = None;
100            }
101
102            return Ok(());
103        };
104
105        let mut next_row_id = i64::try_from(manifest_first_row_id).map_err(|_| {
106            invalid_data!("Invalid first_row_id: {manifest_first_row_id} (exceeds i64::MAX)")
107        })?;
108
109        for entry in entries {
110            if !entry.is_alive() {
111                continue;
112            }
113
114            if entry.data_file.first_row_id.is_none() {
115                let file_first_row_id = next_row_id;
116                entry.data_file.first_row_id = Some(file_first_row_id);
117                let record_count = entry.data_file.record_count;
118                next_row_id = file_first_row_id.checked_add_unsigned(record_count).ok_or_else(|| {
119                    invalid_data!("Row ID overflow assigning first_row_id in {}. File first_row_id: {file_first_row_id}, record count: {record_count}",
120                            manifest_file.manifest_path)
121                })?;
122            }
123        }
124
125        Ok(())
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use std::collections::HashMap;
132    use std::sync::Arc;
133
134    use super::*;
135    use crate::ErrorKind;
136    use crate::encryption::{EncryptedOutputFile, StandardKeyMetadata};
137    use crate::io::FileIO;
138    use crate::spec::{
139        DataContentType, DataFile, DataFileBuilder, DataFileFormat, ManifestEntry, ManifestStatus,
140        ManifestWriterBuilder, NestedField, PartitionSpec, PrimitiveType, Schema, Struct, Type,
141    };
142
143    #[tokio::test]
144    async fn test_read_plaintext_manifest_inherits_entries() {
145        let schema = test_schema();
146        let partition_spec = PartitionSpec::builder(schema.clone())
147            .with_spec_id(0)
148            .build()
149            .unwrap();
150
151        let io = FileIO::new_with_memory();
152        let path = "memory:///table/metadata/plain.avro";
153        let mut writer = ManifestWriterBuilder::new(
154            io.new_output(path).unwrap(),
155            Some(1),
156            schema.clone(),
157            partition_spec,
158        )
159        .build_v2_data();
160        writer.add_entry(test_entry()).unwrap();
161        // Writing the manifest yields the manifest list entry describing it.
162        let manifest_file = writer.write_manifest_file().await.unwrap();
163
164        let manifest = ManifestReader::new(io).read(&manifest_file).await.unwrap();
165        assert_eq!(manifest.entries().len(), 1);
166        assert_eq!(
167            manifest.entries()[0].data_file().file_path(),
168            "memory:///table/data/00000.parquet"
169        );
170        // Entries must inherit values from the manifest list entry.
171        assert_eq!(
172            manifest.entries()[0].sequence_number(),
173            Some(manifest_file.sequence_number)
174        );
175        assert_eq!(
176            manifest.entries()[0].snapshot_id(),
177            Some(manifest_file.added_snapshot_id)
178        );
179    }
180
181    /// Per spec, inheritance does not apply to v1: a v1 manifest's entries read
182    /// with sequence number 0 even when the manifest list entry referencing them
183    /// carries a nonzero one.
184    #[tokio::test]
185    async fn test_read_v1_manifest_defaults_sequence_number_to_zero() {
186        let schema = test_schema();
187        let partition_spec = PartitionSpec::builder(schema.clone())
188            .with_spec_id(0)
189            .build()
190            .unwrap();
191
192        let io = FileIO::new_with_memory();
193        let path = "memory:///table/metadata/v1.avro";
194        let mut writer = ManifestWriterBuilder::new(
195            io.new_output(path).unwrap(),
196            Some(1),
197            schema.clone(),
198            partition_spec,
199        )
200        .build_v1();
201        writer.add_entry(test_entry()).unwrap();
202        let mut manifest_file = writer.write_manifest_file().await.unwrap();
203        manifest_file.sequence_number = 36;
204
205        let manifest = ManifestReader::new(io).read(&manifest_file).await.unwrap();
206        assert_eq!(manifest.entries()[0].sequence_number(), Some(0));
207        assert_eq!(manifest.entries()[0].file_sequence_number, Some(0));
208    }
209
210    #[tokio::test]
211    async fn test_read_encrypted_manifest_roundtrip() {
212        let schema = test_schema();
213        let partition_spec = PartitionSpec::builder(schema.clone())
214            .with_spec_id(0)
215            .build()
216            .unwrap();
217
218        let io = FileIO::new_with_memory();
219        let path = "memory:///table/metadata/encrypted.avro";
220        let encrypted_output =
221            EncryptedOutputFile::new(io.new_output(path).unwrap(), key_metadata());
222
223        let mut writer = ManifestWriterBuilder::new_from_encrypted(
224            encrypted_output,
225            Some(1),
226            schema.clone(),
227            partition_spec,
228        )
229        .unwrap()
230        .build_v3_data();
231        writer.add_entry(test_entry()).unwrap();
232        // The returned manifest list entry records the key metadata.
233        let manifest_file = writer.write_manifest_file().await.unwrap();
234        assert!(manifest_file.key_metadata.is_some());
235
236        // Reading with the recorded key metadata must recover the entry.
237        let manifest = ManifestReader::new(io.clone())
238            .read(&manifest_file)
239            .await
240            .unwrap();
241        assert_eq!(manifest.entries().len(), 1);
242        assert_eq!(
243            manifest.entries()[0].data_file().file_path(),
244            "memory:///table/data/00000.parquet"
245        );
246
247        // Without the key metadata the encrypted bytes must not read as plaintext.
248        let mut plaintext_entry = manifest_file.clone();
249        plaintext_entry.key_metadata = None;
250        assert!(
251            ManifestReader::new(io)
252                .read(&plaintext_entry)
253                .await
254                .is_err(),
255            "encrypted manifest must not parse as plaintext"
256        );
257    }
258
259    fn test_schema() -> Arc<Schema> {
260        Arc::new(
261            Schema::builder()
262                .with_fields(vec![Arc::new(NestedField::optional(
263                    1,
264                    "id",
265                    Type::Primitive(PrimitiveType::Long),
266                ))])
267                .build()
268                .unwrap(),
269        )
270    }
271
272    fn test_entry() -> ManifestEntry {
273        ManifestEntry {
274            status: ManifestStatus::Added,
275            snapshot_id: None,
276            sequence_number: None,
277            file_sequence_number: None,
278            data_file: DataFile {
279                content: DataContentType::Data,
280                file_path: "memory:///table/data/00000.parquet".to_string(),
281                file_format: DataFileFormat::Parquet,
282                partition: Struct::empty(),
283                record_count: 1,
284                file_size_in_bytes: 4096,
285                column_sizes: HashMap::new(),
286                value_counts: HashMap::new(),
287                null_value_counts: HashMap::new(),
288                nan_value_counts: HashMap::new(),
289                lower_bounds: HashMap::new(),
290                upper_bounds: HashMap::new(),
291                key_metadata: None,
292                split_offsets: None,
293                equality_ids: None,
294                sort_order_id: None,
295                partition_spec_id: 0,
296                first_row_id: None,
297                referenced_data_file: None,
298                content_offset: None,
299                content_size_in_bytes: None,
300            },
301        }
302    }
303
304    fn key_metadata() -> StandardKeyMetadata {
305        StandardKeyMetadata::try_new(b"0123456789abcdef").unwrap()
306    }
307
308    /// A reader with an unused in-memory `FileIO`, for exercising the pure
309    /// row-id assignment logic without touching storage.
310    fn test_reader() -> ManifestReader {
311        ManifestReader::new(FileIO::new_with_memory())
312    }
313
314    /// Builds a data-file manifest entry with the given status, record count,
315    /// and pre-existing `first_row_id`.
316    fn data_entry(
317        status: ManifestStatus,
318        record_count: u64,
319        first_row_id: Option<i64>,
320    ) -> ManifestEntry {
321        let data_file = DataFileBuilder::default()
322            .content(DataContentType::Data)
323            .file_path("s3://bucket/table/data/00000.parquet".to_string())
324            .file_format(DataFileFormat::Parquet)
325            .file_size_in_bytes(4096)
326            .record_count(record_count)
327            .first_row_id(first_row_id)
328            .build()
329            .unwrap();
330
331        ManifestEntry::builder()
332            .status(status)
333            .data_file(data_file)
334            .build()
335    }
336
337    /// Builds a manifest file with the given content type and manifest-level
338    /// `first_row_id`. Other fields are irrelevant to row-id assignment.
339    fn manifest_file(content: ManifestContentType, first_row_id: Option<u64>) -> ManifestFile {
340        ManifestFile {
341            manifest_path: "memory:///m.avro".to_string(),
342            manifest_length: 0,
343            partition_spec_id: 0,
344            content,
345            sequence_number: 0,
346            min_sequence_number: 0,
347            added_snapshot_id: 0,
348            added_files_count: None,
349            existing_files_count: None,
350            deleted_files_count: None,
351            added_rows_count: None,
352            existing_rows_count: None,
353            deleted_rows_count: None,
354            partitions: None,
355            key_metadata: None,
356            first_row_id,
357        }
358    }
359
360    #[test]
361    fn test_assign_first_row_ids_interleaved() {
362        let manifest = manifest_file(ManifestContentType::Data, Some(10));
363        let mut entries = vec![
364            data_entry(ManifestStatus::Added, 3, None),
365            // A pre-assigned entry between two assigned ones: it keeps its id and
366            // must not advance the running counter.
367            data_entry(ManifestStatus::Added, 5, Some(100)),
368            // A deleted entry with a pre-set id: it is skipped, so the id is
369            // preserved verbatim and does not advance the counter.
370            data_entry(ManifestStatus::Deleted, 7, Some(999)),
371            data_entry(ManifestStatus::Existing, 2, None),
372        ];
373
374        test_reader()
375            .assign_first_row_ids(&manifest, &mut entries)
376            .unwrap();
377
378        assert_eq!(entries[0].data_file.first_row_id, Some(10));
379        assert_eq!(entries[1].data_file.first_row_id, Some(100));
380        assert_eq!(entries[2].data_file.first_row_id, Some(999));
381        // 10 + 3 = 13; the preserved and deleted entries in between do not move it.
382        assert_eq!(entries[3].data_file.first_row_id, Some(13));
383    }
384
385    #[test]
386    fn test_assign_first_row_ids_clears_without_manifest_first_row_id() {
387        // A data manifest with no manifest-level first_row_id predates row
388        // lineage: any per-entry value inherited from an earlier read is cleared
389        // so callers never observe a stale id.
390        let manifest = manifest_file(ManifestContentType::Data, None);
391        let mut entries = vec![
392            data_entry(ManifestStatus::Added, 3, None),
393            data_entry(ManifestStatus::Existing, 5, Some(100)),
394        ];
395
396        test_reader()
397            .assign_first_row_ids(&manifest, &mut entries)
398            .unwrap();
399
400        assert_eq!(entries[0].data_file.first_row_id, None);
401        assert_eq!(entries[1].data_file.first_row_id, None);
402    }
403
404    #[test]
405    fn test_assign_first_row_ids_ignores_delete_manifest() {
406        // A stray first_row_id on a delete manifest is a writer-side spec
407        // violation; the read ignores it rather than failing, and does not
408        // assign ids to the entries.
409        let manifest = manifest_file(ManifestContentType::Deletes, Some(10));
410        let mut entries = vec![data_entry(ManifestStatus::Added, 3, None)];
411
412        test_reader()
413            .assign_first_row_ids(&manifest, &mut entries)
414            .unwrap();
415
416        assert_eq!(entries[0].data_file.first_row_id, None);
417    }
418
419    #[test]
420    fn test_assign_first_row_ids_rejects_oversized_manifest_first_row_id() {
421        // A manifest-level first_row_id above i64::MAX cannot be represented as the
422        // signed running counter and must be rejected.
423        let manifest = manifest_file(ManifestContentType::Data, Some(i64::MAX as u64 + 1));
424        let mut entries = vec![data_entry(ManifestStatus::Added, 3, None)];
425
426        let err = test_reader()
427            .assign_first_row_ids(&manifest, &mut entries)
428            .expect_err("an oversized manifest first_row_id must be rejected");
429        assert_eq!(err.kind(), ErrorKind::DataInvalid);
430        assert!(err.message().contains("Invalid first_row_id"));
431    }
432
433    #[test]
434    fn test_assign_first_row_ids_rejects_counter_overflow() {
435        // Advancing the running counter past i64::MAX must be rejected rather than
436        // wrapping to a negative value that would corrupt subsequent assignments.
437        let manifest = manifest_file(ManifestContentType::Data, Some(i64::MAX as u64));
438        let mut entries = vec![data_entry(ManifestStatus::Added, 1, None)];
439
440        let err = test_reader()
441            .assign_first_row_ids(&manifest, &mut entries)
442            .expect_err("counter overflow past i64::MAX must be rejected");
443        assert_eq!(err.kind(), ErrorKind::DataInvalid);
444        assert!(err.message().contains("Row ID overflow"));
445    }
446}