voonhous opened a new pull request, #19783:
URL: https://github.com/apache/hudi/pull/19783

   ### Change Logs
   
   Closes #19775. Part of #18937. Stacked on #19777 (the first commit is that 
PR's; review the second commit): the legs need its
   test scaffolds and its Avro-path nested write parity, and it merges first.
   
   Pins that a variant shredded below the top level reads correctly through 
Hudi's Spark-native internal read paths on Spark 4.1+,
   corrects the four comment blocks that deferred the nested case with the 
wrong mechanism, and fixes the one real defect the legs
   found: the file-group reader applied a PushVariantIntoScan projection to 
top-level fields only.
   
   **The defect (issue outcome B, shredding-independent).** Spark 4.1+'s 
`PushVariantIntoScan` rewrites a variant reached through
   a struct path (`cast(s.inner as string)`, `variant_get(s.inner, ...)`) into 
a projection struct nested inside the scan's required
   schema (`s: struct<inner: struct<"0": string>>`). 
`SparkFileFormatInternalRowReaderContext` overlaid and projected such structs at
   the top level only, so on a MOR table with logs the merged row still carried 
a raw `VariantVal` at `s.inner` while the plan read
   that memory as `s.inner.0`: JVM SIGBUS / `InternalError` / OOM or a silent 
null, on native parquet logs and avro blocks, both record
   types, shredded or not. Minimal repro: `create table t (id int, s 
struct<inner: variant>, ts long) using hudi tblproperties
   (type='mor', hoodie.index.type='INMEMORY', ...)`, one insert, `select id, 
cast(s.inner as string) from t`. COW was unaffected
   because the whole catalyst schema goes to Spark's parquet reader, which 
handles a projection struct at any depth; with
   `spark.sql.variant.pushVariantIntoScan=false` everything worked.
   
   Fix: `overlayVariantProjections` / `shouldProjectVariants` recurse into 
struct members, and `buildVariantProjector` (hoisted out of
   the identical `Spark4_1Adapter` / `Spark4_2Adapter` copies into 
`BaseSpark4Adapter`) rebuilds a projection struct below a struct
   path with `VariantGet` children, recreating enclosing structs 
null-preservingly and passing untouched subtrees through by
   reference. Struct paths only, mirroring `VariantInRelation.rewriteType`; 
arrays and maps stay native on both sides.
   
   **What was verified before writing anything** (all on Spark 4.1.1, the 
pinned version):
   
   - The parquet row reader reconstructs a shredded variant for a NATIVE 
`VariantType` request at any depth: top-level, struct member
     and array element. `ParquetReadSupport.clipParquetType` passes a 
`VariantType` leaf's group through unchanged (its
     `clipVariantSchema` is a no-op TODO), 
`ParquetToSparkSchemaConverter.convertField` carries the catalyst target type 
into struct
     members, list elements and map values, and `ParquetRowConverter` builds a 
`ParquetVariantConverter` for it. `PushVariantIntoScan`
     itself only rewrites struct paths; arrays and maps stay native even for 
user queries.
   - What does read a shredded group as `value=null` is a request in the 
physical `{metadata, value}` struct shape. The only
     producer of that shape is the schema-on-read internal-schema branch, which 
#19687 guards.
   - The full-variant projection rewrite 
(`SparkAdapter.buildFullVariantReadSchema`) is therefore a contract, not a 
workaround:
     with it disabled (patched class shadowed on the test classpath, class 
origin verified), `TestVariantDataType` is 20/20 and
     `TestVariantShreddingMixedLayouts` 21/22 -- the one red is the 
schema-on-read fail-fast leg, where the projection shape is what
     lets `validateNoShreddedVariants` name the route instead of a 
`ClassCastException` inside the read. The same holds at the #19558
     fix commit itself (rebuilt in a worktree): the #19556 clustering repro 
passes there with the rewrite disabled and the logged
     parquet requested schema keeps `typed_value`. With 
`HoodieMergeOnReadRDDV2.shouldRerouteVariantSplit` forced off, the legacy
     streaming shredded leg passes too.
   
   So this is issue #19775's outcome A: the rewrite stays top-level and the 
code is unchanged; the comments now say what the shape
   is for and why nested variants need none.
   
   **Tests** (`TestVariantShreddingMixedLayouts`, section F; every table has 
ONLY the nested variant so nothing top-level can mask the
   nested path -- a new `withNestedOnlyVariantTable` scaffold in 
`VariantShreddingTestSupport`):
   
   - MOR: nested-shredded base + nested-shredded log; snapshot merge with the 
vectorized reader on and off, read-optimized, the legacy
     RDD path (`hoodie.file.group.reader.enabled=false`) over a base + log 
slice and over the compacted log-free slice, compaction under
     the forced DDL (base re-shredded on both record types, the AVRO one 
through #19777's Avro-path parity) and under `Unshredded`
     (typed_value stripped at depth), clustering under the forced DDL for both 
`row.writer.enable` values.
   - CDC (`hudi_table_changes(..., 'cdc', 'earliest')`, both supplemental 
logging modes): insert and update images of `s.inner`
     across a layout flip.
   - `variant_get` projection and filter on `s.inner` across a typed file and a 
residual file, `pushVariantIntoScan` on and off: on
     COW, and on MOR over native parquet log files (SPARK record type) and 
table-version-9 avro data blocks (AVRO record type, pinned
     through the log block types). Red before the fix as a JVM abort 
(SIGSEGV/SIGBUS in an unsafe copy stub).
   - `TestBaseSpark4AdapterVariantMethods`: the recursive projector as a unit 
(nested extraction, sibling preserved, null struct
     preserved, no projection -> None).
   - `TestStreamingSource`'s legacy RDD leg (the only remaining 
`hoodie.file.group.reader.enabled=false` consumer; batch reads no
     longer consult that key) gains a nested-shredded column on every split 
plus a footer pin on the compacted base.
   - `array<variant>` shredded at the element through a declared 
`hoodie.write.schema` (the only way a Hudi write produces that layout;
     SPARK record type, since the AVRO record-type insert cannot take a 
write-schema override of a different shape): snapshot read with
     the vectorized reader on and off, `variant_get` on an element, a log 
update, compaction under the declared schema (element
     re-shredded) and without it (element unshredded).
   
   **Not done here, on purpose:** removing the projection rewrite and the 
legacy re-route now that both are shown to be contract rather
   than correctness. That is a behaviour change (it would put variant tables 
back on the skip-merging fast path) and belongs in its
   own PR if wanted.
   
   ### Impact
   
   Reads of a struct-nested variant through the Spark file-group reader with 
`pushVariantIntoScan` on (the default) no longer crash
   or return nulls on MOR tables with log files. No config changes.
   
   ### Risk level
   
   Low
   
   ### Documentation Update
   
   None
   
   ### Contributor's checklist
   
   - [x] Read through [contributor's 
guide](https://hudi.apache.org/contribute/how-to-contribute)
   - [x] Change Logs and Impact were stated clearly
   - [x] Adequate tests were added if applicable
   - [ ] CI passed
   


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