{"record":{"id":"b1dd3910117979f1","repo":"apache/iceberg","slug":"v2-positional-delete-file-s-attached-to-main-data","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/v1.20/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/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/operator/EqualityConvertReader.java#L212-L248","documentation":"While collecting existing deletes attached to a main-branch data file, the loader found a V2 positional delete file. The converter requires the target to be format version 3 with deletes represented only as deletion vectors, so encountering a positional delete means the table state is incompatible with the conversion and the operator fails fast.","triggerScenarios":"EqualityConvertReader.loadExistingDVs() iterates task.deletes() for a main data file and hits a DeleteFile with content POSITION_DELETES - i.e. some writer produced classic positional deletes on the target branch.","commonSituations":"Target table at format-version 2; Spark/Flink writers without DV support committing to the target branch; stale engine writing to a table that was migrated to V3 but whose branch still has old delete files.","solutions":["Upgrade the target table to format-version 3 so all subsequent deletes become DVs.","Find which writer/job committed positional deletes on the target branch and switch it to a V3 DV writer.","Remove or convert legacy positional delete files (e.g. rewrite/compact the affected data files) before running conversion.","Run the converter only on branches whose delete files are DVs (validated via ContentFileUtil.isDV)."],"exampleFix":"// before: Spark job writing to main at format-version 2\nspark.sql(\"DELETE FROM t WHERE id = 42\"); // emits positional deletes\n// after: migrate table to V3 so deletes are DVs\nspark.sql(\"ALTER TABLE t SET TBLPROPERTIES ('format-version'='3')\");","handlingStrategy":"validation","validationCode":"// before conversion, ensure target branch has no positional deletes\ntable.snapshot(targetBranch).deleteFiles(table.io()).forEach(f ->\n  checkState(f.content() != FileContent.POSITION_DELETES,\n      \"positional delete on target: \" + f.location()));","typeGuard":"boolean targetHasOnlyDvs(Table table, String branch) {\n  return table.snapshot(branch).deleteFiles(table.io()).stream()\n      .noneMatch(f -> f.content() == FileContent.POSITION_DELETES);\n}","tryCatchPattern":"try {\n  convert(table);\n} catch (IllegalStateException e) {\n  if (e.getMessage().startsWith(\"V2 positional delete file\")) {\n    compactAffectedDataFiles(); // removes positional deletes, then rerun\n  } else throw e;\n}","preventionTips":["Migrate the table to format-version 3 before converting.","Ensure every writer job targets V3 DV delete mode.","Exclude legacy V2 delete files via compaction before running conversion.","Add a pre-flight delete-file content audit to the maintenance pipeline."],"tags":["flink","format-version","positional-delete"],"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"}