You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by gi...@apache.org on 2026/09/21 20:26:06 UTC

(iceberg-rust) branch main updated: fix(cache): drop schema-id from the manifest-list cache key (#3235)

This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new 53eccc5f0 fix(cache): drop schema-id from the manifest-list cache key (#3235)
53eccc5f0 is described below

commit 53eccc5f0892eb27b4af940e5e10993dc6ab9e4a
Author: Anoop Johnson <an...@apache.org>
AuthorDate: Mon Sep 21 20:18:28 2026 +0000

    fix(cache): drop schema-id from the manifest-list cache key (#3235)
    
    * fix(cache): drop schema-id from the manifest-list cache key
    
    ObjectCache::get_manifest_list built the cache key from
    snapshot.schema_id().unwrap(), but schema-id is optional on snapshots in
    every table version (v1-v3 per spec, and already read as Option<SchemaId>),
    so a snapshot that omits it panicked on every scan (the object cache is on
    by default).
    
    schema-id does not belong in this key: ManifestListReader::load parses the
    manifest list from its bytes and the table format version only and never
    reads schema-id, and the manifest-list location is already unique per
    snapshot.
    
    * Fix import
    
    * Harden test
---
 crates/iceberg/src/io/object_cache.rs | 54 ++++++++++++++++++++++++++++++++---
 1 file changed, 50 insertions(+), 4 deletions(-)

diff --git a/crates/iceberg/src/io/object_cache.rs b/crates/iceberg/src/io/object_cache.rs
index d780e60c1..01a556b9c 100644
--- a/crates/iceberg/src/io/object_cache.rs
+++ b/crates/iceberg/src/io/object_cache.rs
@@ -22,7 +22,7 @@ use crate::encryption::EncryptionManager;
 use crate::io::FileIO;
 use crate::spec::{
     FormatVersion, Manifest, ManifestFile, ManifestList, ManifestListReader, ManifestReader,
-    SchemaId, SnapshotRef, TableMetadataRef,
+    SnapshotRef, TableMetadataRef,
 };
 use crate::{Error, ErrorKind, Result};
 
@@ -36,7 +36,7 @@ pub(crate) enum CachedItem {
 
 #[derive(Clone, Debug, Hash, Eq, PartialEq)]
 pub(crate) enum CachedObjectKey {
-    ManifestList((String, FormatVersion, SchemaId)),
+    ManifestList((String, FormatVersion)),
     // The manifest-level `first_row_id` is part of the key because the parsed
     // manifest inherits it onto its entries: the same physical manifest can be
     // referenced with different offsets across snapshots and branches, so it
@@ -159,7 +159,6 @@ impl ObjectCache {
         let key = CachedObjectKey::ManifestList((
             snapshot.manifest_list().to_string(),
             table_metadata.format_version,
-            snapshot.schema_id().unwrap(),
         ));
         let cache_entry = self
             .cache
@@ -214,6 +213,7 @@ impl ObjectCache {
 
 #[cfg(test)]
 mod tests {
+    use std::collections::HashMap;
     use std::fs;
 
     use minijinja::value::Value;
@@ -226,7 +226,8 @@ mod tests {
     use crate::io::{FileIO, OutputFile};
     use crate::spec::{
         DataContentType, DataFileBuilder, DataFileFormat, Literal, ManifestEntry,
-        ManifestListWriter, ManifestStatus, ManifestWriterBuilder, Struct, TableMetadata,
+        ManifestListWriter, ManifestStatus, ManifestWriterBuilder, Operation, Snapshot, Struct,
+        Summary, TableMetadata,
     };
     use crate::table::Table;
     use crate::test_utils::test_runtime;
@@ -444,6 +445,51 @@ mod tests {
         );
     }
 
+    #[tokio::test]
+    async fn test_get_manifest_list_with_no_schema_id() {
+        let mut fixture = TableTestFixture::new();
+        fixture.setup_manifest_files().await;
+
+        let current_snapshot = fixture.table.metadata().current_snapshot().unwrap();
+
+        // The spec marks `schema-id` optional in every version (v1-v3), so a
+        // snapshot may omit it; fetching its manifest list must not depend on the
+        // schema-id being present.
+        let snapshot_without_schema_id: SnapshotRef = Snapshot::builder()
+            .with_snapshot_id(current_snapshot.snapshot_id())
+            .with_sequence_number(current_snapshot.sequence_number())
+            .with_timestamp_ms(current_snapshot.timestamp_ms())
+            .with_manifest_list(current_snapshot.manifest_list())
+            .with_summary(Summary {
+                operation: Operation::Append,
+                additional_properties: HashMap::new(),
+            })
+            .build()
+            .into();
+        assert!(snapshot_without_schema_id.schema_id().is_none());
+        assert!(current_snapshot.schema_id().is_some());
+
+        let object_cache = ObjectCache::new(fixture.table.file_io().clone(), None);
+
+        // Cold miss: the schema-id-less snapshot populates the cache.
+        let inserted = object_cache
+            .get_manifest_list(&snapshot_without_schema_id, &fixture.table.metadata_ref())
+            .await
+            .unwrap();
+        assert_eq!(inserted.entries().len(), 1);
+
+        // Warm hit: the original snapshot carries a schema-id but points at the same
+        // manifest-list location, so it returns the same cached entry.
+        let cached = object_cache
+            .get_manifest_list(current_snapshot, &fixture.table.metadata_ref())
+            .await
+            .unwrap();
+        assert!(
+            Arc::ptr_eq(&inserted, &cached),
+            "snapshots with and without schema-id at one location must share a cache entry"
+        );
+    }
+
     #[tokio::test]
     async fn test_get_manifest_keys_on_first_row_id() {
         use crate::spec::{NestedField, PartitionSpec, PrimitiveType, Schema, SchemaRef, Type};