This is an automated email from the ASF dual-hosted git repository.

voonhous 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 a78be288c126 fix(spark): check data schema in Spark 4.0 guard (#20158)
a78be288c126 is described below

commit a78be288c126ae02b6af91b2ac8ed1eca3771a0c
Author: voonhous <[email protected]>
AuthorDate: Thu Oct 1 12:25:10 2026 +0800

    fix(spark): check data schema in Spark 4.0 guard (#20158)
    
    Spark 4.0 with spark.sql.variant.pushVariantIntoScan on is unsupported
    and the read is meant to fail with an error that names the conf
    (#20032, #20033). The guard looked at the required schema of the scan
    only. PushVariantIntoScan rewrites every variant column of the
    relation, into a placeholder struct<0:boolean> when the query never
    reads it, so a query that selects no variant slipped past the guard
    and died converting the data schema to a HoodieSchema:
    HoodieSchemaException "Illegal initial character: 0".
    
    Validate the data schema instead, which carries the rewrite for every
    scan of the table. The Spark 4.0 test expected that query to return
    rows, which it never did; it now expects the guard.
    
    CI builds no Spark 4.0 profile. Run locally on Spark 4.0.2: the test
    fails before the change and passes with it.
---
 .../scala/org/apache/spark/sql/hudi/SparkAdapter.scala | 18 ++++++++++--------
 .../parquet/HoodieFileGroupReaderBasedFileFormat.scala |  6 ++++--
 .../sql/hudi/dml/schema/TestVariantDataType.scala      |  9 ++++++---
 .../org/apache/spark/sql/adapter/Spark4_0Adapter.scala |  4 ++--
 4 files changed, 22 insertions(+), 15 deletions(-)

diff --git 
a/hudi-client/hudi-spark-client/src/main/scala/org/apache/spark/sql/hudi/SparkAdapter.scala
 
b/hudi-client/hudi-spark-client/src/main/scala/org/apache/spark/sql/hudi/SparkAdapter.scala
index 2b9725b52049..6949c6fbe11a 100644
--- 
a/hudi-client/hudi-spark-client/src/main/scala/org/apache/spark/sql/hudi/SparkAdapter.scala
+++ 
b/hudi-client/hudi-spark-client/src/main/scala/org/apache/spark/sql/hudi/SparkAdapter.scala
@@ -528,14 +528,16 @@ trait SparkAdapter extends Serializable {
   }
 
   /**
-   * Fails the read when `requiredSchema` carries a variant projection struct 
this Spark version
-   * cannot evaluate. Spark 4.0 rewrites a variant column exactly as 4.1 does 
once the conf is on,
-   * but its readers do not evaluate the projection and Hudi does not align 
log records to it, so
-   * a read there would fall through to the schema-change path instead of 
failing. The Spark 4.0
-   * adapter overrides this to throw; every other version reads the shape 
(4.1+) or never sees it
-   * (3.x). See https://github.com/apache/hudi/issues/20032.
-   */
-  def validateVariantProjectionReadable(requiredSchema: StructType): Unit = ()
+   * Fails the read when `schema`, the data schema of a scan, carries a 
variant projection struct
+   * this Spark version cannot evaluate. Spark 4.0 rewrites a variant column 
exactly as 4.1 does
+   * once the conf is on, but its readers do not evaluate the projection and 
Hudi does not align
+   * log records to it, so a read there would fall through to the 
schema-change path instead of
+   * failing. The data schema rather than the required one: the rule rewrites 
every variant column
+   * of the relation, requested or not. The Spark 4.0 adapter overrides this 
to throw; every other
+   * version reads the shape (4.1+) or never sees it (3.x). See
+   * https://github.com/apache/hudi/issues/20032.
+   */
+  def validateVariantProjectionReadable(schema: StructType): Unit = ()
 
   /**
    * If `sparkRequiredSchema` contains any Spark 4.1 variant projection struct 
(i.e., the
diff --git 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/HoodieFileGroupReaderBasedFileFormat.scala
 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/HoodieFileGroupReaderBasedFileFormat.scala
index 823fda9001f6..241bdff63178 100644
--- 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/HoodieFileGroupReaderBasedFileFormat.scala
+++ 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/HoodieFileGroupReaderBasedFileFormat.scala
@@ -292,8 +292,10 @@ class HoodieFileGroupReaderBasedFileFormat(tablePath: 
String,
                                               options: Map[String, String],
                                               hadoopConf: Configuration): 
PartitionedFile => Iterator[InternalRow] = {
     // Driver side, once per scan: Spark 4.0 cannot read a PushVariantIntoScan 
projection struct and
-    // has to fail here rather than in the schema-change path (#20032).
-    sparkAdapter.validateVariantProjectionReadable(requiredSchema)
+    // has to fail here rather than in the schema-change path (#20032). 
Checked on the data schema:
+    // the rule rewrites every variant column of the relation, into a 
placeholder struct when the
+    // query never reads it, so a scan that requires no variant still carries 
the rewrite there.
+    sparkAdapter.validateVariantProjectionReadable(dataStructType)
     val outputSchema = StructType(requiredSchema.fields ++ 
partitionSchema.fields)
     val isCount = requiredSchema.isEmpty && !isMOR && !isIncremental
     // Spark planner only adds the user-provided predicates (from `WHERE` 
clause or `.filter()`)
diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestVariantDataType.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestVariantDataType.scala
index c0b8ad06c3f9..6b0c51243347 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestVariantDataType.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestVariantDataType.scala
@@ -258,13 +258,16 @@ class TestVariantDataType extends HoodieSparkSqlTestBase 
with VariantShreddingTe
 
         withSQLConf("spark.sql.variant.pushVariantIntoScan" -> "true") {
           // An extraction and a whole-variant read are both rewritten into a 
projection struct.
+          // So is a query that never reads v: the rule rewrites every variant 
column of the
+          // relation, and one no expression asks for becomes a placeholder 
struct in the data
+          // schema of the scan. Without the guard that read dies on the 
placeholder's field name
+          // ("Illegal initial character: 0") instead of naming the conf.
           Seq(s"select id, variant_get(v, '$$.key', 'string') from $tableName",
-            s"select id, cast(v as string) from $tableName").foreach { sql =>
+            s"select id, cast(v as string) from $tableName",
+            s"select id, ts from $tableName").foreach { sql =>
             checkNestedExceptionContains(() => spark.sql(sql).collect())(
               "spark.sql.variant.pushVariantIntoScan")
           }
-          // A query that does not touch the variant column is not rewritten 
and still reads.
-          checkAnswer(s"select id, ts from $tableName")(Seq(1, 1000))
         }
         // Back on the default the same reads work.
         checkAnswer(s"select id, cast(v as string) from $tableName")(Seq(1, 
"{\"key\":\"v2\"}"))
diff --git 
a/hudi-spark-datasource/hudi-spark4.0.x/src/main/scala/org/apache/spark/sql/adapter/Spark4_0Adapter.scala
 
b/hudi-spark-datasource/hudi-spark4.0.x/src/main/scala/org/apache/spark/sql/adapter/Spark4_0Adapter.scala
index 14031abf21d6..09f89fcbd685 100644
--- 
a/hudi-spark-datasource/hudi-spark4.0.x/src/main/scala/org/apache/spark/sql/adapter/Spark4_0Adapter.scala
+++ 
b/hudi-spark-datasource/hudi-spark4.0.x/src/main/scala/org/apache/spark/sql/adapter/Spark4_0Adapter.scala
@@ -123,8 +123,8 @@ class Spark4_0Adapter extends BaseSpark4Adapter {
   // spark.sql.variant.pushVariantIntoScan is on (off by default there), but 
its readers do not
   // evaluate the projection and Hudi does not align log records to it, so 
fail here instead of
   // falling through to the schema-change path (#20032).
-  override def validateVariantProjectionReadable(requiredSchema: StructType): 
Unit = {
-    requiredSchema.fields.find(f => 
containsVariantProjection(f.dataType)).foreach { field =>
+  override def validateVariantProjectionReadable(schema: StructType): Unit = {
+    schema.fields.find(f => containsVariantProjection(f.dataType)).foreach { 
field =>
       throw new HoodieNotSupportedException(
         s"Column '${field.name}' was rewritten by 
spark.sql.variant.pushVariantIntoScan into a variant " +
           s"projection struct, which Hudi does not support on Spark 
${HoodieSparkUtils.getSparkVersion}. " +

Reply via email to