voonhous opened a new issue, #19556:
URL: https://github.com/apache/hudi/issues/19556

   **Describe the problem you faced**
   
   On a COW table with a VARIANT column whose parquet files carry the shredded 
layout,
   inline clustering rewrites carried-over rows with `v = NULL`, silently 
losing the variant
   values. The replacecommit completes cleanly and no error is raised; the 
nulls are physical
   in the new base files, so once the cleaner removes the pre-clustering files 
the data is
   unrecoverable.
   
   Only rows that go through the clustering rewrite lose data. Rows whose file 
group is not
   in the clustering plan keep their values, which is what proves the write 
side (not the
   reader) dropped them.
   
   Shredding is enabled by default 
(`hoodie.parquet.variant.write.shredding.enabled=true`).
   A control experiment with the identical scenario and shredding disabled 
passes, so the
   loss is specific to reading the shredded layout inside the clustering 
rewrite, not to
   variant clustering in general.
   
   **To Reproduce**
   
   Spark SQL on master under the spark4.1 profile (the shredding schema is 
forced via the
   test-only config to deterministically produce shredded files):
   
   ```sql
   create table t (id int, v variant, ts long) using hudi
    location '/tmp/variant_cluster_repro'
    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.clustering.inline = 'true',
     hoodie.clustering.inline.max.commits = '2'
    );
   
   insert into t values (1, parse_json('{"key":"value1"}'), 1000),
                        (2, parse_json('{"key":"value2"}'), 1000);
   -- second commit trips inline clustering
   insert into t values (3, parse_json('{"key":"value3"}'), 1000),
                        (4, parse_json('{"key":"value4"}'), 1000);
   
   select id, cast(v as string), ts from t order by id;
   ```
   
   Observed (rows 1 and 2 went through the clustering rewrite; 3 and 4 did not):
   
   ```
   Expected: [1,{"key":"value1"},1000], [2,{"key":"value2"},1000], 
[3,{"key":"value3"},1000], [4,{"key":"value4"},1000]
   Got:      [1,null,1000],             [2,null,1000],             
[3,{"key":"value3"},1000], [4,{"key":"value4"},1000]
   ```
   
   Test-form repro: add the method below to
   `org.apache.spark.sql.hudi.dml.schema.TestVariantDataType` (it mirrors the 
SQL above and
   asserts a COMPLETED replacecommit exists so the failure cannot be a 
clustering no-op):
   
   ```scala
   test("Test COW clustering preserves VARIANT values") {
     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
       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.clustering.inline = 'true',
            |  hoodie.clustering.inline.max.commits = '2'
            | )
        """.stripMargin)
       spark.sql(s"insert into $tableName values " +
         "(1, parse_json('{\"key\":\"value1\"}'), 1000), " +
         "(2, parse_json('{\"key\":\"value2\"}'), 1000)")
       spark.sql(s"insert into $tableName values " +
         "(3, parse_json('{\"key\":\"value3\"}'), 1000), " +
         "(4, parse_json('{\"key\":\"value4\"}'), 1000)")
   
       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")
   
       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)
       )
     })
   }
   ```
   
   ```bash
   export JAVA_HOME=$(/usr/libexec/java_home -v 17)
   mvn test -Dspark4.1 -pl hudi-spark-datasource/hudi-spark -am \
     -Dtest=none -Dsurefire.failIfNoSpecifiedTests=false \
     -DwildcardSuites='org.apache.spark.sql.hudi.dml.schema.TestVariantDataType'
   ```
   
   The identical test with `hoodie.parquet.variant.write.shredding.enabled = 
'false'` (and no
   force schema) passes.
   
   **Expected behavior**
   
   Variant values survive clustering regardless of the physical layout of the 
input files.
   
   **Suspected cause (differentially verified, code path unverified)**
   
   A shredded parquet file stores the data in `typed_value`; the `value` field 
is physically
   null. User queries reconstruct that via Spark 4.1's `PushVariantIntoScan` 
catalyst rule.
   Clustering reads the file groups through
   `MultipleSparkJobExecutionStrategy.readRecordsForGroupAsRow`, i.e. the 
internal write-side
   reader stack (`SparkReaderContextFactory` -> 
`SparkFileFormatInternalRowReaderContext`),
   which does not go through the catalyst optimizer -- so nothing reconstructs 
`typed_value`,
   the read yields `{metadata, value=null}`, and the row writer persists the 
nulls into the
   clustered base files.
   
   This is the VARIANT twin of #19232 (compaction/clustering dropping INLINE 
blob bytes):
   same internal write-side reader path, same silent-loss shape. The fix likely 
mirrors it --
   engage shredded-variant reconstruction (or the variant projection overlay) 
on the internal
   reader context used by clustering.
   
   Notes on adjacent paths, from the same test suite run:
   
   * MOR compaction with shredded variants passes ("Test Query Log Only MOR 
Table With
     VARIANT column triggers compaction") -- compaction merges from log records 
via a
     different flow.
   * Unshredded clustering passes.
   
   **Environment Description**
   
   * Hudi version: master (1.3.0-SNAPSHOT, c4b38935db04)
   * Spark version: 4.1 profile (Scala 2.13, JDK 17)
   * Table type: COPY_ON_WRITE, parquet base files
   * Storage: local FS
   * Running on Docker: no
   


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