apache/iceberg · error · UnsupportedOperationException

Unknown row kind:

Error message

Unknown row kind: 

What it means

BaseDeltaTaskWriter.write() dispatches RowData by RowKind: INSERT/UPDATE_AFTER go to the data writer, DELETE/UPDATE_BEFORE go to the delete writer. Any other RowKind cannot be written to Iceberg and triggers UnsupportedOperationException naming the kind.

Source

Thrown at flink/v2.3/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. Inspect the record's RowKind in the upstream operator and ensure it's set to one of INSERT, UPDATE_BEFORE, UPDATE_AFTER, DELETE
  2. Fix the producer emitting records with an invalid RowKind byte (custom serialization/deserialization bugs)
  3. If intentionally dropping rows, filter them before the sink instead of sending unknown kinds
  4. Validate changelog mode of the upstream: rows must come from a to-changelog stream with proper kinds

Example fix

// before
generic.setRowKind(RowKind.fromByteValue((byte) 9)); // unknown kind -> UnsupportedOperationException
// after
generic.setRowKind(RowKind.INSERT);
Defensive patterns

Strategy: validation

Validate before calling

// before the sink
DataStream<RowData> validated = stream.filter(r ->
    r.getRowKind() == RowKind.INSERT || r.getRowKind() == RowKind.UPDATE_AFTER ||
    r.getRowKind() == RowKind.UPDATE_BEFORE || r.getRowKind() == RowKind.DELETE);

Type guard

static boolean isWritableRowKind(RowData row) {
  switch (row.getRowKind()) {
    case INSERT:
    case UPDATE_AFTER:
    case UPDATE_BEFORE:
    case DELETE:
      return true;
    default:
      return false;
  }
}

Try / catch

try {
  writer.write(row);
} catch (UnsupportedOperationException e) {
  LOG.error("Row with unsupported RowKind {} reached the Iceberg sink", row.getRowKind(), e);
  throw e;
}

Prevention

When it happens

Trigger: Writing a RowData whose getRowKind() is not INSERT, UPDATE_AFTER, DELETE, or UPDATE_BEFORE — only possible with RowKind not produced by normal Flink changelog semantics, or when upstream code constructs RowData with a custom/invalid kind byte.

Common situations: Custom upstream operators building RowData and setting a wrong/unknown RowKind byte; corruption when translating records from another source; hand-crafted test/generic records with kind 0 (unknown).

Related errors


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