{"record":{"id":"368609637050344a","repo":"apache/iceberg","slug":"equality-delete-file-s-attached-to-main-data-file-368609","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.2/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.2/flink/src/main/java/org/apache/iceberg/flink/maintenance/operator/EqualityConvertReader.java#L222-L255","documentation":"loadExistingDVs treats equality delete files attached to main data files as an error unless stagingBranch equals targetBranch. When staging is on a separate branch, any equality delete found on the target means an unconverted delete leaked onto the target branch, which the converter cannot reason about — it throws IllegalStateException naming the delete file and data file.","triggerScenarios":"existingDeletes -> loadExistingDVs finds a FileContent.EQUALITY_DELETES delete attached to a main data file while stagingBranch != targetBranch, i.e., a writer committed equality deletes directly to the target branch outside the conversion flow.","commonSituations":"A legacy v2 writer (Spark or Flink with equality deletes) writing to the target branch concurrently with the converter; misconfigured writer targeting the wrong branch; branch routing mistakes in multi-job setups.","solutions":["Redirect all writers that produce equality deletes to the staging branch (or upgrade them to v3 DV writers on main).","Pause legacy equality-delete writers while the conversion runs, then rerun the cycle.","Verify the branch routing of every writer job; only the converter's staging flow may add equality deletes.","If the layout should allow it, set stagingBranch == targetBranch so unconverted deletes are indexed and converted via RESOLVE_DELETE commands instead of erroring."],"exampleFix":"// before\nEqualityDeleteConversionConfig.builder()\n    .stagingBranch(\"__iceberg_edc_staging\")\n    .targetBranch(\"main\") // legacy v2 writer still commits eq-deletes to main\n    .build();\n// after\n// stop the legacy writer, or align branches so unconverted deletes are handled:\nEqualityDeleteConversionConfig.builder()\n    .stagingBranch(\"main\")\n    .targetBranch(\"main\")\n    .build();","handlingStrategy":"validation","validationCode":"// confirm no unconverted equality deletes exist on the target when staging != target\nif (!stagingBranch.equals(targetBranch)) {\n  for (FileScanTask t : table.newScan().useBranch(targetBranch).planFiles()) {\n    for (DeleteFile d : t.deletes()) {\n      if (d.content() == FileContent.EQUALITY_DELETES) {\n        throw new IllegalStateException(\"Unconverted eq-delete on target: \" + d.location());\n      }\n    }\n  }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Route all equality-delete writers to the staging branch or upgrade them to v3 DV writers.","Set stagingBranch == targetBranch if the target legitimately carries unconverted deletes.","Pause legacy writers during conversion cycles.","Audit writer branch configuration in multi-job setups."],"tags":["equality-deletes","branching","flink","precondition"],"backgroundTag":"incompatible-source-type","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"}