apache/iceberg · error · IllegalStateException
V2 positional delete file %s attached to main data file %s;
Error message
V2 positional delete file %s attached to main data file %s; the converter expects a V3 target with deletion vectors only.
What it means
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.
Source
Thrown at flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/maintenance/operator/EqualityConvertReader.java:230
@Override
public void close() throws Exception {
super.close();
tableLoader.close();
}
private Schema appendRowPosition(Schema schema) {
List<Types.NestedField> columns = Lists.newArrayList(schema.columns());
columns.add(MetadataColumns.ROW_POSITION);
return new Schema(columns);
}
private PositionDeleteIndex loadExistingDVs(FileScanTask task, String dataFilePath) {
List<DeleteFile> dvs = Lists.newArrayList();
for (DeleteFile deleteFile : task.deletes()) {
if (ContentFileUtil.isDV(deleteFile)) {
dvs.add(deleteFile);
} else if (deleteFile.content() == FileContent.POSITION_DELETES) {
throw new IllegalStateException(
String.format(
"V2 positional delete file %s attached to main data file %s; "
+ "the converter expects a V3 target with deletion vectors only.",
deleteFile.location(), dataFilePath));
} else if (deleteFile.content() == FileContent.EQUALITY_DELETES && !stagingOnTargetBranch) {
// When stagingBranch == targetBranch the target carries unconverted equality deletes; they
// are indexed as rows here and converted via the planner's RESOLVE_DELETE commands. On a
// separate target branch an attached equality delete means an unconverted delete leaked
// onto the target, which the converter cannot reason about.
throw new IllegalStateException(
String.format(
"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.",
deleteFile.location(), dataFilePath));
}
}
if (dvs.isEmpty()) {View on GitHub (pinned to 86d9c8fc54)
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.
Example fix
// before
spark.sql("ALTER TABLE db.t SET TBLPROPERTIES ('format-version'='3')");
runEqualityConvert(table); // old positional deletes still attached
// after
spark.sql("ALTER TABLE db.t SET TBLPROPERTIES ('format-version'='3')");
spark.sql("CALL cat.system.rewrite_data_files(table => 'db.t', strategy => 'sort')"); // absorb positional deletes
runEqualityConvert(table); Defensive patterns
Strategy: validation
Validate before calling
// check for positional deletes on the target before conversion
table.scan().useSnapshot(table.snapshotForBranch(targetBranch).snapshotId())
.planFiles().forEach(t -> t.deletes().forEach(d -> {
if (d.content() == FileContent.POSITION_DELETES) {
throw new IllegalStateException("Target has V2 positional deletes: " + d.location());
}
})); Try / catch
try {
runEqualityConvertJob(table, cfg);
} catch (IllegalStateException e) {
if (e.getMessage().contains("positional delete file")) {
LOG.error("Target branch carries V2 positional deletes; run rewrite_data_files to absorb them", e);
} else throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.
Related errors
- V2 positional delete file %s attached to main data file %s;
- Staging snapshot %s on branch '%s' contains a V2 positional
- Staging snapshot %s on branch '%s' contains a V2 positional
- Staging snapshot %s on branch '%s' contains a V2 positional
- V2 positional delete file %s attached to main data file %s;
AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12).
Data as JSON: /api/errors/e054819b8db125a3.
Report an issue: GitHub.