Skip to content

refactor(variant): self-align log-block variant rows, drop buffer-level projection hook - #18923

Merged
voonhous merged 10 commits into
apache:masterfrom
voonhous:fix-18739
Aug 28, 2026
Merged

refactor(variant): self-align log-block variant rows, drop buffer-level projection hook#18923
voonhous merged 10 commits into
apache:masterfrom
voonhous:fix-18739

Conversation

@voonhous

@voonhous voonhous commented Jun 5, 2026

Copy link
Copy Markdown
Member

Describe the issue this Pull Request addresses

Addresses #18739.

PR #18674 aligned Spark 4.1+ MOR variant reads via HoodieReaderContext.getLogBlockRecordProjection, a per-row projector run inside the engine-neutral FileGroupRecordBuffer. That leaked Spark PushVariantIntoScan concerns into format-agnostic merge code. This PR pushes variant projection down into the log readers so the buffer no longer knows about variants.

Summary and Changelog

Each log reader now emits rows already aligned to the projected read schema; the buffer-level projection hook is removed.

  • Remove FileGroupRecordBuffer.getProjectedTransformer and HoodieReaderContext.getLogBlockRecordProjection. The buffers (FileGroupRecordBuffer, PositionBasedFileGroupRecordBuffer) now call getSchemaTransformerWithEvolvedSchema directly and keep returning the unprojected evolved schema (merger reads metadata cols by ordinal).
  • Add a no-op HoodieReaderContext.projectLogBlockRecords(iter, dataBlockSchema). HoodieAvroDataBlock.deserializeRecords invokes it; the Spark reader context overrides it to apply the VariantGet rewrite (relocated from the old hook).
  • Parquet log blocks: new HoodieSparkParquetReader.getUnsafeRowIterator(HoodieSchema, StructType, filters) overload threads the projected struct so SPARK_ROW_REQUESTED_SCHEMA carries VariantMetadata and parquet-mr projects natively (mirrors the base-file path).
  • Single shouldProjectVariants gate (variant projection present AND merger not PAYLOAD_BASED) drives both paths, preserving the previous custom-payload skip.

No code copied. buildVariantProjector / isVariantProjectionStruct are unchanged (caller moved).

Spark version scope: the projection engages on every Spark version whose adapter reports a PushVariantIntoScan projection struct, which today is Spark 4.1 and 4.2 (Spark4_1Adapter and Spark4_2Adapter implement isVariantProjectionStruct / buildVariantProjector identically). Spark 4.0 and 3.x adapters return false and take the unprojected path. No per-version code in this PR.

Rebase notes (2026-08-25, over #18961): the parquet-log leg now sits in the PARQUET arm of the log-file format dispatch from #19118/#19283 (Lance native logs keep going through baseFileReader.read with the overlaid schema) and uses the position-merge filter guard from #19583; the other five files merged clean.

Impact

Internal change to the HoodieReaderContext extension point (getLogBlockRecordProjection -> projectLogBlockRecords); the new hook is a no-op by default. No new configs. No user-facing behavior change for non-Spark engines or Spark < 4.1; Spark 4.1 and 4.2 variant MOR reads are unchanged in result, only the alignment moves out of the buffer.

Risk Level

Medium. Touches the engine-neutral merge buffer and the reader-context API. Mitigations: the new hook is a no-op default (non-Spark engines and Spark < 4.1 unaffected); projection is gated by shouldProjectVariants (custom-payload tables skip it); the buffer still returns the unprojected evolved schema, so the merger's ordinal-based metadata access is unchanged. Verified with TestVariantDataType on Spark 4.0, 4.1 and 4.2 (cow + mor: insert / update / delete / merge-into, cast(v as string) selects) across key-based, position-based, and unmerged paths, plus a custom-payload variant MOR case. The 4.2 run is on the rebased head (975b687, Spark 4.2.0): 19 passed, 0 failed, the 3 Spark 3.x-only cases canceled by their version guard; it includes the log-only MOR query, MOR compaction with auto-inferred shredding, CDC and clustering cases added after #18961. CI runs the same suite on Spark 4.2 through the test-spark-java17-scala-dml-tests lane (the only 4.x version in the CI matrix). The dedicated shredded-native-log MOR legs in TestVariantShreddingMixedLayouts still run once #19687 lands.

Documentation Update

none

Contributor's checklist

  • Read through contributor's guide
  • Enough context is provided in the sections above
  • Adequate tests were added if applicable
@voonhous voonhous linked an issue Jun 5, 2026 that may be closed by this pull request
5 tasks
@voonhous
voonhous requested a review from yihua June 5, 2026 13:16
@github-actions github-actions Bot added the size:M PR with lines of changes in (100, 300] label Jun 5, 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 the contribution! This PR pushes variant projection down into the log readers (parquet native projection and avro deserialization hook) and removes the buffer-level projection step, which cleanly separates Spark-4.1 variant concerns from format-agnostic merge code. One worth-checking edge case in the inline comment around how the new ordering interacts with the schema-evolution transformer when both are active. Please take a look at any inline comments, and this should be ready for a Hudi committer or PMC member to take it from here. A couple of small readability nits below, otherwise the refactor is clean.

@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 cleanly pushes the Spark 4.1 variant projection out of the engine-neutral merge buffer and into the engine-specific reader/log-block paths, which is a nice architectural improvement. No new issues flagged from this automated pass — the ordering-flip concern (schema-evolve then project → project then schema-evolve, which only matters when schema-on-read is active) was already raised and acknowledged in round 1. A Hudi committer or PMC member can take it from here for a final review. A couple of minor naming and readability suggestions below.

cc @yihua

@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 addressing the feedback! These follow-ups rename the parquet reader's variant overload parameter to projectedStructSchema (matching Javadoc and call sites) and document why the merger != null guard in the Spark reader context is real rather than defensive. Both of my prior nits and the corresponding hudi-agent suggestions are resolved, and no new issues surfaced from this incremental pass — a Hudi committer or PMC member can take it from here for a final review.

cc @yihua

@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 the iteration! This round's changes (renaming structSchemaprojectedStructSchema, extracting isPayloadBased, and the null-guard rationale) all look good. The variant-projection refactor cleanly separates engine-specific concerns from the buffer code — Avro log blocks project in deserializeRecords via projectLogBlockRecords, parquet log blocks project natively in the reader, and the buffers go back to the plain evolved-schema transformer. No new issues flagged from this automated pass — a Hudi committer or PMC member can take it from here for a final review. One minor naming suggestion; the rest of the changes are clean and well-commented.

cc @yihua

@voonhous
voonhous enabled auto-merge (squash) June 22, 2026 01:11

@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 pushes Spark 4.1 variant projection out of the engine-neutral merge buffer and into the log readers: avro log blocks rewrite rows via the new projectLogBlockRecords hook in HoodieAvroDataBlock.deserializeRecords, parquet log blocks project natively through the new getUnsafeRowIterator overload, and a single shouldProjectVariants gate drives both. I traced both read paths, both record buffers, the payload-based gate equivalence, the schema flow into the variant projector, and record count and position alignment, and the refactor looks internally consistent. The previously flagged evolve-then-project ordering change under schema-on-read remains the one item the author has already acknowledged and tested. No correctness issues found. A few style/readability suggestions in the inline comments. Please take a look, and this should be ready for a Hudi committer or PMC member to take it from here., and a Hudi committer or PMC member can take it from here for a final review. A couple of minor readability suggestions below — one on an ambiguous Javadoc opener and one on comment placement.

cc @yihua

@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 pushes Spark 4.1 variant projection down into the log readers so the engine-neutral FileGroupRecordBuffer no longer knows about variants: parquet log blocks project natively via a new getUnsafeRowIterator overload and avro log blocks go through a new no-op projectLogBlockRecords hook. I traced the hook wiring, the removed-hook call sites, and the common-case behavior against the old code and they line up; the evolve-vs-project ordering flip under schema-on-read is already covered in the existing inline discussion. No correctness issues found. A few style/readability suggestions in the inline comments. Please take a look, and this should be ready for a Hudi committer or PMC member to take it from here. - a Hudi committer or PMC member can take it from here for a final review. One naming nit on the new Javadoc overload; the rest of the refactor is clean and well-commented.

cc @yihua

…er hook

Remove the engine-neutral FileGroupRecordBuffer variant-projection composition
(apache#18674's getLogBlockRecordProjection hook) so the merge buffer stays
format-agnostic; each log reader now emits rows already aligned to the projected
read schema (apache#18739).

- Parquet log blocks: thread the variant-overlaid StructType into a new
  HoodieSparkParquetReader.getUnsafeRowIterator(HoodieSchema, StructType, filters)
  overload so SPARK_ROW_REQUESTED_SCHEMA carries VariantMetadata and parquet-mr
  decodes variants into the projected struct shape natively (mirrors the base-file
  path). Wired in SparkFileFormatInternalRowReaderContext.getFileRecordIterator.
- Avro log blocks: new no-op HoodieReaderContext.projectLogBlockRecords hook,
  invoked from HoodieAvroDataBlock.deserializeRecords; Spark overrides it to apply
  the VariantGet rewrite (relocated from the deleted buffer hook).
- Both paths gated by a single shouldProjectVariants predicate (variant projection
  present AND merger not PAYLOAD_BASED), preserving the buffer's custom-payload skip.
- FileGroupRecordBuffer/PositionBased now call getSchemaTransformerWithEvolvedSchema
  directly; getProjectedTransformer and getLogBlockRecordProjection deleted.
- Sub-task 4: documented why the sparkRequiredSchema overlay must stay (HoodieSchema
  can't carry VariantMetadata); kept Spark-side, no schema-model change.

buildVariantProjector / isVariantProjectionStruct unchanged (caller moved).

Addresses apache#18739.
- HoodieSparkParquetReader: rename variant-overload parameter to
  structSchema and drop the no-op alias.
- SparkFileFormatInternalRowReaderContext: extract isPayloadBased and
  drop the double negation in shouldProjectVariants.
…rger guard

- Rename getUnsafeRowIterator param structSchema to projectedStructSchema
- Add inline comment explaining the merger != null guard
…d sparkRequiredSchema

Lead the getUnsafeRowIterator(HoodieSchema, StructType, filters) javadoc with what the
overload does instead of the ambiguous "Variant overload." label, and move the rationale for
the parallel sparkRequiredSchema overlay onto the constructor parameter doc where a reader
first meets the field.

@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 pushes Spark 4.1 variant projection down into the log readers — parquet log blocks project natively via a new getUnsafeRowIterator overload, and avro log blocks rewrite via the new no-op projectLogBlockRecords hook — so the engine-neutral FileGroupRecordBuffer no longer knows about variants. I traced both buffers and both log paths (record-count preservation for the position-based buffer, iterator close chaining, and the unprojected-evolved-schema kept for the merger's read-by-ordinal contract) and didn't surface a new correctness issue in this round; the previously-flagged evolve-vs-project ordering edge case under schema-on-read has already been discussed. No correctness issues found. A few style/readability suggestions in the inline comments. Please take a look, and this should be ready for a Hudi committer or PMC member to take it from here. One naming nit worth a look, otherwise the refactor is clean and readable.

cc @yihua

It reads getRecordMerger(), which is null until setRecordMerger() runs during reader init,
so it is state-dependent rather than a pure property; the empty parens signal that to
callers per Scala convention.

@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 pushes Spark 4.1 variant projection down into the log readers (native parquet-mr projection for parquet log blocks, a projectLogBlockRecords hook in HoodieAvroDataBlock.deserializeRecords for avro) and removes the buffer-level getLogBlockRecordProjection hook so the engine-neutral merge buffer no longer knows about variants. I traced the gate-condition change (old payloadClasses.isPresent skip vs the new PAYLOAD_BASED merger-strategy check, which are equivalent via getPayloadClasses), the single-vs-double projection across the avro and parquet paths, position-based record-to-position alignment, and the closable-iterator chains, and found no new correctness issues in this pass. The evolve-then-project vs project-then-evolve ordering flip is already being discussed on FileGroupRecordBuffer.java, so I have left that thread to run. No new issues flagged from this automated pass, a Hudi committer or PMC member can take it from here for a final review.

cc @yihua

…9 MOR table

withRecordType()'s AVRO leg sets hoodie.logfile.data.block.format=avro, but on the
current table version HoodieNativeLogAppendHandle takes the log format from the base
file format and never consults it, so every MOR case in TestVariantDataType wrote native
parquet logs and projectLogBlockRecords never ran with its gate open. Pin a MOR table to
hoodie.write.table.version=9, read the log blocks back to assert they are AVRO_DATA_BLOCK
(a silent fallback to parquet now fails instead of passing), and read the base + avro-log
merge and a delete through cast(v as string).

@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 pushes Spark 4.1 variant projection down into the log readers: parquet log blocks project natively via the new getUnsafeRowIterator(requiredSchema, structType, filters) overload, avro log blocks via the new projectLogBlockRecords hook, and the buffer-level getLogBlockRecordProjection and getProjectedTransformer are removed so FileGroupRecordBuffer no longer knows about variants. I traced the routing (parquet inline blocks reach getFileRecordIterator, avro blocks reach HoodieAvroDataBlock.deserializeRecords, default pass-through elsewhere), the payload-based gating, and the position-based buffer record-count and position invariants; they hold up, and the single-arg overload delegation is behavior-preserving. No correctness issues found. A few style/readability suggestions in the inline comments. Please take a look, and this should be ready for a Hudi committer or PMC member to take it from here.; a Hudi committer or PMC member can take it from here for a final review.
. One readability nit in the test file — the listLogBlockTypes method is inserted between the listDataParquetFiles Javadoc and its method body, orphaning the comment.

cc @yihua

…parkSqlTestBase companion

It is defined on the companion object, not the base class, so the helper added in
e50c51b did not compile (scala-test-compile on hudi-spark_2.12).
@hudi-bot

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
@apache apache deleted a comment from codecov-commenter Aug 27, 2026
@codecov-commenter

codecov-commenter commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.87879% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.98%. Comparing base (ef07f0f) to head (94e081a).
⚠️ Report is 23 commits behind head on master.

Files with missing lines Patch % Lines
...hudi/SparkFileFormatInternalRowReaderContext.scala 77.77% 1 Missing and 3 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #18923      +/-   ##
============================================
+ Coverage     77.95%   77.98%   +0.02%     
- Complexity    33447    33471      +24     
============================================
  Files          2539     2539              
  Lines        140891   140984      +93     
  Branches      17008    17054      +46     
============================================
+ Hits         109829   109940     +111     
+ Misses        23401    23382      -19     
- Partials       7661     7662       +1     
Components Coverage Δ
hudi-common 83.49% <100.00%> (+0.04%) ⬆️
hudi-client 83.07% <82.60%> (+0.03%) ⬆️
hudi-flink 85.64% <ø> (-0.03%) ⬇️
hudi-spark-datasource 72.38% <ø> (+0.01%) ⬆️
hudi-utilities 74.39% <ø> (+0.05%) ⬆️
hudi-cli 15.06% <ø> (ø)
hudi-hadoop 69.24% <ø> (+0.01%) ⬆️
hudi-sync 75.54% <ø> (+0.09%) ⬆️
hudi-io 79.85% <ø> (ø)
hudi-timeline-service 83.44% <ø> (ø)
hudi-cloud 64.27% <ø> (-0.06%) ⬇️
hudi-kafka-connect 53.20% <ø> (ø)
Flag Coverage Δ
common-and-other-modules 51.05% <21.21%> (+0.11%) ⬆️
flink-integration-tests 48.99% <50.00%> (-0.03%) ⬇️
hadoop-mr-java-client 43.65% <100.00%> (-0.09%) ⬇️
integration-tests 13.56% <6.06%> (-0.01%) ⬇️
spark-client-hadoop-common 50.53% <30.30%> (-0.02%) ⬇️
spark-java-tests 52.01% <63.63%> (+0.01%) ⬆️
spark-scala-tests 46.66% <84.84%> (+0.03%) ⬆️
utilities 36.40% <60.60%> (-0.01%) ⬇️

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

Files with missing lines Coverage Δ
...ache/hudi/io/storage/HoodieSparkParquetReader.java 88.54% <100.00%> (+0.12%) ⬆️
...apache/hudi/common/engine/HoodieReaderContext.java 91.11% <100.00%> (ø)
...di/common/table/log/block/HoodieAvroDataBlock.java 54.87% <100.00%> (+0.23%) ⬆️
...ommon/table/read/buffer/FileGroupRecordBuffer.java 95.18% <100.00%> (-0.38%) ⬇️
...ead/buffer/PositionBasedFileGroupRecordBuffer.java 84.68% <100.00%> (ø)
...hudi/SparkFileFormatInternalRowReaderContext.scala 75.79% <77.77%> (+0.91%) ⬆️

... and 36 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.
@voonhous
voonhous merged commit d4f9eb9 into apache:master Aug 28, 2026
41 of 44 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M PR with lines of changes in (100, 300]

5 participants