linliu-code opened a new issue, #19708:
URL: https://github.com/apache/hudi/issues/19708

   On a Merge-on-Read table using a global bloom or global simple index, a 
`MERGE INTO` whose `UPDATE SET` does not assign the record key fails inside the 
writer:
   
   ```
   org.apache.hudi.exception.HoodieUpsertException: Failed to upsert for commit 
time ...
     at 
org.apache.hudi.table.action.commit.BaseWriteHelper.write(BaseWriteHelper.java:84)
     at 
org.apache.hudi.table.action.deltacommit.SparkUpsertDeltaCommitActionExecutor.execute(SparkUpsertDeltaCommitActionExecutor.java:45)
   Cause: org.apache.hudi.exception.HoodieKeyException:
     recordKey value: "null" for field: "id" cannot be null or empty
       at org.apache.hudi.keygen.KeyGenUtils.getRecordKey(KeyGenUtils.java:270)
       at 
org.apache.hudi.keygen.SimpleAvroKeyGenerator.getRecordKey(SimpleAvroKeyGenerator.java:50)
       at 
org.apache.spark.sql.hudi.command.SqlKeyGenerator.$anonfun$getPartitionPath$1(SqlKeyGenerator.scala:102)
   ```
   
   Not assigning the record key is the ordinary shape for a partial update, so 
this makes partial-update `MERGE INTO` unusable on that combination.
   
   **To reproduce**
   
   ```sql
   -- hoodie.index.type = GLOBAL_BLOOM
   -- hoodie.bloom.index.update.partition.path = false
   CREATE TABLE t (id BIGINT, name STRING, amount DOUBLE, ts BIGINT, dt STRING)
   USING hudi PARTITIONED BY (dt)
   TBLPROPERTIES (type = 'mor', primaryKey = 'id', preCombineField = 'ts');
   
   INSERT INTO t VALUES (1, 'a', 10.0, 1, '2026-08-11');
   
   MERGE INTO t AS t
   USING (SELECT 1L AS id, 15.0 AS amount, 200L AS ts, '2026-08-11' AS dt) AS s
   ON t.id = s.id
   WHEN MATCHED THEN UPDATE SET t.amount = s.amount, t.ts = s.ts;
   ```
   
   Expected: the row updates in place, with `name` and `dt` keeping their 
existing values.
   
   **Scope**
   
   | Configuration | Result |
   | --- | --- |
   | MOR, `GLOBAL_BLOOM` or `GLOBAL_SIMPLE` | fails |
   | MOR, `RECORD_INDEX` or `GLOBAL_RECORD_LEVEL_INDEX` | works |
   | Copy-on-Write, any of the above | works |
   | Same statement with the record key assigned | works |
   
   The index types split because `mayContainDuplicateLookup` is `tableType == 
MERGE_ON_READ` for global bloom and global simple, while the record-index 
implementations pass `false` and never reach the merge stage. Assigning the 
record key works because it makes `areAllFieldsUpdated` true, which turns 
partial updates off.
   
   **Cause**
   
   `SqlKeyGenerator#getPartitionPath(GenericRecord)` resolves the partition 
path by way of `BaseKeyGenerator#getKey`, which is `new 
HoodieKey(getRecordKey(record), getPartitionPath(record))`. Asking for a 
partition path therefore also validates the record key. On the path above, 
`HoodieIndexUtils#inferPartitionPath` asks for the partition path of a merged 
record materialised against `WRITE_PARTIAL_UPDATE_SCHEMA`, which carries only 
the columns named in `UPDATE SET`, so the record key is legitimately absent.
   
   **Note**
   
   Fixing this exposes a second, currently unreachable problem on the same 
path, where the partial-update merged record is serialized against a mismatched 
schema and raises `UnresolvedUnionException` from 
`BaseAvroPayload#getRecordBytes`. That has a distinct cause in the payload path 
and will be filed separately.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to