diff --git a/spark/v4.1/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMerge.java b/spark/v4.1/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMerge.java index 3f428963c412..aa80b4ce5123 100644 --- a/spark/v4.1/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMerge.java +++ b/spark/v4.1/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMerge.java @@ -3001,6 +3001,29 @@ public void testMergeToWapBranchWithTableBranchIdentifier() { }); } + @TestTemplate + public void testMergePreservesNullableStructWithRequiredChildren() { + // Regression test for https://github.com/apache/iceberg/issues/16246 + // A nullable struct with non-nullable children should not have its children + // silently nullified when a MERGE updates a different column. + createAndInitTable( + "id INT, status STRING, info STRUCT", + "{ \"id\": 1, \"status\": \"active\", \"info\": { \"type\": \"A\", \"attr\": \"val1\" } }"); + createOrReplaceView("source", "{ \"id\": 1, \"status\": \"inactive\" }"); + + sql( + "MERGE INTO %s t USING source s " + + "ON t.id == s.id " + + "WHEN MATCHED THEN " + + " UPDATE SET t.status = s.status", + commitTarget()); + + assertEquals( + "Struct children should be preserved after MERGE updates a different column", + ImmutableList.of(row(1, "inactive", row("A", "val1"))), + sql("SELECT * FROM %s", selectTarget())); + } + private void checkJoinAndFilterConditions(String query, String join, String icebergFilters) { // disable runtime filtering for easier validation withSQLConf(