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

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.

Source

Thrown at flink/v1.20/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

  1. Upgrade the target table to format-version 3 so all subsequent deletes become DVs.
  2. Find which writer/job committed positional deletes on the target branch and switch it to a V3 DV writer.
  3. Remove or convert legacy positional delete files (e.g. rewrite/compact the affected data files) before running conversion.
  4. Run the converter only on branches whose delete files are DVs (validated via ContentFileUtil.isDV).

Example fix

// before: Spark job writing to main at format-version 2
spark.sql("DELETE FROM t WHERE id = 42"); // emits positional deletes
// after: migrate table to V3 so deletes are DVs
spark.sql("ALTER TABLE t SET TBLPROPERTIES ('format-version'='3')");
Defensive patterns

Strategy: validation

Validate before calling

// before conversion, ensure target branch has no positional deletes
table.snapshot(targetBranch).deleteFiles(table.io()).forEach(f ->
  checkState(f.content() != FileContent.POSITION_DELETES,
      "positional delete on target: " + f.location()));

Type guard

boolean targetHasOnlyDvs(Table table, String branch) {
  return table.snapshot(branch).deleteFiles(table.io()).stream()
      .noneMatch(f -> f.content() == FileContent.POSITION_DELETES);
}

Try / catch

try {
  convert(table);
} catch (IllegalStateException e) {
  if (e.getMessage().startsWith("V2 positional delete file")) {
    compactAffectedDataFiles(); // removes positional deletes, then rerun
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

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/b1dd3910117979f1. Report an issue: GitHub.