apache/iceberg · error · UnsupportedOperationException

Unknown row kind: <row.getRowKind()>

Error message

Unknown row kind: <row.getRowKind()>

What it means

BaseDeltaTaskWriter.write dispatches RowData by RowKind: INSERT goes to the upsert/insert writer, DELETE (and UPDATE_BEFORE in some paths) to the delete writer. Any other RowKind (only UPDATE_AFTER remains) reaches the default arm and throws UnsupportedOperationException. Flink change streams must be normalized to INSERT/DELETE before writing to Iceberg.

Source

Thrown at flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/BaseDeltaTaskWriter.java:109

        break;

      case UPDATE_BEFORE:
        if (upsert) {
          break; // UPDATE_BEFORE is not necessary for UPSERT, we do nothing to prevent delete one
          // row twice
        }
        writer.delete(row);
        break;
      case DELETE:
        if (upsert) {
          writer.deleteKey(keyProjection.wrap(row));
        } else {
          writer.delete(row);
        }
        break;

      default:
        throw new UnsupportedOperationException("Unknown row kind: " + row.getRowKind());
    }
  }

  protected class RowDataDeltaWriter extends BaseEqualityDeltaWriter {
    RowDataDeltaWriter(PartitionKey partition, PartitioningDVWriter<RowData> dvFileWriter) {
      super(partition, schema, deleteSchema, DeleteGranularity.FILE, dvFileWriter);
    }

    @Override
    protected StructLike asStructLike(RowData data) {
      return wrapper.wrap(data);
    }

    @Override
    protected StructLike asStructLikeKey(RowData data) {
      return keyWrapper.wrap(data);
    }
  }

View on GitHub (pinned to 86d9c8fc54)

Solutions

  1. Convert the changelog stream to retract semantics with toRetractStream so only INSERT and DELETE kinds reach the sink.
  2. For CDC sources, drop UPDATE_BEFORE and map UPDATE_AFTER to INSERT (e.g. via a map that sets RowKind.INSERT).
  3. Verify upstream operators are not emitting UPDATE_AFTER into the Iceberg sink.

Example fix

// before
stream.map(row -> row); // RowData with RowKind.UPDATE_AFTER reaches sink
// after
DataStream<RowData> normalized = stream.map(row -> {
  if (row.getRowKind() == RowKind.UPDATE_AFTER) { row.setRowKind(RowKind.INSERT); }
  return row;
});
Defensive patterns

Strategy: validation

Validate before calling

if (row.getRowKind() == RowKind.UPDATE_AFTER || row.getRowKind() == RowKind.UPDATE_BEFORE) { throw new IllegalArgumentException("Normalize changelog before Iceberg sink: " + row.getRowKind()); }

Type guard

boolean isSinkCompatible(RowData row) { RowKind k = row.getRowKind(); return k == RowKind.INSERT || k == RowKind.DELETE; }

Try / catch

try { writer.write(row); } catch (UnsupportedOperationException e) { if (e.getMessage().startsWith("Unknown row kind")) { LOG.error("Changelog not normalized; use toRetractStream", e); throw e; } }

Prevention

When it happens

Trigger: Writing a RowData with RowKind.UPDATE_AFTER (or any kind other than INSERT/DELETE/UPDATE_BEFORE handled upstream) into an Iceberg delta sink that expects a delete/insert-only changelog.

Common situations: Feeding an upsert changelog stream (from CDC sources like Debezium/Kafka with toRetractStream semantics kept as update kinds) directly into the Iceberg sink instead of retracting to INSERT/DELETE; custom pipeline emitting UPDATE_AFTER rows.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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