apache/iceberg · error · UnsupportedOperationException

Update must be represented as delete and insert

Error message

Update must be represented as delete and insert

What it means

Even in the file-scoped (rewritable deletes) delta writer, updates cannot be applied in place; Iceberg's Morpheus-style position delta protocol represents an update strictly as a delete plus an insert. The writer throws UnsupportedOperationException to enforce this contract.

Source

Thrown at spark/v3.5/spark/src/main/java/org/apache/iceberg/spark/source/SparkPositionDeltaWrite.java:744

  }

  private static class UnpartitionedDeltaWriter extends DeleteAndDataDeltaWriter {
    private final PartitionSpec dataSpec;

    UnpartitionedDeltaWriter(
        Table table,
        Map<String, DeleteFileSet> rewritableDeletes,
        SparkFileWriterFactory writerFactory,
        OutputFileFactory dataFileFactory,
        OutputFileFactory deleteFileFactory,
        Context context) {
      super(table, rewritableDeletes, writerFactory, dataFileFactory, deleteFileFactory, context);
      this.dataSpec = table.spec();
    }

    @Override
    public void update(InternalRow meta, InternalRow id, InternalRow row) throws IOException {
      throw new UnsupportedOperationException("Update must be represented as delete and insert");
    }

    @Override
    public void insert(InternalRow row) throws IOException {
      delegate.insert(row, dataSpec, null);
    }
  }

  private static class PartitionedDeltaWriter extends DeleteAndDataDeltaWriter {
    private final PartitionSpec dataSpec;
    private final PartitionKey dataPartitionKey;
    private final InternalRowWrapper internalRowDataWrapper;

    PartitionedDeltaWriter(
        Table table,
        Map<String, DeleteFileSet> rewritableDeletes,
        SparkFileWriterFactory writerFactory,
        OutputFileFactory dataFileFactory,

View on GitHub (pinned to 86d9c8fc54)

Solutions

  1. Configure the write so updates are decomposed into delete+insert (this is the required representation).
  2. Set the table property write.update.mode=copy-on-write (or merge mode) so the engine rewrites whole files for updates.
  3. Check the Spark/Iceberg version pairing: older integrations did not lower updates for this writer; upgrade if updates must flow through.

Example fix

// before
MERGE INTO t ... WHEN MATCHED THEN UPDATE SET ... // direct update op
// after
ALTER TABLE t SET TBLPROPERTIES ('write.update.mode'='copy-on-write')
Defensive patterns

Strategy: validation

Validate before calling

// verify update lowering before writing
String updateMode = table.properties().getOrDefault("write.update.mode", "merge-on-read");
if (!"copy-on-write".equals(updateMode)) { planUpdatesAsDeleteAndInsert(); }

Try / catch

try { writer.update(meta, id, row); } catch (UnsupportedOperationException e) { writer.delete(meta, id); writer.insert(row); }

Prevention

When it happens

Trigger: A MERGE/UPDATE operation emitting update rows (meta, id, row) to V2FileWriter/PositionDeltaWrite path where update ops reach this writer instead of separate delete+insert ops.

Common situations: Running UPDATE or MERGE WHEN MATCHED UPDATE on a v2 table using merge-on-read with the position delta writer, without the engine lowering updates into delete+insert pairs.

Related errors


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