{"record":{"id":"5a82917df0056f10","repo":"apache/iceberg","slug":"cannot-expire-snapshots-from-a-s-table","errorCode":null,"errorMessage":"Cannot expire snapshots from a %s table","messagePattern":"Cannot expire snapshots from a (.+?) table","errorType":"exception","errorClass":"UnsupportedOperationException","httpStatus":null,"severity":"error","filePath":"core/src/main/java/org/apache/iceberg/BaseReadOnlyTable.java","lineNumber":111,"sourceCode":"  public DeleteFiles newDelete() {\n    throw new UnsupportedOperationException(\"Cannot delete from a \" + descriptor + \" table\");\n  }\n\n  @Override\n  public UpdateStatistics updateStatistics() {\n    throw new UnsupportedOperationException(\n        \"Cannot update statistics of a \" + descriptor + \" table\");\n  }\n\n  @Override\n  public UpdatePartitionStatistics updatePartitionStatistics() {\n    throw new UnsupportedOperationException(\n        \"Cannot update partition statistics of a \" + descriptor + \" table\");\n  }\n\n  @Override\n  public ExpireSnapshots expireSnapshots() {\n    throw new UnsupportedOperationException(\n        \"Cannot expire snapshots from a \" + descriptor + \" table\");\n  }\n\n  @Override\n  public ManageSnapshots manageSnapshots() {\n    throw new UnsupportedOperationException(\n        \"Cannot manage snapshots in a \" + descriptor + \" table\");\n  }\n\n  @Override\n  public Transaction newTransaction() {\n    throw new UnsupportedOperationException(\n        \"Cannot create transactions for a \" + descriptor + \" table\");\n  }\n}\n","sourceCodeStart":93,"sourceCodeEnd":127,"githubUrl":"https://github.com/apache/iceberg/blob/86d9c8fc543e7c56c9f624eb725f76c9baff9570/core/src/main/java/org/apache/iceberg/BaseReadOnlyTable.java#L93-L127","documentation":"Read-only table guard in BaseReadOnlyTable: expireSnapshots() mutates table state, which is not allowed on this read-only view of a table (e.g. a materialized/branch view described by 'descriptor'). Any call fails before touching metadata — the table handle, not the snapshots, is the limiting factor.","triggerScenarios":"Calling table.expireSnapshots() on a snapshot wrapper or any BaseReadOnlyTable-backed descriptor.","commonSituations":"Retention/cleanup jobs iterating snapshots and attempting expiration directly on the snapshot object instead of the parent table.","solutions":["Call expireSnapshots() on the live table loaded via catalog.loadTable().","Perform expiration inside a transaction on the writable table.","In generic cleanup code, resolve the parent table handle before expiring."],"exampleFix":"// before\nreadOnlyTable.expireSnapshots().expireOlderThan(ts).commit();\n// after\nTable table = catalog.loadTable(tableIdentifier);\ntable.expireSnapshots().expireOlderThan(ts).commit();","handlingStrategy":"validation","validationCode":"if (!(table instanceof BaseReadOnlyTable)) {\n  table.expireSnapshots().expireOlderThan(cutoff).commit();\n}","typeGuard":"boolean supportsExpiration(Table t) { return !(t instanceof BaseReadOnlyTable); }","tryCatchPattern":"try { table.expireSnapshots()...commit(); } catch (UnsupportedOperationException e) { /* expire via the parent table instead */ }","preventionTips":["Run expiration jobs against catalog-loaded tables only","Never iterate snapshot objects calling expireSnapshots on each"],"tags":["unsupported-operation","read-only-table","snapshot-expiration"],"backgroundTag":"unsupported-operation","analyzedSha":"86d9c8fc543e7c56c9f624eb725f76c9baff9570","analyzedAt":"2026-09-12T00:46:39.097Z","contentChangedAt":"2026-09-12T00:46:39.097Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}