@@ -463,6 +463,86 @@ void testPartialUpdateAndDelete() throws Exception {
463463 checkEqual (actualLogRecords , expectedLogs , rowType );
464464 }
465465
466+ @ Test
467+ void testPartialUpdateFirstInsertNullsNonTargetColumns () throws Exception {
468+ initLogTabletAndKvTablet (DATA2_SCHEMA , new HashMap <>());
469+ RowType rowType = DATA2_SCHEMA .getRowType ();
470+ KvRecordTestUtils .KvRecordFactory data2kvRecordFactory =
471+ KvRecordTestUtils .KvRecordFactory .of (rowType );
472+
473+ // Bug reproduction: partial update with targetColumns={0,1} (a, b) but the row
474+ // contains non-null values for ALL columns including non-target column c.
475+ // On first insert (no existing row), non-target columns should be set to null.
476+ KvRecordBatch kvRecordBatch =
477+ kvRecordBatchFactory .ofRecords (
478+ data2kvRecordFactory .ofRecord (
479+ "k1" .getBytes (), new Object [] {1 , "v1" , "should_be_null" }));
480+
481+ int [] targetColumns = new int [] {0 , 1 };
482+ kvTablet .putAsLeader (kvRecordBatch , targetColumns );
483+
484+ // The stored value should have column c (index 2) set to null,
485+ // NOT "should_be_null", because c is not in targetColumns.
486+ assertThat (kvTablet .getKvPreWriteBuffer ().get (Key .of ("k1" .getBytes ())))
487+ .isEqualTo (valueOf (compactedRow (rowType , new Object [] {1 , "v1" , null })));
488+
489+ // Also verify CDC log emits the correct row with null for non-target column
490+ LogRecords actualLogRecords = readLogRecords ();
491+ List <MemoryLogRecords > expectedLogs =
492+ Collections .singletonList (
493+ logRecords (
494+ rowType ,
495+ 0 ,
496+ Collections .singletonList (ChangeType .INSERT ),
497+ Collections .singletonList (new Object [] {1 , "v1" , null })));
498+ checkEqual (actualLogRecords , expectedLogs , rowType );
499+ }
500+
501+ @ Test
502+ void testPartialUpdateFirstInsertThenUpdate () throws Exception {
503+ initLogTabletAndKvTablet (DATA2_SCHEMA , new HashMap <>());
504+ RowType rowType = DATA2_SCHEMA .getRowType ();
505+ KvRecordTestUtils .KvRecordFactory data2kvRecordFactory =
506+ KvRecordTestUtils .KvRecordFactory .of (rowType );
507+
508+ // First insert: partial update columns a and b, column c should be null
509+ KvRecordBatch batch1 =
510+ kvRecordBatchFactory .ofRecords (
511+ data2kvRecordFactory .ofRecord (
512+ "k1" .getBytes (), new Object [] {1 , "v1" , "ignored" }));
513+ kvTablet .putAsLeader (batch1 , new int [] {0 , 1 });
514+ long endOffset = logTablet .localLogEndOffset ();
515+
516+ // Verify first insert stored correctly with null for non-target column
517+ assertThat (kvTablet .getKvPreWriteBuffer ().get (Key .of ("k1" .getBytes ())))
518+ .isEqualTo (valueOf (compactedRow (rowType , new Object [] {1 , "v1" , null })));
519+
520+ // Second update: partial update columns a and c, column b should retain "v1"
521+ KvRecordBatch batch2 =
522+ kvRecordBatchFactory .ofRecords (
523+ data2kvRecordFactory .ofRecord (
524+ "k1" .getBytes (), new Object [] {1 , "ignored2" , "c1" }));
525+ kvTablet .putAsLeader (batch2 , new int [] {0 , 2 });
526+
527+ // Verify: b should retain "v1" from first insert, c should be updated to "c1"
528+ assertThat (kvTablet .getKvPreWriteBuffer ().get (Key .of ("k1" .getBytes ())))
529+ .isEqualTo (valueOf (compactedRow (rowType , new Object [] {1 , "v1" , "c1" })));
530+
531+ // Verify CDC log for the second update
532+ LogRecords actualLogRecords = readLogRecords (endOffset );
533+ List <MemoryLogRecords > expectedLogs =
534+ Collections .singletonList (
535+ logRecords (
536+ rowType ,
537+ endOffset ,
538+ Arrays .asList (
539+ ChangeType .UPDATE_BEFORE , ChangeType .UPDATE_AFTER ),
540+ Arrays .asList (
541+ new Object [] {1 , "v1" , null },
542+ new Object [] {1 , "v1" , "c1" })));
543+ checkEqual (actualLogRecords , expectedLogs , rowType );
544+ }
545+
466546 @ Test
467547 void testPutWithMultiThread () throws Exception {
468548 initLogTabletAndKvTablet (DATA1_SCHEMA_PK , new HashMap <>());
0 commit comments