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

  1. Set the table format version to >=3 (recreate or migrate the table) so Flink writes DVs instead of positional deletes.
  2. Ensure all writers on the staging branch are Flink V3 DV writers; do not run Spark/Trino position-delete writers against the staging branch.
  3. Identify which commit wrote the positional delete file (inspect snapshot history) and remove/rebase that commit from the staging branch.
  4. 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

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


AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12). Data as JSON: /api/errors/cfe7a0038311f403. Report an issue: GitHub.