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
- Convert the changelog stream to retract semantics with toRetractStream so only INSERT and DELETE kinds reach the sink.
- For CDC sources, drop UPDATE_BEFORE and map UPDATE_AFTER to INSERT (e.g. via a map that sets RowKind.INSERT).
- 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
- Use statementset/toRetractStream so only INSERT and DELETE rows reach the sink
- For CDC, collapse UPDATE_BEFORE/UPDATE_AFTER pairs before the sink
- Add a validation operator before the Iceberg sink in custom pipelines
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
- Unknown row kind:
- Unknown row kind:
- Unknown row kind:
- Failed to load iceberg table from table loader:
- Unrecognized :
AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12).
Data as JSON: /api/errors/0bc99051ba39af51.
Report an issue: GitHub.