wombatu-kun opened a new issue, #20372:
URL: https://github.com/apache/hudi/issues/20372

   ### Bug Description
   
   **What happened:**
   
   On master, MERGE_ON_READ tables at table version 10, the default, write 
deletes into native delete log files. With the Spark record type 
(`DefaultSparkRecordMerger`) and a string ordering field, a write that sends a 
delete to a log file fails with `HoodieException: Failed to write to Hudi`. The 
executor logs `java.lang.ClassCastException: class 
org.apache.hudi.Spark3HoodieUTF8String cannot be cast to class 
org.apache.spark.unsafe.types.UTF8String`.
   
   `HoodieNativeLogFormatWriter.appendDeleteRecord` builds the delete row from 
`record.getOrderingValue(...)`, which is the comparable form of the value. 
Spark wraps a string into `HoodieUTF8String` there, because `UTF8String` no 
longer supports `compareTo` (SPARK-46832). The wrapper goes into the 
`GenericInternalRow` unchanged, and the Spark Parquet writer fails when it 
reads the column with `getUTF8String`. The Spark record context builds these 
rows for the Spark record type since #19283 
(`HoodieSparkTable.getRecordContextForWrite`).
   
   Native log compaction with the Spark record type writes the deletes it reads 
back through the same method, again with a `HoodieUTF8String` from the Spark 
reader context, whoever wrote the deletes. There the exception is swallowed: 
`HoodieAppendHandle` only marks the record as failed 
(`hoodie.write.ignore.failed` defaults to true), the log compaction commits, 
and the delete is gone. A table written with the default Avro record type 
therefore loses its string-ordered deletes once a Spark-typed job log-compacts 
it.
   
   With several ordering fields, a null field fails the delete write too: the 
writer stores the default ordering value, an `Integer` 0, in that field's 
column instead of NULL, and a string column rejects it.
   
   **What you expected:**
   
   The delete row holds the values the engine stores, with NULL for a null 
ordering field. The write and the log compaction succeed, and later reads order 
the delete correctly against older and newer updates.
   
   **Steps to reproduce:**
   
   1. On master, create a MERGE_ON_READ table (table version 10 is the default) 
with 
`hoodie.write.record.merge.custom.implementation.classes=org.apache.hudi.DefaultSparkRecordMerger`,
 a string ordering field `ts` and `EVENT_TIME_ORDERING`.
   2. Insert `id1` and `id2`.
   3. Upsert `id1` with `_hoodie_is_deleted=true`. The write fails.
   
   For the log compaction case, write the same rows with the default Avro 
record type, run log compaction with `DefaultSparkRecordMerger`, then upsert 
`id1` with an ordering value older than the delete's: `id1` comes back.
   
   A test in `TestMORDataSource` reproduces both. On master, rows 
`SPARK;SPARK;string;ts` and `SPARK;SPARK;string;ts,seq` fail at the delete 
write with `Failed to write to Hudi`, and row `AVRO;SPARK;string;ts` fails 
after log compaction with `expected: <Map(id2 -> 120)> but was: <Map(id2 -> 
120, id1 -> 105)>`:
   
   ```scala
     @ParameterizedTest
     @CsvSource(value = Array(
       "AVRO;AVRO;string;ts", "SPARK;SPARK;string;ts", 
"SPARK;SPARK;decimal(10,2);ts", "SPARK;SPARK;string;ts,seq",
       "AVRO;SPARK;string;ts"), delimiter = ';')
     def testNativeDeleteLogWithEngineOrdering(recordType: HoodieRecordType, 
logCompactionRecordType: HoodieRecordType,
                                               orderingType: String, 
orderingFields: String): Unit = {
       val (writeOpts, readOpts) = getWriterReaderOpts(recordType)
       val opts = writeOpts ++ Map(
         TABLE_TYPE.key -> MOR_TABLE_TYPE_OPT_VAL,
         RECORDKEY_FIELD.key -> "id",
         PARTITIONPATH_FIELD.key -> "",
         HoodieTableConfig.ORDERING_FIELDS.key -> orderingFields,
         HoodieWriteConfig.RECORD_MERGE_MODE.key -> 
RecordMergeMode.EVENT_TIME_ORDERING.name(),
         // Table version 10 writes deletes to native delete log files.
         HoodieWriteConfig.WRITE_TABLE_VERSION.key -> 
HoodieTableVersion.TEN.versionCode().toString,
         HoodieCompactionConfig.INLINE_COMPACT.key -> "false",
         HoodieCompactionConfig.ENABLE_LOG_COMPACTION.key -> "true",
         HoodieCompactionConfig.LOG_COMPACTION_BLOCKS_THRESHOLD.key -> "1")
   
       def write(rows: Seq[(String, String, Boolean)], mode: SaveMode): Unit = {
         val localSpark = spark
         import localSpark.implicits._
         rows.toDF("id", "ts", "_hoodie_is_deleted")
           .withColumn("ts", col("ts").cast(orderingType))
           .withColumn("seq", when(col("_hoodie_is_deleted"), 
lit(null)).otherwise(lit("1")).cast(orderingType))
           
.coalesce(1).write.format("hudi").options(opts).mode(mode).save(basePath)
       }
   
       write(Seq(("id1", "100", false), ("id2", "100", false)), 
SaveMode.Overwrite)
       // The delete leaves seq null, so with two ordering fields its second 
value is missing.
       write(Seq(("id1", "110", true)), SaveMode.Append)
       write(Seq(("id2", "120", false)), SaveMode.Append)
   
       // Log compaction reads the delete back and writes it into a new native 
delete log. Its record type can differ
       // from the writers', e.g. when a Spark-typed job compacts deletes that 
an Avro-typed one wrote.
       val logCompactionOpts = if (logCompactionRecordType == 
HoodieRecordType.SPARK) opts ++ sparkOpts else opts
       val client = DataSourceUtils.createHoodieClient(spark.sparkContext, "", 
basePath, tableName, logCompactionOpts.asJava)
         .asInstanceOf[SparkRDDWriteClient[HoodieRecordPayload[Nothing]]]
       try {
         val logCompactionInstant = 
client.scheduleLogCompaction(Option.empty()).get()
         client.logCompact(logCompactionInstant, true)
       } finally {
         client.close()
       }
   
       def readOrdering(): Map[String, Int] = 
spark.read.format("hudi").options(readOpts).load(basePath)
         .select("id", "ts").collect().map(row => row.getString(0) -> 
BigDecimal(row.get(1).toString).toInt).toMap
   
       // The delete keeps its ordering value: an older update leaves id1 
deleted, a newer one brings it back.
       write(Seq(("id1", "105", false)), SaveMode.Append)
       assertEquals(Map("id2" -> 120), readOrdering())
       write(Seq(("id1", "115", false)), SaveMode.Append)
       assertEquals(Map("id1" -> 115, "id2" -> 120), readOrdering())
     }
   ```
   
   ### Environment
   
   **Hudi version:** master (table version 10 and native log files are not 
released yet)
   **Query engine:** Spark
   **Relevant configs:** 
`hoodie.write.record.merge.custom.implementation.classes=org.apache.hudi.DefaultSparkRecordMerger`,
 a string ordering field, table version 10
   
   ### Logs and Stack Trace
   
   The delete write in the reproducing test on master:
   
   ```
   ERROR org.apache.hudi.io.HoodieAppendHandle [] - Error writing record 
HoodieRecord{key=HoodieKey(recordKey=id1, partitionPath=), ...}
   java.lang.ClassCastException: class org.apache.hudi.Spark3HoodieUTF8String 
cannot be cast to class org.apache.spark.unsafe.types.UTF8String
        at 
org.apache.spark.sql.catalyst.expressions.GenericInternalRow.getUTF8String(rows.scala:165)
        at 
org.apache.hudi.io.storage.row.HoodieRowParquetWriteSupport.lambda$makeWriter$23(HoodieRowParquetWriteSupport.java:569)
        ...
        at 
org.apache.hudi.io.storage.HoodieSparkParquetWriter.writeRow(HoodieSparkParquetWriter.java:100)
        at 
org.apache.hudi.io.cdc.HoodieNativeLogFormatWriter.appendDeleteRecord(HoodieNativeLogFormatWriter.java:178)
        at 
org.apache.hudi.io.HoodieNativeLogAppendHandle.writeDelete(HoodieNativeLogAppendHandle.java:152)
        at 
org.apache.hudi.io.HoodieAppendHandle.writeRecord(HoodieAppendHandle.java:397)
   
   org.apache.hudi.exception.HoodieException: Failed to write to Hudi
        at org.apache.hudi.DefaultSource.createRelation(DefaultSource.scala:189)
   ```
   
   Log compaction of the Avro-written table logs the same `ClassCastException` 
from `HoodieNativeLogFormatWriter.appendDeleteRecord`, and then commits without 
the delete.
   


-- 
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