{"record":{"id":"70f4e30ba571e147","repo":"apache/iceberg","slug":"equality-delete-file-s-attached-to-main-data-file-70f4e3","errorCode":null,"errorMessage":"Equality delete file %s attached to main data file %s; the converter expects equality deletes only on the staging branch, converted to DVs on the target.","messagePattern":"Equality delete file (.+?) attached to main data file (.+?); the converter expects equality deletes only on the staging branch, converted to DVs on the target\\.","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"flink/v2.1/flink/src/main/java/org/apache/iceberg/flink/maintenance/operator/EqualityConvertReader.java","lineNumber":240,"sourceCode":"  }\n\n  private PositionDeleteIndex loadExistingDVs(FileScanTask task, String dataFilePath) {\n    List<DeleteFile> dvs = Lists.newArrayList();\n    for (DeleteFile deleteFile : task.deletes()) {\n      if (ContentFileUtil.isDV(deleteFile)) {\n        dvs.add(deleteFile);\n      } else if (deleteFile.content() == FileContent.POSITION_DELETES) {\n        throw new IllegalStateException(\n            String.format(\n                \"V2 positional delete file %s attached to main data file %s; \"\n                    + \"the converter expects a V3 target with deletion vectors only.\",\n                deleteFile.location(), dataFilePath));\n      } else if (deleteFile.content() == FileContent.EQUALITY_DELETES && !stagingOnTargetBranch) {\n        // When stagingBranch == targetBranch the target carries unconverted equality deletes; they\n        // are indexed as rows here and converted via the planner's RESOLVE_DELETE commands. On a\n        // separate target branch an attached equality delete means an unconverted delete leaked\n        // onto the target, which the converter cannot reason about.\n        throw new IllegalStateException(\n            String.format(\n                \"Equality delete file %s attached to main data file %s; the converter expects \"\n                    + \"equality deletes only on the staging branch, converted to DVs on the target.\",\n                deleteFile.location(), dataFilePath));\n      }\n    }\n\n    if (dvs.isEmpty()) {\n      return null;\n    }\n\n    return deleteLoader.loadPositionDeletes(dvs, dataFilePath);\n  }\n}\n","sourceCodeStart":222,"sourceCodeEnd":255,"githubUrl":"https://github.com/apache/iceberg/blob/86d9c8fc543e7c56c9f624eb725f76c9baff9570/flink/v2.1/flink/src/main/java/org/apache/iceberg/flink/maintenance/operator/EqualityConvertReader.java#L222-L255","documentation":"loadExistingDVs rejects equality delete files attached directly to main data files when staging and target branches differ. The converter expects equality deletes only on the staging branch, where they are read as rows and converted to deletion vectors on the target; an equality delete attached on the target branch is an unconverted leak the converter cannot reason about, so it throws IllegalStateException naming the delete and data files.","triggerScenarios":"task.deletes() on a main-branch data file contains a DeleteFile with content == FileContent.EQUALITY_DELETES while stagingOnTargetBranch is false (separate staging branch configured).","commonSituations":"A normal Flink/Spark writer with merge-on-read equality deletes committed to the target branch while the converter maintenance job is enabled with a separate staging branch; misconfigured branch names so 'target' receives raw equality deletes.","solutions":["Configure writers to send equality deletes to the staging branch (or enable staging on the target branch) while the converter is active.","Trigger an immediate conversion cycle that consumes the leaked equality delete, then resume normal maintenance.","Double-check the maintenance table-branch configuration so the target branch only holds converted DV output.","Temporarily disable writers to the target branch (or quiesce commits via the trigger lock) until the backlog of equality deletes is converted."],"exampleFix":"// before\n// staging and target differ, but an eq delete landed on target\n// after\n// either run staging on the same branch:\nbuilder.stagingBranch(targetBranch);\n// or pause merge-on-read writes to targetBranch until conversion catches up","handlingStrategy":"validation","validationCode":"boolean eqDeletesOnTarget = table.newScan().useBranch(targetBranch)\n    .planFiles().stream()\n    .flatMap(t -> t.deletes().stream())\n    .anyMatch(d -> d.content() == FileContent.EQUALITY_DELETES);\nPreconditions.checkState(!eqDeletesOnTarget, \"Unconverted equality deletes on target branch\");","typeGuard":null,"tryCatchPattern":"try {\n  loadExistingDVs(task, dataFilePath);\n} catch (IllegalStateException e) {\n  LOG.error(\"Unconverted equality deletes leaked to target: {}\", e.getMessage());\n  // trigger an immediate conversion cycle or pause writers\n}","preventionTips":["Route all merge-on-read equality deletes to the staging branch, or configure staging on the target branch","Quiesce or throttle writers to the target branch while the converter runs","Audit branch configuration (stagingBranch vs targetBranch) when enabling the maintenance job"],"tags":["iceberg","equality-deletes","branching","table-maintenance"],"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"}