Conversation
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.
| #[derive(Clone, Debug, Hash, Eq, PartialEq)] | ||
| pub(crate) enum CachedObjectKey { | ||
| ManifestList((String, FormatVersion, SchemaId)), | ||
| ManifestList((String, FormatVersion)), |
There was a problem hiding this comment.
Another alternative is to continue to use Option<SchemaId> - but it doesn't seem necessary
xanderbailey
left a comment
There was a problem hiding this comment.
Nice find! I think this change makes sense to me. I think Java just caches on location from what I can see https://github.com/apache/iceberg/blob/main/core/src/main/java/org/apache/iceberg/io/ContentCache.java#L233. Their caching is setup a little differently to ours but the idea holds.
| use std::collections::HashMap; | ||
|
|
||
| use crate::spec::{Operation, Snapshot, Summary}; |
There was a problem hiding this comment.
micro nit: I think typically we do test imports at the top
laskoviymishka
left a comment
There was a problem hiding this comment.
Nice find — the schema_id().unwrap() was always going to panic on a snapshot that omits schema-id, which the spec allows in every version, and dropping it from the cache key is the correct fix.
One thing I'd add before merge: the new test only calls get_manifest_list once, so it proves the panic is gone but never exercises the cached return. A second call asserting the same result would cover the warm-cache path — and if that call reuses the original current_snapshot, it also demonstrates the nice side effect here, that the schema-id Some and None variants at the same location now share a cache entry.
I left a smaller note inline about whether format_version still needs to be in the key at all, but that's out of scope for this PR.
I have no objection to merge as is, but would keep this soak for some time to other to take a look
| .await | ||
| .unwrap(); | ||
|
|
||
| assert_eq!(result_manifest_list.entries().len(), 1); |
There was a problem hiding this comment.
this proves the panic is gone, but it only hits the cold path — a single get_manifest_list call, so the cached return is never exercised. If a refactor reintroduced schema_id into the key on the insert path only, this test would still pass while the cache quietly degraded to miss-every-time.
I'd add a second call and assert it matches:
// second call must hit the cache without rebuilding
let cached = object_cache
.get_manifest_list(&snapshot, &fixture.table.metadata_ref())
.await
.unwrap();
assert_eq!(cached.entries().len(), 1);And if that second call uses the original current_snapshot (schema-id Some(1)) instead, it also covers the merge this fix enables — the Some and None variants at the same location now sharing one entry.
There was a problem hiding this comment.
That is a good idea. I added a second get_manifest_list call to excercise this.
| #[derive(Clone, Debug, Hash, Eq, PartialEq)] | ||
| pub(crate) enum CachedObjectKey { | ||
| ManifestList((String, FormatVersion, SchemaId)), | ||
| ManifestList((String, FormatVersion)), |
There was a problem hiding this comment.
not for this PR, but now that schema_id is gone I'm a little curious what format_version is still buying us. Each ObjectCache is bound to a single table, so the format version is fixed for the cache's lifetime and can't disambiguate two lookups — the location is already unique.
Given xanderbailey's note that Java keys on location alone, I'd either drop it in a follow-up or leave a one-liner on why it stays, so the next reader isn't left guessing. wdyt?
There was a problem hiding this comment.
I think format_version should stay. The Java and Rust caching models have different semantics. The Java implementation caches raw bytes and parses after retrieval, so the cached value is version-agnostic. Rust implementation caches the parsed values, so the cached value is version-specific.
If we ever move Rust impelementation to a byte-cache like Java's, format_version could then come out of the key too, but that's a separate change.
What changes are included in this PR?
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), 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.
Are these changes tested?
Added new test
test_get_manifest_list_with_no_schema_id