Apache Iceberg version
1.11.0 (latest release)
Query engine
None
Please describe the bug 🐞
Behavior
In snapshot-loading-mode=refs, CatalogHandlers.loadTable fails with IllegalArgumentException: Cannot set metadata location with changes to table metadata: 1 changes when serving GET .../tables/{table}?snapshots=refs for a table that has a statistics file (or partition statistics file) attached to a historical (unreferenced) snapshot.
Cause
The REFS branch builds the response metadata like this (CatalogHandlers.java#L526-L531):
metadata =
TableMetadata.buildFrom(loadedMetadata)
.withMetadataLocation(loadedMetadata.metadataFileLocation())
.suppressHistoricalSnapshots()
.build();
suppressHistoricalSnapshots() does not record RemoveSnapshots changes, but it removes the suppressed snapshots' statistics via removeStatistics(...) / removePartitionStatistics(...), which do record MetadataUpdate.RemoveStatistics / RemovePartitionStatistics changes (TableMetadata.java, rewriteSnapshotsInternal). build() then rejects the combination of pending changes and a set metadata location.
The client-side equivalent in RESTSessionCatalog.loadTable already prevents this with by calling .discardChanges() when rebuilding metadata; CatalogHandlers performs the change-recording suppression without that measure.
To Reproduce
E.g. via RESTCatalogAdapter with snapshot-loading-mode=refs on the client:
- Create a table, commit snapshot A, attach a
StatisticsFile to A (e.g. UpdateStatistics).
- Commit snapshot B so A becomes historical (only reachable via parent chain, not via refs).
loadTable with snapshots=refs → 500 / IllegalArgumentException: Cannot set metadata location with changes to table metadata: 1 changes.
Encountered in practice testing Trino's REST catalog against the in-memory test server with snapshot-loading-mode=refs: Trino writes statistics on INSERT by default, so the first loadTable after a stats-bearing snapshot becomes historical reliably fails.
Suggested Fix
Add .discardChanges() to the builder chain in CatalogHandlers.loadTable (matching the client-side wrapper), e.g.
TableMetadata.buildFrom(loadedMetadata)
.withMetadataLocation(loadedMetadata.metadataFileLocation())
.suppressHistoricalSnapshots()
.discardChanges()
.build();
This would be the minimal fix, keeping the general behavior as is. Alternatively, the suppress-parameter that already prevents MetadataUpdate.RemoveSnapshots changes from being created here could be incorporated into removeStatistics() and removePartitionStatistics() to prevent change creation there at the root.
AI assistance
Found this while working on a trino patch. This root-cause analysis was AI-assisted. I'm quite confident this is an actual bug and checked this in detail before opening the issue. I'll link to the related PR at trino shortly, whose test triggers this behavior.
Willingness to contribute
Apache Iceberg version
1.11.0 (latest release)
Query engine
None
Please describe the bug 🐞
Behavior
In
snapshot-loading-mode=refs,CatalogHandlers.loadTablefails withIllegalArgumentException: Cannot set metadata location with changes to table metadata: 1 changeswhen servingGET .../tables/{table}?snapshots=refsfor a table that has a statistics file (or partition statistics file) attached to a historical (unreferenced) snapshot.Cause
The
REFSbranch builds the response metadata like this (CatalogHandlers.java#L526-L531):suppressHistoricalSnapshots()does not recordRemoveSnapshotschanges, but it removes the suppressed snapshots' statistics viaremoveStatistics(...)/removePartitionStatistics(...), which do recordMetadataUpdate.RemoveStatistics/RemovePartitionStatisticschanges (TableMetadata.java, rewriteSnapshotsInternal).build()then rejects the combination of pending changes and a set metadata location.The client-side equivalent in
RESTSessionCatalog.loadTablealready prevents this with by calling.discardChanges()when rebuilding metadata;CatalogHandlersperforms the change-recording suppression without that measure.To Reproduce
E.g. via
RESTCatalogAdapterwithsnapshot-loading-mode=refson the client:StatisticsFileto A (e.g.UpdateStatistics).loadTablewithsnapshots=refs→ 500 /IllegalArgumentException: Cannot set metadata location with changes to table metadata: 1 changes.Encountered in practice testing Trino's REST catalog against the in-memory test server with
snapshot-loading-mode=refs: Trino writes statistics on INSERT by default, so the firstloadTableafter a stats-bearing snapshot becomes historical reliably fails.Suggested Fix
Add
.discardChanges()to the builder chain inCatalogHandlers.loadTable(matching the client-side wrapper), e.g.This would be the minimal fix, keeping the general behavior as is. Alternatively, the
suppress-parameter that already preventsMetadataUpdate.RemoveSnapshotschanges from being created here could be incorporated intoremoveStatistics()andremovePartitionStatistics()to prevent change creation there at the root.AI assistance
Found this while working on a trino patch. This root-cause analysis was AI-assisted. I'm quite confident this is an actual bug and checked this in detail before opening the issue. I'll link to the related PR at trino shortly, whose test triggers this behavior.
Willingness to contribute