This is an automated email from the ASF dual-hosted git repository.
danny0405 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git
The following commit(s) were added to refs/heads/master by this push:
new 5b8323f88891 fix(spark): resolve nested ordering fields for Spark
deletes written to delete blocks (#20368)
5b8323f88891 is described below
commit 5b8323f88891be103668ce098a0aafcf38e0b208
Author: Vova Kolmakov <[email protected]>
AuthorDate: Wed Oct 21 09:40:41 2026 +0700
fix(spark): resolve nested ordering fields for Spark deletes written to
delete blocks (#20368)
* fix(spark): resolve nested ordering fields for Spark deletes written to
delete blocks
* addressed review comments: drop the repeated schema and path lookups in
getOrderingValueAsJava, rename the test
* addressed review comments: add a NestedFieldPath overload of
getFieldValueFromInternalRowInternal
---------
Co-authored-by: Vova Kolmakov <[email protected]>
---
.../hudi/common/model/HoodieSparkRecord.java | 8 ++++--
.../hudi/BaseSparkInternalRecordContext.java | 13 +++++++--
.../apache/hudi/functional/TestMORDataSource.scala | 32 ++++++++++++++++++++++
3 files changed, 48 insertions(+), 5 deletions(-)
diff --git
a/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/common/model/HoodieSparkRecord.java
b/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/common/model/HoodieSparkRecord.java
index de30e4ef94a6..3443d9411cbf 100644
---
a/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/common/model/HoodieSparkRecord.java
+++
b/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/common/model/HoodieSparkRecord.java
@@ -389,11 +389,15 @@ public class HoodieSparkRecord extends
HoodieRecord<InternalRow> {
if (orderingFields == null) {
return OrderingValues.getDefault();
} else {
+ StructType structType =
HoodieInternalRowUtils.getCachedSchema(recordSchema);
return OrderingValues.create(orderingFields, field -> {
- if (recordSchema.getField(field).isEmpty()) {
+ // Ordering fields can be nested, so resolve them by path as
doGetOrderingValue does.
+ scala.Option<NestedFieldPath> cachedNestedFieldPath =
+ HoodieInternalRowUtils.getCachedPosList(structType, field);
+ if (cachedNestedFieldPath.isEmpty()) {
return OrderingValues.getDefault();
}
- return (Comparable<?>) getColumnValueAsJava(recordSchema, field,
props);
+ return (Comparable<?>) getFieldValueFromInternalRowAsJava(data,
cachedNestedFieldPath.get());
});
}
}
diff --git
a/hudi-client/hudi-spark-client/src/main/scala/org/apache/hudi/BaseSparkInternalRecordContext.java
b/hudi-client/hudi-spark-client/src/main/scala/org/apache/hudi/BaseSparkInternalRecordContext.java
index ef29b0849682..0fdc4dddbb36 100644
---
a/hudi-client/hudi-spark-client/src/main/scala/org/apache/hudi/BaseSparkInternalRecordContext.java
+++
b/hudi-client/hudi-spark-client/src/main/scala/org/apache/hudi/BaseSparkInternalRecordContext.java
@@ -76,6 +76,10 @@ public abstract class BaseSparkInternalRecordContext extends
RecordContext<Inter
return getFieldValueFromInternalRowInternal(row, recordSchema, fieldName,
true);
}
+ public static Object getFieldValueFromInternalRowAsJava(InternalRow row,
HoodieUnsafeRowUtils.NestedFieldPath nestedFieldPath) {
+ return getFieldValueFromInternalRowInternal(row, nestedFieldPath, true);
+ }
+
private static Object getFieldValueFromInternalRowInternal(InternalRow row,
HoodieSchema recordSchema, String fieldName, boolean convertToJavaType) {
return getFieldValueFromInternalRowInternal(row,
getCachedSchema(recordSchema), fieldName, convertToJavaType);
}
@@ -84,14 +88,17 @@ public abstract class BaseSparkInternalRecordContext
extends RecordContext<Inter
scala.Option<HoodieUnsafeRowUtils.NestedFieldPath> cachedNestedFieldPath =
HoodieInternalRowUtils.getCachedPosList(structType, fieldName);
if (cachedNestedFieldPath.isDefined()) {
- HoodieUnsafeRowUtils.NestedFieldPath nestedFieldPath =
cachedNestedFieldPath.get();
- Object value = HoodieUnsafeRowUtils.getNestedInternalRowValue(row,
nestedFieldPath);
- return convertToJavaType ? sparkTypeToJavaType(value) : value;
+ return getFieldValueFromInternalRowInternal(row,
cachedNestedFieldPath.get(), convertToJavaType);
} else {
return null;
}
}
+ private static Object getFieldValueFromInternalRowInternal(InternalRow row,
HoodieUnsafeRowUtils.NestedFieldPath nestedFieldPath, boolean
convertToJavaType) {
+ Object value = HoodieUnsafeRowUtils.getNestedInternalRowValue(row,
nestedFieldPath);
+ return convertToJavaType ? sparkTypeToJavaType(value) : value;
+ }
+
private static Object sparkTypeToJavaType(Object value) {
if (value == null) {
return null;
diff --git
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestMORDataSource.scala
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestMORDataSource.scala
index 363cf5a3a4c6..f2db2ac68753 100644
---
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestMORDataSource.scala
+++
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestMORDataSource.scala
@@ -2627,6 +2627,38 @@ class TestMORDataSource extends
HoodieSparkClientTestBase with SparkDatasetMixin
def testNestedFieldPartition(): Unit = {
TestCOWDataSource.runNestedFieldPartitionTest(spark, basePath, storage,
"MOR")
}
+
+ @ParameterizedTest
+ @CsvSource(value = Array("AVRO;bigint;8", "SPARK;bigint;9",
"SPARK;string;8", "SPARK;decimal(10,2);9"), delimiter = ';')
+ def testDeleteWithNestedOrderingFieldRespectsEventTimeOrdering(recordType:
HoodieRecordType, orderingType: String, tableVersion: Int): 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 -> "meta.ts",
+ HoodieWriteConfig.RECORD_MERGE_MODE.key ->
RecordMergeMode.EVENT_TIME_ORDERING.name(),
+ HoodieWriteConfig.WRITE_TABLE_VERSION.key -> tableVersion.toString,
+ HoodieTableConfig.VERSION.key -> tableVersion.toString,
+ HoodieCompactionConfig.INLINE_COMPACT.key -> "false")
+
+ def write(rows: Seq[(String, String, Boolean)], mode: SaveMode): Unit = {
+ val localSpark = spark
+ import localSpark.implicits._
+ rows.toDF("id", "ts", "_hoodie_is_deleted")
+ .select(col("id"),
struct(col("ts").cast(orderingType).as("ts")).as("meta"),
col("_hoodie_is_deleted"))
+
.coalesce(1).write.format("hudi").options(opts).mode(mode).save(basePath)
+ }
+
+ write(Seq(("id1", "100", false), ("id2", "100", false)),
SaveMode.Overwrite)
+ // The delete of id1 is older than the stored row and must not remove it;
the delete of id2 is newer.
+ // "050" has a leading zero so that it also sorts before "100" as a string.
+ write(Seq(("id1", "050", true), ("id2", "150", true)), SaveMode.Append)
+
+ val rows =
spark.read.format("hudi").options(readOpts).load(basePath).select("id",
"meta.ts").collect()
+ assertEquals(Seq("id1"), rows.map(_.getString(0)).toSeq)
+ assertEquals(100, BigDecimal(rows.head.get(1).toString).toInt)
+ }
}
object TestMORDataSource {