{"record":{"id":"f21ffae19f2c0459","repo":"apache/iceberg","slug":"cannot-cherrypick-snapshot-s-already-an-ancestor","errorCode":null,"errorMessage":"Cannot cherrypick snapshot %s: already an ancestor","messagePattern":"Cannot cherrypick snapshot (.+?): already an ancestor","errorType":"validation","errorClass":"CherrypickAncestorCommitException","httpStatus":null,"severity":"error","filePath":"core/src/main/java/org/apache/iceberg/CherryPickOperation.java","lineNumber":209,"sourceCode":"    }\n\n    boolean isFastForward = isFastForward(base);\n    if (requireFastForward || isFastForward) {\n      ValidationException.check(\n          isFastForward,\n          \"Cannot cherry-pick snapshot %s: not append, dynamic overwrite, or fast-forward\",\n          cherrypickSnapshot.snapshotId());\n      return base.snapshot(cherrypickSnapshot.snapshotId());\n    } else {\n      // validate(TableMetadata) is called in apply(TableMetadata) after this apply refreshes the\n      // table state\n      return super.apply();\n    }\n  }\n\n  private static void validateNonAncestor(TableMetadata meta, long snapshotId) {\n    if (isCurrentAncestor(meta, snapshotId)) {\n      throw new CherrypickAncestorCommitException(snapshotId);\n    }\n\n    Long ancestorId = lookupAncestorBySourceSnapshot(meta, snapshotId);\n    if (ancestorId != null) {\n      throw new CherrypickAncestorCommitException(snapshotId, ancestorId);\n    }\n  }\n\n  private static void validateReplacedPartitions(\n      TableMetadata meta, Long parentId, PartitionSet replacedPartitions, FileIO io) {\n    if (replacedPartitions != null && meta.currentSnapshot() != null) {\n      ValidationException.check(\n          parentId == null || isCurrentAncestor(meta, parentId),\n          \"Cannot cherry-pick overwrite, based on non-ancestor of the current state: %s\",\n          parentId);\n      List<Snapshot> snapshots =\n          Lists.newArrayList(\n              SnapshotUtil.ancestorsBetween(","sourceCodeStart":191,"sourceCodeEnd":227,"githubUrl":"https://github.com/apache/iceberg/blob/86d9c8fc543e7c56c9f624eb725f76c9baff9570/core/src/main/java/org/apache/iceberg/CherryPickOperation.java#L191-L227","documentation":"CherryPickOperation.validateNonAncestor rejects cherry-picking a snapshot that is already an ancestor of the table's current state, because re-applying it would be a no-op/conflict. If the target snapshot is the current ancestor chain, CherrypickAncestorCommitException is thrown — either for the snapshot itself or for an ancestor already produced by the same source snapshot. This validation happens during the apply/validate phase of the cherry-pick scan task.","triggerScenarios":"Calling cherryPick(snapshotId) (RemoveSnapshots/ManageSnapshots cherryPick) with a snapshot ID that is already in the current table's ancestor chain, or a snapshot whose source-snapshot-id already produced an existing ancestor.","commonSituations":"Re-running the same cherry-pick operation twice; picking a snapshot from the main branch back onto main itself; fast-forward-style workflows where the branch was already rolled back into the current history.","solutions":["Check whether the snapshot is already an ancestor before cherry-picking: compare with the table's current snapshot history and skip if present.","Make the operation idempotent in your workflow code: catch CherrypickAncestorCommitException and treat it as success.","Cherry-pick onto a different branch whose current snapshot is not already the picked snapshot.","Use rollback to the intended ancestor instead of cherry-pick when the goal is restoring existing history."],"exampleFix":"// before\ntable.manageSnapshots().cherryPick(snapshotId).commit(); // throws if already ancestor\n\n// after\nSnapshot picked = table.snapshot(snapshotId);\nboolean alreadyAncestor = ((HasSnapshotOperations) table).operations().current()\n    .currentSnapshotsAsIds().contains(snapshotId);\nif (!alreadyAncestor) {\n  table.manageSnapshots().cherryPick(snapshotId).commit();\n}","handlingStrategy":"validation","validationCode":"TableMetadata meta = ((HasTableOperations) table).operations().current();\nboolean isAncestor = meta.currentSnapshotsAsIds().stream()\n    .anyMatch(id -> id == snapshotId) // plus check via history/current-ancestor chain\n    || org.apache.iceberg.events.Listeners.instance()\n        != null; // caller-specific ancestor check via snapshot log\nboolean alreadyPicked = meta.snapshots().stream()\n    .anyMatch(s -> s.snapshotId() == snapshotId);","typeGuard":null,"tryCatchPattern":"try {\n  table.manageSnapshots().cherryPick(snapshotId).commit();\n} catch (CherrypickAncestorCommitException e) {\n  // snapshot is already an ancestor — treat as no-op\n  LOG.info(\"Snapshot {} already in current history; skipping cherry-pick\", snapshotId);\n}","preventionTips":["Skip cherry-pick when the snapshot ID already appears in the target branch's snapshot history.","Make retry/idempotency wrappers treat CherrypickAncestorCommitException as success.","Only cherry-pick snapshots whose parent lineage diverges from the target branch's current ancestor chain."],"tags":["snapshot","cherry-pick","validation","invalid-state-transition"],"backgroundTag":"invalid-state-transition","analyzedSha":"86d9c8fc543e7c56c9f624eb725f76c9baff9570","analyzedAt":"2026-09-12T00:46:39.097Z","contentChangedAt":"2026-09-12T00:46:39.097Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}