{"record":{"id":"c74d107112ab88e4","repo":"apache/iceberg","slug":"structinternalrow-is-read-only","errorCode":null,"errorMessage":"StructInternalRow is read-only","messagePattern":"StructInternalRow is read-only","errorType":"exception","errorClass":"UnsupportedOperationException","httpStatus":null,"severity":"error","filePath":"spark/v3.5/spark/src/main/java/org/apache/iceberg/spark/source/StructInternalRow.java","lineNumber":89,"sourceCode":"\n  private StructInternalRow(Types.StructType type, StructLike struct) {\n    this.type = type;\n    this.struct = struct;\n  }\n\n  public StructInternalRow setStruct(StructLike newStruct) {\n    this.struct = newStruct;\n    return this;\n  }\n\n  @Override\n  public int numFields() {\n    return struct.size();\n  }\n\n  @Override\n  public void setNullAt(int i) {\n    throw new UnsupportedOperationException(\"StructInternalRow is read-only\");\n  }\n\n  @Override\n  public void update(int i, Object value) {\n    throw new UnsupportedOperationException(\"StructInternalRow is read-only\");\n  }\n\n  @Override\n  public InternalRow copy() {\n    return this;\n  }\n\n  @Override\n  public boolean isNullAt(int ordinal) {\n    return struct.get(ordinal, Object.class) == null;\n  }\n\n  @Override","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/apache/iceberg/blob/86d9c8fc543e7c56c9f624eb725f76c9baff9570/spark/v3.5/spark/src/main/java/org/apache/iceberg/spark/source/StructInternalRow.java#L71-L107","documentation":"StructInternalRow is a read-only wrapper that adapts an Iceberg StructLike into a Spark InternalRow for reading. Spark occasionally calls mutation methods like setNullAt on InternalRow implementations, but this row is produced only for query output and never supports mutation, so any mutation attempt throws UnsupportedOperationException.","triggerScenarios":"Spark's InternalRow API contract calls setNullAt(i) on this row — e.g. during UnsafeRow conversion, aggregation buffer updates, or any operator that tries to write into the row instead of copying it first.","commonSituations":"Custom Spark expressions or UDFs that mutate input rows in place; third-party Spark extensions that assume writable rows; using StructInternalRow outside the read path (e.g. as a write buffer).","solutions":["Do not mutate StructInternalRow; call copy() (returns a mutable copy via Spark's InternalRow.copy path) before writing, or wrap in an UnsafeRow first","If writing custom Spark code, copy the row into your own buffer instead of calling setNullAt/update","If hit inside a connector/plugin, report it — the plugin must not mutate read-path rows"],"exampleFix":"// before\nrow.setNullAt(3);\n// after\nInternalRow mutable = row.copy(); // via GenericInternalRow conversion\nmutable.setNullAt(3);","handlingStrategy":"try-catch","validationCode":"// treat all scan-produced InternalRows as immutable\nif (row instanceof org.apache.iceberg.spark.source.StructInternalRow) {\n  throw new IllegalArgumentException(\"StructInternalRow is read-only; copy before mutating\");\n}","typeGuard":"boolean isReadOnlyRow(InternalRow r) { return r instanceof org.apache.iceberg.spark.source.StructInternalRow; }","tryCatchPattern":"try {\n  row.setNullAt(i);\n} catch (UnsupportedOperationException e) {\n  InternalRow copy = row.copy();\n  copy.setNullAt(i);\n}","preventionTips":["Never mutate rows returned from Iceberg scans","Call copy() when you need a writable InternalRow","Assume InternalRow from batch sources may be read-only"],"tags":["spark","internalrow","read-only","unsupported-operation"],"backgroundTag":"unsupported-operation","analyzedSha":"86d9c8fc543e7c56c9f624eb725f76c9baff9570","analyzedAt":"2026-09-12T00:46:39.097Z","contentChangedAt":"2026-09-12T00:46:39.097Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}