{"record":{"id":"296343c5a41e6dbd","repo":"apache/iceberg","slug":"equality-delete-file-s-attached-to-main-data-file-296343","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.3/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.3/flink/src/main/java/org/apache/iceberg/flink/maintenance/operator/EqualityConvertReader.java#L222-L255","documentation":"Thrown by EqualityConvertReader.loadExistingDVs when an equality delete file is attached to a main data file while the staging branch is separate from the target branch (stagingOnTargetBranch is false). The converter expects equality deletes only on the staging branch, already converted to DVs on the target; an attached equality delete on the target means an unconverted delete leaked there and cannot be handled.","triggerScenarios":"Unconverted equality deletes committed directly to the target branch (e.g. by a job writing to targetBranch instead of stagingBranch, or stagingBranch == wrong branch configuration), while the converter runs in separate-branch mode and reads deletes for a main data file via existingDeletes.","commonSituations":"Misconfigured maintenance table properties pointing staging and target branches incorrectly; a normal Flink/Spark write job committing equality deletes straight to the target branch; recovery/rollback placing pre-conversion snapshots on the target.","solutions":["Ensure all delete-producing writers commit to the configured staging branch, not the target branch; check the maintenance job's branch configuration.","Run the conversion job so the leaked equality deletes on the target are converted (or use stagingOnTargetBranch mode if that matches your topology).","Compact/rewrite the affected target data files to eliminate the attached equality deletes, then rerun conversion."],"exampleFix":"// before: writers on target branch\nwriteJob(table).branch(targetBranch).execute(); // equality deletes land on target\nrunEqualityConvert(table);\n// after\nwriteJob(table).branch(stagingBranch).execute(); // deletes staged, converted to DVs\nrunEqualityConvert(table);","handlingStrategy":"validation","validationCode":"// confirm delete-producing writers commit to the staging branch\nif (!stagingBranch.equals(deleteWriterBranch)) {\n  throw new IllegalArgumentException(\"Writers must commit equality deletes to staging branch \" + stagingBranch);\n}","typeGuard":null,"tryCatchPattern":"try {\n  runEqualityConvertJob(table, cfg);\n} catch (IllegalStateException e) {\n  if (e.getMessage().startsWith(\"Equality delete file\")) {\n    LOG.error(\"Unconverted equality deletes on target; fix writer branch routing or run conversion\", e);\n  } else throw e;\n}","preventionTips":["Route all delete-producing jobs to the staging branch, never the target","Double-check staging/target branch properties in the maintenance config","Run conversion promptly after deletes are staged so nothing leaks","Use stagingOnTargetBranch mode only when your topology actually commits to the same branch"],"tags":["equality-deletes","branching","flink","precondition"],"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-14T16:17:12.679Z"}