voonhous commented on code in PR #19558:
URL: https://github.com/apache/hudi/pull/19558#discussion_r3755654219
##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestVariantDataType.scala:
##########
@@ -250,9 +250,248 @@ class TestVariantDataType extends HoodieSparkSqlTestBase {
metaClient.reloadActiveTimeline()
assert(metaClient.getActiveTimeline.getCleanerTimeline.countInstants() >
0,
"Expected at least one .clean instant on the timeline after
compaction")
+
+ // Round 2: the compaction above compacted a log-only slice, so it never
read a
+ // parquet base file. Pin that the compacted base file is shredded, then
drive a
+ // second compaction that reads it through the internal reader
+ // (SparkFileFormatInternalRowReaderContext) and must carry rows 3 and
4, which
+ // exist only in that base file, forward (#19556).
+ val compactedFiles = listDataParquetFiles(tablePath)
+ assert(compactedFiles.nonEmpty, "Should have a compacted base parquet
file")
+ compactedFiles.foreach { filePath =>
+ val parquetSchema = readParquetSchema(filePath)
+ val variantGroup = getFieldAsGroup(parquetSchema, "v")
+ assert(variantGroup.containsField("typed_value"),
+ s"Compacted base file should carry typed_value.
Schema:\n$variantGroup")
+ }
+
+ // The v2-merged deltacommit above was the first after the compaction;
four more
+ // reach max.delta.commits = 5 and trip the second compaction inline.
+ spark.sql(s"""update $tableName set v = parse_json('{"key":"v1-r2"}'),
ts = 1003 where id = 1""")
+ spark.sql(s"""update $tableName set v = parse_json('{"key":"v2-r2"}'),
ts = 1004 where id = 2""")
+ spark.sql(s"""update $tableName set v = parse_json('{"key":"v1-r3"}'),
ts = 1005 where id = 1""")
+ spark.sql(s"""update $tableName set v = parse_json('{"key":"v2-r3"}'),
ts = 1006 where id = 2""")
+
+ metaClient.reloadActiveTimeline()
+
assertResult(2)(metaClient.getActiveTimeline.getCommitTimeline.filterCompletedInstants.countInstants)
+
+ checkAnswer(s"select id, cast(v as string), ts from $tableName order by
id")(
+ Seq(1, "{\"key\":\"v1-r3\"}", 1005),
+ Seq(2, "{\"key\":\"v2-r3\"}", 1006),
+ Seq(3, "{\"key\":\"value3\"}", 1000),
+ Seq(4, "{\"key\":\"value4\"}", 1000)
+ )
})
}
+ test("Test COW clustering preserves VARIANT values") {
+ // Same Spark 4.1 gate as the compaction test above: clustering reads the
shredded
+ // base files back through the native reader, which rejects the 3-field
shredded
+ // layout before SPARK-54410 (Spark 4.1+).
+ assume(HoodieSparkUtils.gteqSpark4_1, "Shredded variant base-file read
requires Spark 4.1 or higher")
+
+ withRecordType()(withTempDir { tmp =>
+ val tableName = generateTableName
+ val tablePath = tmp.getCanonicalPath
+ // Clustering rewrites ALL rows of the clustered file groups through the
internal
+ // write-side reader context (SparkReaderContextFactory ->
+ // SparkFileFormatInternalRowReaderContext), the stack whose blob
handling silently
+ // lost bytes in #19232. Nothing pinned its VARIANT behavior: this is
the first
+ // clustering coverage for the type. Shredding is forced so the rewrite
reads and
+ // rewrites the shredded layout, the default in production.
+ spark.sql(
+ s"""
+ |create table $tableName (
+ | id int,
+ | v variant,
+ | ts long
+ |) using hudi
+ | location '$tablePath'
+ | tblproperties (
+ | primaryKey = 'id',
+ | type = 'cow',
+ | preCombineField = 'ts',
+ | hoodie.parquet.variant.write.shredding.enabled = 'true',
+ | hoodie.parquet.variant.force.shredding.schema.for.test = 'key
string',
+ | hoodie.index.type = 'INMEMORY',
+ | hoodie.parquet.small.file.limit = '0',
+ | hoodie.clustering.inline = 'true',
+ | hoodie.clustering.inline.max.commits = '2'
+ | )
+ """.stripMargin)
+
+ // small.file.limit = 0 keeps the second insert in its own file group.
Otherwise the
+ // second commit bin-packs into the first file group and rewrites it
through the CoW
+ // small-file MERGE (a different internal read stack), conflating that
path's variant
+ // handling with the clustering rewrite this test isolates.
+ spark.sql(s"insert into $tableName values " +
+ "(1, parse_json('{\"key\":\"value1\"}'), 1000), " +
+ "(2, parse_json('{\"key\":\"value2\"}'), 1000)")
+
+ // The pre-clustering base file must actually carry the shredded layout;
without this
+ // check the test silently degrades into the unshredded twin below if
the forced
+ // shredding schema ever stops taking effect.
+ val preClusteringFiles = listDataParquetFiles(tablePath)
+ assert(preClusteringFiles.nonEmpty, "Should have at least one data
parquet file before clustering")
+ preClusteringFiles.foreach { filePath =>
+ val parquetSchema = readParquetSchema(filePath)
+ val variantGroup = getFieldAsGroup(parquetSchema, "v")
+ assert(variantGroup.containsField("typed_value"),
+ s"Pre-clustering base file should carry typed_value.
Schema:\n$variantGroup")
+ }
+
+ // Second commit trips inline clustering (max.commits = 2), which
rewrites the rows
+ // of the first commit too.
+ spark.sql(s"insert into $tableName values " +
+ "(3, parse_json('{\"key\":\"value3\"}'), 1000), " +
+ "(4, parse_json('{\"key\":\"value4\"}'), 1000)")
+
+ // getLastClusteringInstant filters by action only, so a
REQUESTED/INFLIGHT instant
+ // satisfies isPresent; isCompleted confirms the rewrite finished.
+ val metaClient = createMetaClient(spark, tablePath)
+ val lastClustering =
metaClient.getActiveTimeline.getLastClusteringInstant
+ assert(lastClustering.isPresent && lastClustering.get.isCompleted,
+ "A COMPLETED clustering (replacecommit) instant must exist after
inline clustering; " +
+ "without a completed rewrite the round-trip below proves nothing")
+
+ checkAnswer(s"select id, cast(v as string), ts from $tableName order by
id")(
+ Seq(1, "{\"key\":\"value1\"}", 1000),
+ Seq(2, "{\"key\":\"value2\"}", 1000),
+ Seq(3, "{\"key\":\"value3\"}", 1000),
+ Seq(4, "{\"key\":\"value4\"}", 1000)
+ )
+
+ // VARIANT must still surface as the native type after the clustering
rewrite.
+ val variantField = spark.table(tableName).schema.find(_.name == "v").get
+ assertResult("variant")(variantField.dataType.typeName)
+ })
+ }
+
+ test("Test COW clustering preserves unshredded VARIANT values") {
+ // Companion to the shredded clustering test above, with shredding
disabled. If this
+ // passes while the shredded one fails, the loss is specific to reading
the shredded
+ // layout inside the clustering rewrite, not to variant clustering in
general.
+ assume(HoodieSparkUtils.gteqSpark4_1, "Variant clustering read-back
requires Spark 4.1 or higher")
+
+ withRecordType()(withTempDir { tmp =>
+ val tableName = generateTableName
+ val tablePath = tmp.getCanonicalPath
+ spark.sql(
+ s"""
+ |create table $tableName (
+ | id int,
+ | v variant,
+ | ts long
+ |) using hudi
+ | location '$tablePath'
+ | tblproperties (
+ | primaryKey = 'id',
+ | type = 'cow',
+ | preCombineField = 'ts',
+ | hoodie.parquet.variant.write.shredding.enabled = 'false',
Review Comment:
Addressed.
--
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]