{"record":{"id":"52334c36d62dc1f5","repo":"apache/iceberg","slug":"cannot-incrementally-clean-files-when-there-are-sn","errorCode":null,"errorMessage":"Cannot incrementally clean files when there are snapshots outside of main","messagePattern":"Cannot incrementally clean files when there are snapshots outside of main","errorType":"exception","errorClass":"UnsupportedOperationException","httpStatus":null,"severity":"error","filePath":"core/src/main/java/org/apache/iceberg/RemoveSnapshots.java","lineNumber":425,"sourceCode":"            : new ReachableFileCleanup(\n                ops.io(), deleteExecutorService, planExecutorService(), deleteFunc);\n\n    cleanupStrategy.cleanFiles(base, current, cleanupLevel);\n  }\n\n  private void validateCleanupCanBeIncremental(TableMetadata current) {\n    if (specifiedSnapshotId) {\n      throw new UnsupportedOperationException(\n          \"Cannot clean files incrementally when snapshot IDs are specified\");\n    }\n\n    if (hasRemovedNonMainAncestors(base, current)) {\n      throw new UnsupportedOperationException(\n          \"Cannot incrementally clean files when snapshots outside of main ancestry were removed\");\n    }\n\n    if (hasNonMainSnapshots(current)) {\n      throw new UnsupportedOperationException(\n          \"Cannot incrementally clean files when there are snapshots outside of main\");\n    }\n  }\n\n  private boolean hasRemovedNonMainAncestors(\n      TableMetadata beforeExpiration, TableMetadata afterExpiration) {\n    Set<Long> mainAncestors = mainAncestors(beforeExpiration);\n    for (Snapshot snapshotBeforeExpiration : beforeExpiration.snapshots()) {\n      boolean removedSnapshot =\n          afterExpiration.snapshot(snapshotBeforeExpiration.snapshotId()) == null;\n      boolean snapshotInMainAncestry =\n          mainAncestors.contains(snapshotBeforeExpiration.snapshotId());\n      if (removedSnapshot && !snapshotInMainAncestry) {\n        return true;\n      }\n    }\n\n    return false;","sourceCodeStart":407,"sourceCodeEnd":443,"githubUrl":"https://github.com/apache/iceberg/blob/86d9c8fc543e7c56c9f624eb725f76c9baff9570/core/src/main/java/org/apache/iceberg/RemoveSnapshots.java#L407-L443","documentation":"Even after expiration, the resulting table metadata may still contain snapshots outside the main ancestry (e.g. snapshots referenced by branches). Incremental cleanup cannot handle those, so RemoveSnapshots throws UnsupportedOperationException when hasNonMainSnapshots(current) is true.","triggerScenarios":"expireSnapshots with incremental cleanup on a table that retains non-main snapshots after expiration — typically snapshots still referenced by active branches or tags.","commonSituations":"Tables with long-lived feature branches; branch-based pipelines (WAP); users enabling incremental cleanup without realizing branches keep snapshots alive.","solutions":["Use full file cleanup (don't request incremental) when branches/non-main snapshots exist","Delete or merge the extra branches (removeBranch / removeSnapshots on those branches) so all snapshots are on main, then run incremental cleanup","Switch to timestamp-based expiration with full cleanup as a one-time consolidation step"],"exampleFix":"// before\nexpireSnapshots(table).withIncrementalCleanup().cleanExpiredFiles(true).execute();\n// after\n// remove extra branches first\ntable.manageSnapshots().dropBranch(\"feature-x\").commit();\nexpireSnapshots(table).withIncrementalCleanup().cleanExpiredFiles(true).execute();","handlingStrategy":"validation","validationCode":"TableMetadata current = ((HasTableOperations) table).operations().current();\nSet<Long> main = Sets.newHashSet(current.mainAncestorIds());\nboolean nonMainPresent = current.snapshots().stream().anyMatch(s -> !main.contains(s.snapshotId()));\nif (nonMainPresent) { /* drop branches or use full cleanup */ }","typeGuard":null,"tryCatchPattern":"try {\n  expireSnapshots(table).withIncrementalCleanup().execute();\n} catch (UnsupportedOperationException e) {\n  expireSnapshots(table).cleanExpiredFiles(true).execute(); // full cleanup\n}","preventionTips":["Remove unused branches/tags before incremental cleanup","Default to full cleanup when branches are in use","Monitor branch usage in table maintenance jobs"],"tags":["iceberg","snapshot-expiration","branches"],"backgroundTag":"conflicting-config-options","analyzedSha":"86d9c8fc543e7c56c9f624eb725f76c9baff9570","analyzedAt":"2026-09-12T00:46:39.097Z","contentChangedAt":"2026-09-12T00:46:39.097Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}