{"record":{"id":"e054819b8db125a3","repo":"apache/iceberg","slug":"v2-positional-delete-file-s-attached-to-main-data-e05481","errorCode":null,"errorMessage":"V2 positional delete file %s attached to main data file %s; the converter expects a V3 target with deletion vectors only.","messagePattern":"V2 positional delete file (.+?) attached to main data file (.+?); the converter expects a V3 target with deletion vectors only\\.","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":230,"sourceCode":"  @Override\n  public void close() throws Exception {\n    super.close();\n    tableLoader.close();\n  }\n\n  private Schema appendRowPosition(Schema schema) {\n    List<Types.NestedField> columns = Lists.newArrayList(schema.columns());\n    columns.add(MetadataColumns.ROW_POSITION);\n    return new Schema(columns);\n  }\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()) {","sourceCodeStart":212,"sourceCodeEnd":248,"githubUrl":"https://github.com/apache/iceberg/blob/86d9c8fc543e7c56c9f624eb725f76c9baff9570/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/maintenance/operator/EqualityConvertReader.java#L212-L248","documentation":"Thrown by EqualityConvertReader.loadExistingDVs when a scan task for a main data file has an attached V2 positional delete file. The converter requires the V3 target branch to carry only deletion vectors for deletes; a positional delete on the target means legacy delete state that the converter cannot consume, so it aborts.","triggerScenarios":"Main-branch data files still referenced by positional delete files written before the table was upgraded to V3, or non-Flink/older writers committing positional deletes to the target branch, and the reader then loads existing deletes for such a file via existingDeletes.","commonSituations":"Upgrading format-version to 3 without rewriting/compacting old positional deletes away; Spark or Flink 1.x jobs with old Iceberg versions still writing to the target branch; restore of a pre-V3 snapshot onto the target branch.","solutions":["Rewrite/compact affected data files on the target branch so positional deletes are applied and removed (CALL rewrite_data_files / RewriteDataFilesAction).","Stop or upgrade any remaining V2-style writers (old Spark/Flink jobs) writing positional deletes to the target branch.","Verify table format-version is 3 and that no old snapshots with positional deletes were restored onto the branch."],"exampleFix":"// before\nspark.sql(\"ALTER TABLE db.t SET TBLPROPERTIES ('format-version'='3')\");\nrunEqualityConvert(table); // old positional deletes still attached\n// after\nspark.sql(\"ALTER TABLE db.t SET TBLPROPERTIES ('format-version'='3')\");\nspark.sql(\"CALL cat.system.rewrite_data_files(table => 'db.t', strategy => 'sort')\"); // absorb positional deletes\nrunEqualityConvert(table);","handlingStrategy":"validation","validationCode":"// check for positional deletes on the target before conversion\ntable.scan().useSnapshot(table.snapshotForBranch(targetBranch).snapshotId())\n    .planFiles().forEach(t -> t.deletes().forEach(d -> {\n      if (d.content() == FileContent.POSITION_DELETES) {\n        throw new IllegalStateException(\"Target has V2 positional deletes: \" + d.location());\n      }\n    }));","typeGuard":null,"tryCatchPattern":"try {\n  runEqualityConvertJob(table, cfg);\n} catch (IllegalStateException e) {\n  if (e.getMessage().contains(\"positional delete file\")) {\n    LOG.error(\"Target branch carries V2 positional deletes; run rewrite_data_files to absorb them\", e);\n  } else throw e;\n}","preventionTips":["Upgrade to format-version 3 and compact to remove legacy positional deletes","Upgrade or retire old writers that emit V2 positional deletes","Do not restore pre-V3 snapshots onto the target branch","Scan target deletes for POSITION_DELETES content before starting the job"],"tags":["deletion-vectors","format-version","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"}