apache/iceberg · error · UnsupportedOperationException

does not implement markRowDeleted

Error message

 does not implement markRowDeleted

What it means

DeleteFilter.markRowDeleted is an extension hook that subclasses override to mark a record as deleted when applying equality deletes in-place. The base implementation throws UnsupportedOperationException, so calling it on a DeleteFilter subclass that did not override it (or a caller invoking the base method directly) fails.

Source

Thrown at data/src/main/java/org/apache/iceberg/data/DeleteFilter.java:245

  public CloseableIterable<T> findEqualityDeleteRows(CloseableIterable<T> records) {
    // Predicate to test whether a row has been deleted by equality deletions.
    Predicate<T> deletedRows = applyEqDeletes().stream().reduce(Predicate::or).orElse(t -> false);

    return CloseableIterable.filter(records, deletedRows);
  }

  private CloseableIterable<T> applyEqDeletes(CloseableIterable<T> records) {
    if (eqDeletes.isEmpty()) {
      return records;
    }

    Predicate<T> isEqDeleted = applyEqDeletes().stream().reduce(Predicate::or).orElse(t -> false);

    return createDeleteIterable(records, isEqDeleted);
  }

  protected void markRowDeleted(T item) {
    throw new UnsupportedOperationException(
        this.getClass().getName() + " does not implement markRowDeleted");
  }

  public Predicate<T> eqDeletedRowFilter() {
    if (eqDeleteRows == null) {
      eqDeleteRows =
          applyEqDeletes().stream().map(Predicate::negate).reduce(Predicate::and).orElse(t -> true);
    }
    return eqDeleteRows;
  }

  public PositionDeleteIndex deletedRowPositions() {
    if (deleteRowPositions == null && !posDeletes.isEmpty()) {
      this.deleteRowPositions = deleteLoader().loadPositionDeletes(posDeletes, filePath);
    }

    return deleteRowPositions;
  }

View on GitHub (pinned to 86d9c8fc54)

Solutions

  1. Override markRowDeleted in your DeleteFilter subclass to implement the row-mutation semantics.
  2. Use eqDeletedRowFilter()/applyEqDeletes() (predicate-based filtering) instead of the mutation path if you don't mutate rows in place.
  3. Check which code path is calling markRowDeleted and switch to one your subclass supports.
  4. Fix subclass inheritance so the concrete filter used at runtime actually implements the hook.

Example fix

// before
public class MyFilter extends DeleteFilter<Record> { /* no markRowDeleted */ }

// after
@Override
protected void markRowDeleted(Record item) {
  item.setField("_deleted", true);
}
Defensive patterns

Strategy: validation

Validate before calling

if (filter.getClass().getMethod("markRowDeleted")
        .getDeclaringClass() == DeleteFilter.class) {
  throw new IllegalStateException("Filter does not support in-place row deletion");
}

Type guard

boolean supportsMarkRowDeleted(DeleteFilter<?> f) {
  try { f.getClass().getDeclaredMethod("markRowDeleted", Object.class); return true; }
  catch (NoSuchMethodException e) { return false; }
}

Try / catch

try {
  filter.markRowDeleted(item);
} catch (UnsupportedOperationException e) {
  // fall back to predicate-based filtering
  Predicate<T> isDeleted = filter.eqDeletedRowFilter();
}

Prevention

When it happens

Trigger: Calling markRowDeleted on a DeleteFilter instance whose concrete class does not override the method — e.g. a custom filter subclass that only implemented applyEqDeletes/eqDeletedRowFilter but uses a code path (like applyEqDeletes in mutation mode) that relies on markRowDeleted.

Common situations: Custom engine integrations extending DeleteFilter incompletely; refactors that changed which hook is used (filter-based vs mutation-based equality delete application); invoking protected hooks reflectively or from new code paths.

Related errors


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