Skip to content

feat(variant): infer shredding schemas by default on Spark 4.1+ - #19808

Merged
voonhous merged 6 commits into
apache:masterfrom
voonhous:variant-shredding-inference-default-on
Sep 2, 2026
Merged

feat(variant): infer shredding schemas by default on Spark 4.1+#19808
voonhous merged 6 commits into
apache:masterfrom
voonhous:variant-shredding-inference-default-on

Conversation

@voonhous

@voonhous voonhous commented Sep 1, 2026

Copy link
Copy Markdown
Member

Breaking change. Spark 4.1+ writers now shred variant columns by default, and only Spark 4.1+ can read the result: Spark 4.0, Spark 3.x, Hive and Flink readers fail fast on such tables. Opt out with hoodie.parquet.variant.shredding.schema.inference.enabled=false (writer option or tblproperty); clustering with it off rewrites shredded files back. Existing files are untouched.

Describe the issue this Pull Request addresses

Closes #19690 (last step after #18961, #19687, #19777, #19783). Spark 4.1 itself defaults spark.sql.variant.inferShreddingSchema and spark.sql.variant.writeShredding.enabled to true; Hudi on Spark 4.1 was the one parquet writer in that stack still writing unshredded variants.

Summary and Changelog

  • hoodie.parquet.variant.shredding.schema.inference.enabled defaults to true; its doc now carries the reader matrix, the opt-out and the un-shred recipe.
  • Only writers with a Spark 4.1+ inferrer on the classpath change. Spark 3.x, Spark 4.0, Flink and Java keep writing unshredded; TestHoodieAvroFileWriterFactoryVariantInference pins that with a stock config.
  • TestVariantSchemaUtils pins the default; its disabled legs set the flag off explicitly.
  • No Spark assertion moved: every unshredded-layout pin already disables shredding or inference. Sweep green on spark4.1 and spark4.2 (TestVariantDataType, TestVariantShreddingMixedLayouts, TestStreamingSource variant legs).
  • Spark 3.x reads a variant column by declaring it as struct<value: binary, metadata: binary>; that path projected a shredded group by name and returned a null value per shredded row. ParquetSchemaEvolutionUtils.validateNoShreddedVariantStructs now rejects it, called from Spark33/34/35ParquetReader and the Spark 3 legacy file format, with unit tests in TestParquetSchemaEvolutionUtils.

Impact

New base files and native parquet log files (table version 10+) from Spark 4.1+ writers carry typed_value for top-level variant columns. Commit schema unchanged; mixed layouts supported (#19687).

Risk Level

medium: the default changes on-disk layout and cross-engine readability. Other engines fail with an explicit error, never dropped payloads; the opt-out is one config.

Documentation Update

Config doc in this PR; the variant page on asf-site in a separate PR.

More detail: mechanics, verification, cross-engine notes, release note

Mechanics

  • Write paths affected on Spark 4.1+: SQL DML, the DataSource writer, the streamer, compaction and clustering. Inference engages only when VariantShreddingRuntime.lookupInferrer() finds a Spark 4.1+/4.2 inferrer on the classpath; Flink's HoodieRowDataFileWriterFactory never consults the flag, and the Avro factory (Flink AVRO merger, Java client) declines when no Spark provider or inferrer is present.
  • Inference stays top-level: a variant nested in a struct, array or map is unaffected (fix(variant): shred nested variants on the Avro write path for parity… #19777 covers declared/forced nested shredding, not inference).
  • typed_value is per-file physical layout: TableSchemaResolver strips it, so the commit schema and the catalog schema do not change.
  • PARQUET_VARIANT_WRITE_SHREDDING_ENABLED's doc now names inference as the second source of a shredding schema next to a schema-declared typed_value.
  • Inference buffers up to 4096 records or 64MB per open file writer before the writer is created, on top of parquet's own row-group buffer; that buffer is now active by default on Spark 4.1+ writers.

Verification (default on)

  • spark4.1, scala-2.13, JDK 17: TestVariantDataType 20 passed / 3 canceled (Spark 3 legs), TestVariantShreddingMixedLayouts 27/27, TestStreamingSource shredded-variant legs 2/2, TestVariantSchemaUtils 15/15, TestHoodieAvroFileWriterFactoryVariantInference 1/1, TestHoodieVariantReconstruction 13/13; checkstyle clean on hudi-common and hudi-hadoop-common.
  • spark4.2 (Spark 4.2.0, scala 2.13.18): the same three Spark suites, 20/0+3, 27/27, 2/2.
  • Why no Spark assertion moved: VariantShreddingTestSupport.layoutConfs sets write shredding and inference explicitly per layout, and the unshredded twins in TestVariantDataType (CDC, small-file merge, clustering) set hoodie.parquet.variant.write.shredding.enabled=false, which also gates inference.

Cross-engine

  • Spark 4.1+ reconstructs shredded variants natively. Spark 4.0 and Hive fail fast through the fix(variant): close the shredded-read gaps exposed by a mixed-layout test matrix #19687 guards, Flink through ParquetSplitReaderUtil (ITTestVariantCrossEngineCompatibility.testFlinkReadShreddedVariantCOWTableFailsFast), Spark 3.x through ParquetSchemaEvolutionUtils.validateNoShreddedVariantStructs - whether the column is auto-resolved from the table schema or declared as the two-binary struct. None of them returns partial payloads. The Trino connector does not read variant yet.
  • Opt-out keys: hoodie.parquet.variant.shredding.schema.inference.enabled=false (inference only) or hoodie.parquet.variant.write.shredding.enabled=false (all shredding). For SQL tables that is a tblproperty set once at CREATE or ALTER; path-based procedures, the DataSource writer and the streamer take it as a write option.
  • Mixed layouts within a table or file group are supported (fix(variant): close the shredded-read gaps exposed by a mixed-layout test matrix #19687), so opting out later or clustering back is safe.

Release note

Variant columns are shredded by default on Spark 4.1+ writers (hoodie.parquet.variant.shredding.schema.inference.enabled now defaults to true), matching Spark 4.1's own parquet writer. Shredded files need Spark 4.1+ to read; Spark 4.0, Spark 3.x, Hive and Flink fail fast on them, with an explicit error rather than a null variant payload. Set hoodie.parquet.variant.shredding.schema.inference.enabled=false (or hoodie.parquet.variant.write.shredding.enabled=false) on the writer for tables those engines read; already shredded files can be rewritten unshredded by clustering with that option off. Existing files are unaffected.

Risk mitigation

Contributor's checklist

  • Read through contributor's guide
  • Enough context is provided in the sections above
  • Adequate tests were added if applicable

Flip hoodie.parquet.variant.shredding.schema.inference.enabled to
true, closing apache#19690. Spark 4.1 itself defaults both
spark.sql.variant.writeShredding.enabled and
spark.sql.variant.inferShreddingSchema to true, so a Hudi table on
Spark 4.1 was the one parquet writer in that stack still producing
unshredded variant columns.

Breaking change: base files and native parquet log files written by
Spark 4.1+ writers now carry typed_value for top-level variant
columns, and only Spark 4.1+ can read them. Spark 4.0, Hive and
Flink readers of such tables fail fast (apache#19687) where they read the
unshredded layout before. Opt out per writer or per table with
hoodie.parquet.variant.shredding.schema.inference.enabled=false (or
hoodie.parquet.variant.write.shredding.enabled=false); already
shredded files are rewritten unshredded by clustering with the
option off. Existing files are not touched.

Only writers with a Spark 4.1+ inferrer on the classpath change
behavior: Spark 3.x, Spark 4.0, Flink and Java writers keep writing
unshredded files, which the Avro-factory test now pins with a stock
config. The config doc carries the reader matrix, the opt-out and
the un-shred recipe.

TestVariantSchemaUtils pins the new default and sets the flag off
explicitly where it tested the disabled path. No Spark assertion
moved: every unshredded-layout pin already disables write shredding
or inference explicitly.
@voonhous
voonhous force-pushed the variant-shredding-inference-default-on branch from 203830a to 4d8078c Compare September 1, 2026 11:50

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for working on this! This PR flips hoodie.parquet.variant.shredding.schema.inference.enabled to default on, so Spark 4.1+ writers shred top-level variant columns by default, and refreshes the config docs and the two pinning tests accordingly. I traced the three writer factories (Avro, Spark row, Spark) and confirmed the inference path is consistently gated on VariantShreddingRuntime.lookupInferrer().isPresent(), so non-4.1 writers still degrade to unshredded — the gating looks correct and the tests are self-consistent with the new default. One cross-engine durability point on the default-on decision is worth a committer's eyes (see inline). Please take a look at the inline comment, and this should be ready for a Hudi committer or PMC member to take it from here.

public static final ConfigProperty<Boolean> PARQUET_VARIANT_SHREDDING_SCHEMA_INFERENCE_ENABLED = ConfigProperty
.key("hoodie.parquet.variant.shredding.schema.inference.enabled")
.defaultValue(false)
.defaultValue(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Flipping this to default-on while it stays a write config (not persisted per table) inverts the failure mode of the "writer forgot the config" case: before, a writer missing this wrote unshredded and stayed cross-engine readable; now any Spark 4.1+ writer that misses the opt-out (a path-based write, the DataSource writer, a streamer) silently shreds and breaks Flink/Hive/Spark-4.0 reads of that same table. Have you considered persisting the shredding decision as a table property so the opt-out is durable across every writer, rather than relying on each write path to pass it? @yihua since this is effectively a storage-format-default change, worth a committer/PMC look at the default-on choice and confirming the non-4.1 reader fail-fast is complete (no silent payload drop) across all read paths.

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

@voonhous voonhous Sep 1, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping it on by default. The audit this asks for did turn up one real silent-drop path, now fixed in this PR.

Default-on stays. Spark 4.1 defaults both spark.sql.variant.writeShredding.enabled and spark.sql.variant.inferShreddingSchema to true; a plain variant column in Hudi should land on disk the way the same column lands in plain parquet. The key is sinceVersion("1.3.0") and unreleased, so no existing table changes behavior on upgrade -- flipping it after the first release is the change that would be an upgrade regression.

On persisting the decision as a table property. Shredded-ness is per file, not per table, and already was before this PR: inference samples the first records of each open file and splices a typed_value schema for that file alone, a file whose sample has nothing to shred stays unshredded, Avro log blocks never shred, and hoodie.parquet.variant.write.shredding.enabled has defaulted to true since 1.1.0 -- a writer that missed the config already shredded whenever the write schema carried a typed_value. So there is no table-level invariant to persist, and none this PR removes. The value is also not out of reach per table: SQL DML and table-name procedures pick it up from the table's catalog properties. The remaining gap for path-based procedures, the DataSource writer and the streamer is the same for every hoodie.parquet.* write config (codec, block size, bloom filter).

Fail-fast audit for non-4.1 readers

Reader On a shredded file Where
Spark 4.1 / 4.2 reconstructs supported
Spark 4.0 throws Spark40HoodieParquetReadSupport.rejectShreddedVariants, off the footer from both the read support and Spark40ParquetReader
Spark 3.x, schema from commit metadata throws no VariantType, the schema does not convert (BaseSpark3Adapter)
Spark 3.x / 4.x schema-on-read throws ParquetSchemaEvolutionUtils.validateNoShreddedVariants
Flink 2.1 / 2.2 throws ParquetSplitReaderUtil.validateVariantType, ParquetSchemaConverter.convertToRowType
Flink <= 2.0 no VARIANT type, the column cannot be declared --
Hive throws HoodieParquetInputFormat.validateNoShreddedVariantRead, HiveHoodieReaderContext
Java / Avro reader throws on no-provider and on allow.reading.shredded=false HoodieVariantReconstruction.create
Compaction / merge / bootstrap reconstructs, keeps typed_value VariantSchemaUtils.alignShreddedVariants (#19567)

Spark 4.0 is in that list because it can write shredded from an explicit typed_value schema; what it cannot do is infer one, since the inferrer ships only in the 4.1 and 4.2 modules. So this flip never produces a shredded file on a 4.0-only pipeline.

On Flink, base files and native parquet log blocks both reach the guard -- FlinkRowDataReaderContext.getFileRecordIterator routes both through HoodieRowDataParquetReader -> RecordIterators -> ParquetSplitReaderUtil. It fires at reader construction, before any row, and only for a projected column. Flink writes over such a table (upsert, compaction) read through the same path, so they fail rather than round-tripping a dropped typed_value. ITTestVariantCrossEngineCompatibility#testFlinkReadShreddedVariantCOWTableFailsFast pins it against a fixture whose first file group is shredded and second is not, and asserts the guard's own message.

The gap, fixed in this PR. Spark 3.x has no VariantType, so the table's own schema does not convert and the documented way to read a variant table there is to declare the column as struct<value: binary, metadata: binary> -- the same shape Hive sync writes to the metastore. Parquet reconciles requested against file fields by name, so a shredded group's typed_value was simply not projected and those rows came back with a null value: dropped silently, which is precisely what this thread asks about. Added ParquetSchemaEvolutionUtils.validateNoShreddedVariantStructs, called from Spark33/34/35ParquetReader and Spark3LegacyHoodieParquetFileFormat. The anchor is two-sided -- the requested side exactly two binary members named metadata and value, the file side carrying typed_value at that same path -- so a plain user struct is exempt, an unshredded file still reads byte for byte as before, and a query that does not project the column is untouched. Reconstruction is not an alternative on Spark 3.x: the only VariantShreddingProvider ships in spark4-common. Unit tests in TestParquetSchemaEvolutionUtils. 404c531.

Trino has no variant support and no guard on our side, and the synced Hive type there is the same two-binary struct. That is out of scope here -- variant is not usable through that connector today regardless.

Opt-out for tables other engines read. hoodie.parquet.variant.shredding.schema.inference.enabled=false keeps schema-declared shredding; hoodie.parquet.variant.write.shredding.enabled=false disables shredding entirely. Files already shredded return to the unshredded layout by clustering with either set to false.

P.S. comment is a little long and may read like it's LLM, but it's long to cover every aspect of cross version support, configuration location and ownership. So please bear with the lengthiness.

@github-actions github-actions Bot added the size:S PR with lines of changes in (10, 100] label Sep 1, 2026
@codecov-commenter

codecov-commenter commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.90566% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.31%. Comparing base (c59987a) to head (047578b).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
...asources/parquet/ParquetSchemaEvolutionUtils.scala 85.71% 0 Missing and 4 partials ⚠️
.../parquet/Spark3LegacyHoodieParquetFileFormat.scala 0.00% 2 Missing ⚠️
...torage/row/HoodieInternalRowFileWriterFactory.java 87.50% 0 Missing and 1 partial ⚠️
...urces/parquet/Spark3HoodieParquetReadSupport.scala 90.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19808      +/-   ##
============================================
- Coverage     78.31%   78.31%   -0.01%     
- Complexity    33888    33897       +9     
============================================
  Files          2541     2542       +1     
  Lines        141728   141775      +47     
  Branches      17182    17197      +15     
============================================
+ Hits         111001   111033      +32     
- Misses        23017    23029      +12     
- Partials       7710     7713       +3     
Components Coverage Δ
hudi-common 83.64% <100.00%> (-0.01%) ⬇️
hudi-client 83.22% <87.50%> (+<0.01%) ⬆️
hudi-flink 85.65% <ø> (-0.06%) ⬇️
hudi-spark-datasource 73.24% <83.72%> (+0.03%) ⬆️
hudi-utilities 74.55% <ø> (+<0.01%) ⬆️
hudi-cli 15.06% <ø> (ø)
hudi-hadoop 70.71% <ø> (-0.02%) ⬇️
hudi-sync 75.56% <ø> (ø)
hudi-io 79.98% <ø> (ø)
hudi-timeline-service 83.44% <ø> (ø)
hudi-cloud 65.81% <ø> (ø)
hudi-kafka-connect 53.20% <ø> (ø)
Flag Coverage Δ
common-and-other-modules 51.45% <52.83%> (+<0.01%) ⬆️
flink-integration-tests 48.82% <100.00%> (-0.02%) ⬇️
hadoop-mr-java-client 44.06% <100.00%> (-0.02%) ⬇️
integration-tests 13.51% <3.77%> (-0.01%) ⬇️
spark-client-hadoop-common 50.55% <20.00%> (+<0.01%) ⬆️
spark-java-tests 52.24% <71.05%> (+0.02%) ⬆️
spark-scala-tests 46.97% <23.68%> (+0.09%) ⬆️
utilities 36.56% <54.71%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...apache/hudi/common/config/HoodieStorageConfig.java 90.25% <100.00%> (ø)
...g/apache/spark/sql/adapter/BaseSpark3Adapter.scala 30.61% <100.00%> (+2.95%) ⬆️
...ion/datasources/parquet/Spark35ParquetReader.scala 94.40% <100.00%> (+0.03%) ⬆️
...torage/row/HoodieInternalRowFileWriterFactory.java 65.38% <87.50%> (+2.88%) ⬆️
...urces/parquet/Spark3HoodieParquetReadSupport.scala 90.00% <90.00%> (ø)
.../parquet/Spark3LegacyHoodieParquetFileFormat.scala 0.00% <0.00%> (ø)
...asources/parquet/ParquetSchemaEvolutionUtils.scala 66.12% <85.71%> (+4.10%) ⬆️

... and 14 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Spark 3.x has no VariantType, so a variant column is read there by
declaring it as struct<value: binary, metadata: binary>, the same
shape Hive sync writes to the metastore. Parquet reconciles the
requested against the file fields by name, so a shredded group's
typed_value was never projected and those rows came back with a
null value: the payload was dropped silently. Reconstruction is
not an option, as the only VariantShreddingProvider ships in
spark4-common, so reject the read instead, the way Spark 4.0,
Flink and Hive already do.

Add ParquetSchemaEvolutionUtils.validateNoShreddedVariantStructs
and call it from Spark33/34/35ParquetReader and the legacy file
format. The anchor is two-sided - the request must be exactly the
two binary members named metadata and value, and the file must
carry typed_value at that path - so a plain user struct, an
unshredded file and a query that does not project the column are
all untouched.
@github-actions github-actions Bot added size:M PR with lines of changes in (100, 300] and removed size:S PR with lines of changes in (10, 100] labels Sep 1, 2026
testValidateNoShreddedVariantStructsRejectsTopLevelShreddedVariant
passed the bare struct<metadata, value> as the required schema, so
the guard walked its two members against a file whose only column
is v, matched nothing and returned without throwing. Request the
struct as column v, the way the nested and exemption legs already
do.
@voonhous

voonhous commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

@wombatu-kun Can you. please help to review this PR, this should be the last PR for toggling shredding inference to true by default for Spark4.1+ writers.

This should wrap up our work for Variant + shredding on Spark, allowing us to work on other more important priorities.

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for working on this! This PR flips hoodie.parquet.variant.shredding.schema.inference.enabled to default-on for Spark 4.1+ writers and adds a Spark 3.x read guard (validateNoShreddedVariantStructs) that fails fast when a variant column requested as its unshredded struct<value, metadata> shape sits over a shredded parquet group. The guards are wired into the Spark 3.3/3.4/3.5 readers and the legacy file format, and I confirmed the MOR file-group-reader path reuses the same createParquetFileReader().read, so it's covered too. One edge case around partial (nested-pruned) projection of the variant struct is worth double-checking — see the inline comment. The default-on failure-mode tradeoff has already been discussed in the earlier thread. Please take a look at the inline comment, and this should be ready for a Hudi committer or PMC member to take it from here.

* reading it deliberately.
*/
private def isUnshreddedVariantStruct(struct: StructType): Boolean = {
struct.fields.length == 2 &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 isUnshreddedVariantStruct requires exactly 2 fields, but nested schema pruning can hand this guard a pruned variant struct. SELECT v.value on a CoW read (via the HadoopFsRelation fallback in BaseFileOnlyRelation, where nested pruning applies) yields requiredSchema = v: struct<value: binary> — length 1, so this returns false, the walk falls through to the plain-struct recursion into the primitive value, and typed_value is never checked. That silently returns null for every shredded row, exactly the drop this guard aims to stop. The sibling validateNoShreddedVariant avoids this by keying on variant field-id markers rather than field count. Could the subset-projection case be covered too?

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed reachable: HoodieFileGroupReaderBasedFileFormat extends ParquetFileFormat, so Spark's SchemaPruning narrows the relation's data schema exactly as TestNestedSchemaPruningOptimization asserts, and FileGroupReaderSchemaHandler forwards that narrowed struct verbatim on a base-file-only read. Accepting a non-empty subset of the two binary members in isUnshreddedVariantStruct closes it without weakening the third-member exemption the new tests pin.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed. isUnshreddedVariantStruct now accepts a non-empty subset: every member must be binary and named metadata or value. A member outside those two names still exempts the struct, so the three-member case the tests pin is unchanged.

One consequence worth naming: pruning a deliberately declared {metadata, value, typed_value} struct down to value alone now lands on the guard, since at that point nothing distinguishes it from the variant request. Called out in the scaladoc.

Coverage: testValidateNoShreddedVariantStructsRejectsPrunedVariantStruct pins both single-member spellings against a shredded file and its unshredded twin, and the new functional test runs a pruned select id, v.value leg.

Comment thread hudi-common/src/main/java/org/apache/hudi/common/config/HoodieStorageConfig.java Outdated
Comment thread hudi-common/src/main/java/org/apache/hudi/common/config/HoodieStorageConfig.java Outdated
Review follow-ups on the Spark 3.x shredded-variant guard.

Nested schema pruning narrows a request to the leaves a query
touches, so `SELECT v.value` reached the guard as a one-member
struct and slipped past a shape test that wanted both members.
Accept a non-empty subset instead: every member binary and named
metadata or value. A member outside those two names still exempts
the struct, so the three-member shape stays exempt unless pruning
strips it back to one of the two.

Log blocks never met the guard at all. They are read through
HoodieSparkParquetReader, which builds its reader on whatever
SparkAdapter.createParquetReadSupport returns, and Spark 3.x got
the plain support. Return Spark3HoodieParquetReadSupport instead,
guarding in init against the file schema the way Spark 4.0 already
does. validateNoShreddedVariantStructs now takes a MessageType so
both routes share the walk.

Also name Spark 3.x in the config's reader list, name
write.shredding.enabled in the un-shred recipe (the key that
actually strips typed_value), correct the Avro factory test's gate
description, and pin the guard's call sites with a Spark 3 leg
over the mixed shredded fixture.
@github-actions github-actions Bot added size:L PR with lines of changes in (300, 1000] and removed size:M PR with lines of changes in (100, 300] labels Sep 2, 2026

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for working on this! This PR flips variant shredding-schema inference on by default for Spark 4.1+ writers and adds a fail-fast guard (validateNoShreddedVariantStructs) so Spark 3.x readers reject shredded-variant files instead of silently returning null values. I traced the guard's schema walk (top-level, nested, and pruned-subset cases), confirmed all Spark 3.x read routes are covered (per-version base readers, the legacy format, and the log-block read-support path), and checked that the inference gating still skips schema-on-read tables under the new default. No new issues flagged from this automated pass beyond what earlier rounds already covered — a Hudi committer or PMC member can take it from here for a final review.

cc @yihua

Review follow-ups on the Spark 3.x shredded-variant guard.

The readers it protects force spark.sql.caseSensitive=false, so a
column declared V or a member declared Value still resolves onto
the file's lower-case group. Resolve the same way in the guard:
member names compare case-insensitively, and requested columns and
struct members are looked up in the file schema ignoring case.

Cover Spark3HoodieParquetReadSupport directly over a hand-built
InitContext, in a new test dir for hudi-spark3-common, so a wiring
slip in init cannot fall silently back to the null-value read. A
third leg shows the clipped request carries no typed_value, which
is why init anchors on the file schema.

Drop the explicit inference tblproperty from the COW inference test
so the default flip is pinned end to end.
public static final ConfigProperty<Boolean> PARQUET_VARIANT_SHREDDING_SCHEMA_INFERENCE_ENABLED = ConfigProperty
.key("hoodie.parquet.variant.shredding.schema.inference.enabled")
.defaultValue(false)
.defaultValue(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With inference on by default, HoodieInternalRowFileWriterFactory.newParquetInternalRowFileWriter calls getInferableVariantColumnsFromConfig before checking VariantShreddingRuntime.lookupInferrer, so every row-writer file handle now pays an uncached HoodieSchema.parse of hoodie.avro.schema - on every table, and on classpaths that can never infer. Check the inferrer first (it is a static field), the way HoodieSparkFileWriterFactory and HoodieAvroFileWriterFactory gate on the already-parsed schema argument.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reordered by cost: the inferrer lookup (a static field) goes first, so a Spark 3.x or 4.0 classpath pays nothing; then a scan of the StructType argument for a top-level variant through the adapter's isVariantType; only a variant table on an inferring classpath then reaches getInferableVariantColumnsFromConfig and its parse. The other two factories already gate on their parsed schema argument; this one now does the equivalent on its StructType.

val group = parquetType.asGroupType()
dataType match {
case struct: StructType if isUnshreddedVariantStruct(struct) =>
if (group.containsField(HoodieSchema.Variant.VARIANT_TYPED_VALUE_FIELD)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file side fires on typed_value alone, while Spark40HoodieParquetReadSupport.isVariantGroup and HoodieParquetInputFormat.collectShreddedVariantPaths both also require a binary metadata in the same group - so a plain user struct that happens to carry a typed_value member is rejected once pruning narrows the request to value. Add the metadata requirement here too so the file-side anchor matches the siblings this mirrors.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added - isShreddedVariantGroup now requires typed_value next to a binary metadata, the file-side anchor the Hive and Spark 4.0 guards use (value stays optional, as the spec allows). A leg in testValidateNoShreddedVariantStructsLeavesOtherRequestsAlone pins a user struct {value: binary, typed_value: int} under a request pruned to value, which now reads.

Review follow-ups.

The row-writer factory called getInferableVariantColumnsFromConfig
before looking up the inferrer, and with inference on by default
its flag gates pass on every table, so every file handle paid an
uncached parse of hoodie.avro.schema - on classpaths that can never
infer too. Order the gates by cost: the static inferrer lookup,
then a scan of the StructType for a top-level variant, and only
then the config-schema parse.

The Spark 3.x guard fired on typed_value alone, so once pruning had
narrowed a request to a lone value member, a user struct that
merely carried a typed_value was rejected too. Require a binary
metadata beside it, the file-side anchor the Hive and Spark 4.0
guards already use.
@hudi-bot

hudi-bot commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

CI report:

Bot commands @hudi-bot supports the following commands:
  • @hudi-bot run azure re-run the last Azure build

@voonhous
voonhous merged commit ef83418 into apache:master Sep 2, 2026
22 of 23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L PR with lines of changes in (300, 1000]

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flip variant shredding-schema inference on by default

5 participants