apache/iceberg · error · IllegalStateException
Staging snapshot %s on branch '%s' contains a V2 positional
Error message
Staging snapshot %s on branch '%s' contains a V2 positional delete file (%s); equality delete conversion expects a V3 staging branch written by Flink, which produces only deletion vectors for deletes.
What it means
The planner found a V2 positional delete file among the staging branch's delete files. Equality delete conversion expects a V3 staging branch where Flink writes only deletion vectors (DVs); positional deletes indicate the table or branch is not in the expected format version or was written by a non-DV producer, so the planner fails fast.
Source
Thrown at flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/operator/EqualityConvertPlanner.java:584
for (DeleteFile deleteFile : changes.addedDeleteFiles()) {
if (deleteFile.content() == FileContent.EQUALITY_DELETES) {
Set<Integer> deleteFieldIds = Sets.newHashSet(deleteFile.equalityFieldIds());
Preconditions.checkState(
deleteFieldIds.equals(eqFieldIds),
"Staging snapshot %s on branch '%s' contains an equality delete file %s with "
+ "equalityFieldIds=%s, which does not match the configured eqFieldIds=%s. "
+ "The writer must use the same equality field IDs as the converter.",
stagingSnapshot.snapshotId(),
stagingBranch,
deleteFile.location(),
deleteFieldIds,
eqFieldIds);
validateDeleteSpecPartitionColumns(stagingSnapshot, deleteFile);
eqDeleteFiles.add(deleteFile);
} else if (ContentFileUtil.isDV(deleteFile)) {
stagingDVFiles.add(deleteFile);
} else {
throw new IllegalStateException(
String.format(
"Staging snapshot %s on branch '%s' contains a V2 positional delete file (%s); "
+ "equality delete conversion expects a V3 staging branch written by Flink, "
+ "which produces only deletion vectors for deletes.",
stagingSnapshot.snapshotId(), stagingBranch, deleteFile.location()));
}
}
return new StagingInputs(newDataFiles, stagingDVFiles, eqDeleteFiles);
}
private void validateDeleteSpecPartitionColumns(Snapshot stagingSnapshot, DeleteFile deleteFile) {
PartitionSpec spec = table.specs().get(deleteFile.specId());
for (PartitionField field : spec.fields()) {
Preconditions.checkState(
eqFieldIds.contains(field.sourceId()),
"Staging snapshot %s on branch '%s' contains an equality delete file %s under spec %s, "
+ "which partitions by field '%s' (source id %s) that is not an equality field %s. "View on GitHub (pinned to 86d9c8fc54)
Solutions
- Set the table format version to >=3 (recreate or migrate the table) so Flink writes DVs instead of positional deletes.
- Ensure all writers on the staging branch are Flink V3 DV writers; do not run Spark/Trino position-delete writers against the staging branch.
- Identify which commit wrote the positional delete file (inspect snapshot history) and remove/rebase that commit from the staging branch.
- Re-run the conversion after the staging branch contains only DVs and equality deletes.
Example fix
// before
CREATE TABLE t (...) TBLPROPERTIES ('format-version'='2');
// after
CREATE TABLE t (...) TBLPROPERTIES ('format-version'='3'); Defensive patterns
Strategy: validation
Validate before calling
// pre-flight: table must be V3 and staging deletes must be DVs only
if (table.operations().current().formatVersion() < 3)
throw new IllegalArgumentException("format-version >= 3 required");
table.snapshot(branch).dataManifests(table.io()).forEach(m ->
ManifestFiles.readDeleteManifest(m, table.io(), null).deleteFiles()
.forEach(f -> checkState(ContentFileUtil.isDV(f) || f.content() == FileContent.EQUALITY_DELETES)));
Type guard
boolean allDeletesAreDvOrEq(Snapshot s, Table table) {
return s.deleteFiles(table.io()).stream()
.allMatch(f -> ContentFileUtil.isDV(f) || f.content() == FileContent.EQUALITY_DELETES);
} Try / catch
try {
planner.inputs();
} catch (IllegalStateException e) {
if (e.getMessage().contains("positional delete file")) {
migrateTableToV3(); // then rerun
} else throw e;
} Prevention
- Set format-version=3 before enabling equality-delete conversion.
- Only allow Flink V3 DV writers on the staging branch; block Spark/Trino writers there.
- Verify writer engines' delete modes in pipeline configs (no position-delete mode).
- Pre-flight scan staging manifests for non-DV delete files.
When it happens
Trigger: EqualityConvertPlanner.retrieveStagingFiles() iterates delete files of the staging snapshot and encounters a delete file that is neither an equality delete nor a DV (i.e. a position delete file).
Common situations: The table's format version is still 2 (or the branch was written by a V2 writer) so deletes are stored as positional delete files; an engine other than Flink (Spark, Trino) wrote positional deletes onto the staging branch; format-version upgrade not applied before running conversion.
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
- V2 positional delete file %s attached to main data file %s;
- Staging snapshot %s on branch '%s' contains a V2 positional
AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12).
Data as JSON: /api/errors/cfe7a0038311f403.
Report an issue: GitHub.