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 9a45b5efed11 test(variant): pin rename under schema-on-read (#20142)
9a45b5efed11 is described below

commit 9a45b5efed114e4076db57e66adc3761e5334498
Author: voonhous <[email protected]>
AuthorDate: Wed Sep 30 12:22:56 2026 +0800

    test(variant): pin rename under schema-on-read (#20142)
    
    Item 5 of the PushVariantIntoScan checklist on #18285: rename the
    variant column with schema-on-read on, then read it with pushdown on.
    
    The read fails through the schema-on-read guard on the pushdown arm
    (validateNoShreddedVariants names the renamed column), on COW from the
    base-file reader and on MOR through the file group reader. With
    pushdown off the read dies in the generated UnsafeProjection with a
    ClassCastException from SpecificInternalRow to VariantVal, because the
    guard's shredded arm resolves footer columns by the query name and
    skips a renamed one, so the merged internal-schema request goes on as
    a struct where the plan expects a variant. Both arms are loud; no
    silent nulls. The DDL needs
    hoodie.datasource.write.schema.allow.auto.evolution.column.drop=true
    to get past #19766, which the test sets around the DDL only.
    
    Green on master as-is; no production change.
    
    Part of #18285.
---
 .../schema/TestVariantShreddingMixedLayouts.scala  | 78 ++++++++++++++++++++++
 1 file changed, 78 insertions(+)

diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestVariantShreddingMixedLayouts.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestVariantShreddingMixedLayouts.scala
index 984c97b38989..1be340407c56 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestVariantShreddingMixedLayouts.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestVariantShreddingMixedLayouts.scala
@@ -955,6 +955,84 @@ class TestVariantShreddingMixedLayouts extends 
HoodieSparkSqlTestBase with Varia
     }
   }
 
+  test("Rename of the variant column under schema-on-read fails fast on both 
pushVariantIntoScan arms") {
+    assume(HoodieSparkUtils.gteqSpark4_1, SPARK_4_1_GATE)
+
+    // A rename is the schema-on-read DDL whose file and query disagree on the 
variant's NAME: the
+    // footer keeps `v`, the internal schema maps it to `w` by field id. 
Nothing reconstructs a
+    // variant under schema-on-read until #18285, so what is pinned is how 
each read of the renamed
+    // column fails - loudly, never with silent nulls - and that reads without 
it still work. Every
+    // failure below, and what unblocks it, is tracked in #20141. The legs 
cover a shredded COW base,
+    // an unshredded one, and a shredded MOR base merged with a log block 
through the file group
+    // reader.
+    Seq(("cow", Forced("a bigint")), ("cow", Unshredded), ("mor", Forced("a 
bigint"))).foreach {
+      case (tableType, layout) =>
+      withVariantTable(s"rename under schema-on-read, $tableType, $layout", 
tableType,
+        recordTypes = Seq(HoodieRecordType.SPARK)) { (tableName, tablePath, 
leg) =>
+        withWriteLayout(layout) {
+          spark.sql(s"""insert into $tableName values (1, 
parse_json('{"a":1}'), 1000), (2, parse_json('{"a":2}'), 1000)""")
+          if (tableType == "mor") {
+            // An update puts a log block under the base file so the read 
merges through the file
+            // group reader.
+            spark.sql(s"""update $tableName set v = parse_json('{"a":22}') 
where id = 2""")
+          }
+        }
+        assertVariantLayout(tablePath, shredded = layout != Unshredded, leg)
+        if (tableType == "mor") {
+          assert(logBlockSchemas(tablePath).nonEmpty, s"[$leg] the update must 
have written a log block")
+        }
+
+        withSQLConf("hoodie.schema.on.read.enable" -> "true") {
+          val instantBefore = latestCompletedInstant(tablePath)
+          val filesBefore = listDataParquetFiles(tablePath)
+          // Under default configs the rename itself is refused (#19766, its 
own pin; a failed attempt
+          // leaves a REQUESTED instant behind). The column-drop knob 
short-circuits that check, so
+          // it is set around the DDL only.
+          
withSQLConf("hoodie.datasource.write.schema.allow.auto.evolution.column.drop" 
-> "true") {
+            spark.sql(s"alter table $tableName rename column v to w")
+          }
+          assert(latestCompletedInstant(tablePath) != instantBefore,
+            s"[$leg] the rename must commit on its own instant")
+          assert(listDataParquetFiles(tablePath) == filesBefore, s"[$leg] a 
rename rewrites no data file")
+          assert(variantGroupOf(filesBefore.head, 
"v").containsField("metadata"),
+            s"[$leg] the file keeps the old column name")
+          val columns = spark.sql(s"describe $tableName").collect()
+            .map(r => r.getString(0) -> r.getString(1)).toMap
+          assert(columns.get("w").contains("variant") && 
!columns.contains("v"),
+            s"[$leg] catalog columns: $columns")
+
+          // Pushdown on (the default): both full-variant-shape reads fail 
through the guard
+          // (ParquetSchemaEvolutionUtils.validateNoShreddedVariants). On COW 
it fires from the
+          // base-file reader, on MOR from 
SparkFileFormatInternalRowReaderContext through the file
+          // group reader; matching on `w` also pins that the guard resolved 
the renamed name.
+          checkNestedExceptionContains(() => spark.sql(s"select id, w from 
$tableName").collect())(
+            "Column 'w' is a variant requested in Spark's full-variant 
projection shape")
+          checkNestedExceptionContains(
+            () => spark.sql(s"select id, variant_get(w, '$$.a', 'int') from 
$tableName").collect())(
+            "Column 'w' is a variant requested in Spark's full-variant 
projection shape")
+
+          // Pushdown off: the guard's shredded-file arm skips a renamed 
column (it resolves footer
+          // columns by the query-schema name), so the read goes on and dies 
in the generated
+          // projection's variant cast. Loud, engine-internal, on every leg; 
see #20141, item 3.
+          withSQLConf("spark.sql.variant.pushVariantIntoScan" -> "false") {
+            checkNestedExceptionContains(
+              () => spark.sql(s"select id, cast(w as string) from 
$tableName").collect())(
+              "cannot be cast to class 
org.apache.spark.unsafe.types.VariantVal")
+          }
+
+          // A projection without the variant still reads correctly after the 
rename, on both arms.
+          checkAnswer(s"select id, ts from $tableName order by id")(Seq(1, 
1000), Seq(2, 1000))
+          withSQLConf("spark.sql.variant.pushVariantIntoScan" -> "false") {
+            checkAnswer(s"select id, ts from $tableName order by id")(Seq(1, 
1000), Seq(2, 1000))
+          }
+          // count(*) is deliberately not pinned: it fails on any 
schema-on-read variant table once
+          // an internal schema is committed, rename or not (#20139), and the 
existing pin in
+          // "Schema-on-read reads of shredded variant files fail fast" is 
green only through #20140.
+        }
+      }
+    }
+  }
+
   test("Inline compaction and clustering under schema-on-read fail fast on the 
variant column") {
     assume(HoodieSparkUtils.gteqSpark4_1, SPARK_4_1_GATE)
 

Reply via email to