{"record":{"id":"00cf333d161e98d4","repo":"apache/iceberg","slug":"cannot-clean-files-incrementally-when-snapshot-ids","errorCode":null,"errorMessage":"Cannot clean files incrementally when snapshot IDs are specified","messagePattern":"Cannot clean files incrementally when snapshot IDs are specified","errorType":"exception","errorClass":"UnsupportedOperationException","httpStatus":null,"severity":"error","filePath":"core/src/main/java/org/apache/iceberg/RemoveSnapshots.java","lineNumber":415,"sourceCode":"              && !hasNonMainSnapshots(current);\n    }\n\n    LOG.info(\n        \"Cleaning up expired files (local, {})\", incrementalCleanup ? \"incremental\" : \"reachable\");\n\n    FileCleanupStrategy cleanupStrategy =\n        incrementalCleanup\n            ? new IncrementalFileCleanup(\n                ops.io(), deleteExecutorService, planExecutorService(), deleteFunc)\n            : 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()) {","sourceCodeStart":397,"sourceCodeEnd":433,"githubUrl":"https://github.com/apache/iceberg/blob/86d9c8fc543e7c56c9f624eb725f76c9baff9570/core/src/main/java/org/apache/iceberg/RemoveSnapshots.java#L397-L433","documentation":"RemoveSnapshots.validateCleanupCanBeIncremental rejects incremental file cleanup when the expiration was targeted at a specific snapshot ID (expireSnapshotsTo/expireSnapshotsById). Incremental cleanup is only valid for the default timestamp-based expiration of main-ancestry snapshots, so the combination is rejected with UnsupportedOperationException.","triggerScenarios":"Calling expireSnapshots(table).expireSnapshotsId(someId)... (or .expireSnapshotsTo(timestamp) with a specified ID path) while cleanup level is incremental — i.e., cleanExpiredFiles enabled with incremental strategy and specifiedSnapshotId is true.","commonSituations":"Users combining programmatic per-snapshot expiration APIs with the incremental cleanup option introduced for timestamp-based expiration; configuration defaults enabling incremental cleanup while scripts call expireById.","solutions":["Drop the snapshot-ID targeting and use default (timestamp/ancestry-based) expiration if incremental cleanup is desired","Keep the specified snapshot ID but switch the cleanup level to full (e.g. do not request incremental cleanup; use the full cleanup strategy)","Split the work: run targeted expiration without incremental cleanup, then a normal expiration with incremental cleanup"],"exampleFix":"// before\nexpireSnapshots(table)\n    .cleanExpiredFiles(true)\n    .withIncrementalCleanup()\n    .expireSnapshotsId(12345L)\n    .execute();\n// after\nexpireSnapshots(table)\n    .cleanExpiredFiles(true)\n    .withIncrementalCleanup()\n    .execute(); // default ancestry-based expiration, or remove withIncrementalCleanup() if ID targeting is required","handlingStrategy":"validation","validationCode":"// before enabling incremental cleanup, ensure no snapshot ID was specified\nboolean specifiedSnapshotId = /* tracked from expireSnapshotsId/To calls */;\nif (specifiedSnapshotId) {\n  throw new IllegalArgumentException(\"Incremental cleanup requires default (non-ID-targeted) expiration\");\n}","typeGuard":null,"tryCatchPattern":"try {\n  expireSnapshots(table).withIncrementalCleanup().execute();\n} catch (UnsupportedOperationException e) {\n  // rerun without incremental cleanup or without ID targeting\n}","preventionTips":["Use expireSnapshotsId/To only with full cleanup","Pick one: targeted expiration OR incremental cleanup","Document cleanup strategy in job configuration to avoid mixing modes"],"tags":["iceberg","snapshot-expiration","configuration"],"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"}