Skip to content

feat(trino): resolve merge-required columns from the table schema - #19288

Merged
voonhous merged 7 commits into
apache:masterfrom
voonhous:19249--resolve-required-columns-from-table-schema
Jul 29, 2026
Merged

feat(trino): resolve merge-required columns from the table schema#19288
voonhous merged 7 commits into
apache:masterfrom
voonhous:19249--resolve-required-columns-from-table-schema

Conversation

@voonhous

@voonhous voonhous commented Jul 14, 2026

Copy link
Copy Markdown
Member

Follow-up to the RFC-105 migration (#18837, merged); rebased on master.

Describe the issue this Pull Request addresses

Closes #19249

The file-group reader can require columns the query projection does not carry: meta columns like _hoodie_commit_time, delete markers, or the full table schema when a CUSTOM merger is not projection compatible. The connector hand-built a VARCHAR handle for the meta-column case and threw NOT_SUPPORTED for everything else, so non-projection-compatible mergers could not be used at all.

Summary and Changelog

  • Merge-required columns are now typed from the table schema's avro types (HudiUtil.toColumnHandle) instead of being synthesized as VARCHAR. The HOODIE_META_COLUMNS guard and the NOT_SUPPORTED error are gone.
  • When the resolved merger is not projection compatible, the read projection expands to the full table schema so the base and log reads both carry every merge column. The decision mirrors FileGroupReaderSchemaHandler.generateRequiredSchema, including the pre-v9 merge-config inference (a 0.x table with a custom payload class infers CUSTOM mode with no persisted strategy id).
  • The base read also requests the remaining merge-mandatory columns (_hoodie_is_deleted, _hoodie_operation, the custom delete key, record-key fields when hoodie.populate.meta.fields=false), and the reader context now fails loudly if the base projection ever misses a column the file-group reader demands, instead of merging with nulls.
  • SynthesizedColumnHandler is replaced by PrefilledColumnValues, a thin adapter over trino-hive's getPrefilledColumnValue. This intentionally goes past the issue's "no behavioral change" acceptance: $partition renders keys in partition order instead of hash order, the \N null marker is handled, more partition-key types work, and $file_modified_time renders in the JVM default zone as the Hive connector does instead of the previous pinned UTC (same instant, different rendered zone).
  • MOR splits with log files but no base file fail with a clear NOT_SUPPORTED error instead of a bare NoSuchElementException.
  • Under hudi.parquet.use-column-names=false, a projected column absent from a base file (schema-on-write evolution never rewrites old files) now null-fills exactly like the name-based path, instead of failing hard (previously an autoboxing NPE). Column renames under full schema evolution (hoodie.schema.on.read.enable) stay out of scope on both paths: [FEATURE] hudi-trino: resolve parquet columns through the internal schema for full schema evolution (hoodie.schema.on.read.enable) #19381.
  • A table that resolves to CUSTOM merge mode without a persisted hoodie.record.merge.strategy.id (possible at table version 8 or later) fails with an actionable HUDI_BAD_DATA error instead of an NPE inside hudi-common's merger resolution.
  • Merge-required columns the metastore does not carry but the Avro table schema does (e.g. hive sync with omit_metadata_fields=true) are recovered on the merge read path from the already-resolved table schema -- including columns a CUSTOM merger declares mandatory, by asking the same resolved merger -- at no extra I/O (pinned by the file-operation-count tests).

New tests: TestHudiNonProjectionCompatibleMerger runs narrow-projection MOR queries that only merge correctly if both the base and log side supply the never-projected merge_rank column (one key wins on each side); unit coverage for the new helpers and for the pre-v9 inference at table versions 6, 8 and 9. Review rounds added: a payload-class-only table version 6 fixture read end to end, fixtures whose metastore omits a merge-required column (the ordering field, and a merger-declared column) pinning the schema recovery end to end, and a shared initializer base (AbstractMergerHudiTablesInitializer) replacing three near-identical fixture copies.

Impact

Custom mergers no longer have to override isProjectionCompatible() / getMandatoryFieldsForMerging(). Note that payload-class tables resolve HoodieAvroRecordMerger, which is not projection compatible, so such tables read the full table schema on MOR splits with log files -- same as Spark, and inherent to payload-based merging. Projection-compatible and base-file-only reads are unchanged.

Risk Level

low -- confined to hudi-trino; the projection-compatible fast path keeps its existing test coverage.

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 marked this pull request as draft July 14, 2026 15:24
@github-actions github-actions Bot added the size:XL PR with lines of changes > 1000 label Jul 14, 2026
@voonhous voonhous changed the title 19249 resolve required columns from table schema feat(trino): resolve merge-required columns from the table schema Jul 14, 2026
@voonhous
voonhous requested a review from yihua July 14, 2026 16:25
@voonhous
voonhous force-pushed the 19249--resolve-required-columns-from-table-schema branch from 6f089ae to f6a1bc0 Compare July 15, 2026 03:35
@voonhous
voonhous force-pushed the 19249--resolve-required-columns-from-table-schema branch from f6a1bc0 to 74e1c97 Compare July 15, 2026 16:53
Fixes apache#19249, including the scope update in issue comment
4937143911; relates to the base read path of apache#18837.

The file-group reader can require merge columns the query projection does
not carry: Hudi meta fields like _hoodie_commit_time, delete markers,
record-key data columns, a custom merger's mandatory fields, or the whole
table schema when a CUSTOM merger is not projection compatible.
buildRequiredColumnHandles previously hand-synthesized a
HIVE_STRING/VARCHAR handle behind a HOODIE_META_COLUMNS guard and threw
NOT_SUPPORTED for anything else, and the base-file read silently returned
projection-only records that made such mergers see nulls.

Log side:
- Every required-schema field carries its real Avro type from the table
  schema, so unresolved handles are typed directly from the field
  (NativeLogicalTypesAvroTypeBlockHandler + HiveTypeTranslator) via the
  new HudiUtil.toColumnHandle; the synthesis, guard and exception are
  gone. trino-hive-formats moves from runtime to compile scope.

Base side:
- HudiPageSourceProvider mirrors the file-group reader's decision:
  HudiUtil.resolveMergeModeAndStrategyId applies the same version-gated
  inference as hudi-common (merge mode below table version 9, strategy id
  below version 8), and when the resolved CUSTOM merger is not projection
  compatible (interface default; also every payload-based table, since
  the payload strategy resolves HoodieAvroRecordMerger), the read
  projection expands to every table-schema column via
  HudiUtil.appendMissingSchemaColumns. Matches Spark; the extra I/O is
  inherent to such mergers.
- getMergeRequiredColumnHandles predicts every column
  FileGroupReaderSchemaHandler.getMandatoryFieldsForMerging can demand:
  ordering columns under the inferred (not raw) merge mode,
  _hoodie_is_deleted and _hoodie_operation unconditionally (dropped when
  not table columns), the custom delete key when both
  hoodie.payload.delete.field and .marker are set, and the record-key
  data columns when hoodie.populate.meta.fields is false.
- The base read reuses the pre-built page source, so its projection MUST
  cover requiredSchema: HudiTrinoReaderContext now guards that invariant
  and fails with HUDI_SCHEMA_ERROR naming any missing columns instead of
  merging with nulls. MOR splits with log files but no base file fail
  with a clear NOT_SUPPORTED instead of a bare NoSuchElementException.

Cleanups in the same area:
- Replace SynthesizedColumnHandler with PrefilledColumnValues, a thin
  per-split adapter over trino-hive's HiveUtil.getPrefilledColumnValue.
  Fixes $partition rendering multi-key partitions in hash order, handles
  the hive null marker (backslash-N) in partition values, adds previously
  unsupported partition-key types, and deletes SynthesizedColumnStrategy
  plus the hand-rolled appendPartitionKey dispatch.
- Remove dead code (HudiTrinoReaderContext.colToPosMap and dataHandles,
  HudiPageSource.readerContext); make fields private final.
- remapColumnIndicesToPhysical throws a diagnosable TrinoException
  instead of an autoboxing NPE for columns absent from the parquet file
  schema, and lowercases with Locale.ROOT.

Tests:
- TestHudiNonProjectionCompatibleMerger + NonProjectionCompatibleRankMerger
  (no isProjectionCompatible/getMandatoryFieldsForMerging overrides) +
  initializer: narrow-projection MOR queries that never project
  merge_rank, laid out so one key's winning rank is on the log record and
  the other's on the base record; sum(value)=199 discriminates the
  correct merge from base-only (110) and newest-wins (103).
- Unit coverage: toColumnHandle, appendMissingSchemaColumns,
  usesNonProjectionCompatibleMerger, resolveMergeModeAndStrategyId
  (table versions 6/8/9), merge-required column prediction,
  PrefilledColumnValues (incl. the $partition ordering fix), and the
  typed missing-column remap error (both remap tests).
- Existing projection-compatible merger tests stay as fast-path
  regression coverage; stale MaxRankRecordMerger javadoc refreshed.
@voonhous
voonhous force-pushed the 19249--resolve-required-columns-from-table-schema branch from 74e1c97 to 6569ea3 Compare July 27, 2026 11:22
@voonhous
voonhous marked this pull request as ready for review July 27, 2026 11:22
voonhous added 2 commits July 27, 2026 20:51
TestHudiCustomMerger's negative test was added on master while this
change was in flight, pinning the old NOT_SUPPORTED rejection per the
plan in issue comment 4937143911. Flip it to the positive assertions
that plan called for: narrow-projection sum(value)=199 plus a full-row
check under NonProjectionCompatibleTestRecordMerger, whose javadoc is
refreshed to describe the full-schema path.
@voonhous

Copy link
Copy Markdown
Member Author

@wombatu-kun Can you please help to review this? I have a few more PRs to get the Trino integrations landed in a good shape.

I'd like to get this track of work done by this week.

Thank you!

@voonhous
voonhous requested a review from wombatu-kun July 27, 2026 13:28
Comment thread hudi-trino/src/main/java/io/trino/plugin/hudi/HudiPageSourceProvider.java Outdated
Comment thread hudi-trino/src/main/java/io/trino/plugin/hudi/HudiPageSourceProvider.java Outdated
Comment thread hudi-trino/src/main/java/io/trino/plugin/hudi/HudiUtil.java Outdated
Comment thread hudi-trino/src/main/java/io/trino/plugin/hudi/HudiUtil.java
- soften requiresFullSchemaRead javadoc to the actual guarantee
- remap missing columns one past the last field so the parquet reader
  null-fills them like the name-based path (was a typed hard failure)
- reject CUSTOM merge mode without a persisted strategy id with an
  actionable HUDI_BAD_DATA error instead of an NPE in hudi-common
- recover merge-required columns the metastore lacks from the resolved
  table schema on the merge path (appendMissingMergeRequiredColumns);
  no extra I/O, pinned by the file-operation-count tests
- document the $file_modified_time zone change in PrefilledColumnValues
- extract AbstractMergerHudiTablesInitializer; the three MoR fixture
  initializers keep only their deltas
- add payload-only table version 6 fixture (RankBasedTestPayload) and
  E2E coverage in TestHudiNonProjectionCompatibleMerger
Comment thread hudi-trino/src/main/java/io/trino/plugin/hudi/HudiPageSourceProvider.java Outdated
Comment thread hudi-trino/src/main/java/io/trino/plugin/hudi/HudiUtil.java Outdated
Comment thread hudi-trino/src/main/java/io/trino/plugin/hudi/HudiUtil.java Outdated
Comment thread hudi-trino/src/test/java/io/trino/plugin/hudi/TestHudiMergeRequiredColumns.java Outdated
voonhous added 2 commits July 28, 2026 12:38
New EVENT_TIME_ORDERING MOR fixture (omitted_ordering_field_mor) whose
metastore column list omits the ts its Avro schema carries, the shape
hive sync leaves when a column is not synced. Event-time merging needs
ts on both sides, so the row-level and narrow-projection sum(value)=199
cases only pass when the merge path recovers the column from the table
schema; reverting the recovery line fails both with the missing-column
guard error.
- recover a CUSTOM merger's own mandatory fields on the merge path too:
  appendMissingMergeRequiredColumns asks the same resolved merger the
  file-group reader will use (classloading only, no I/O), unit-tested
  with MaxRankRecordMerger and pinned end to end by a fixture whose
  metastore omits the merger-declared merge_rank column
- widen the no-strategy-id guard wording to version 8 or later
- scope the schema-gate javadoc claims to _hoodie_is_deleted and
  _hoodie_operation, the two fields the reader actually schema-gates,
  and split the metastore/schema halves across the functions that
  implement them
- move the withPreCombineField constraint comment back into the write
  config builder chain
- document that the composite initializer cannot host fixtures that
  keep their write client open
- strip only the merge strategy id in the payload-only v6 fixture: the
  merge mode never reaches disk (its config is since-version 1.0.0,
  dropped by table creation itself)
Comment thread hudi-trino/src/main/java/io/trino/plugin/hudi/HudiUtil.java
…dColumns

Mirror the two usesNonProjectionCompatibleMerger cases onto the append:
CUSTOM without a strategy id and CUSTOM with an id no configured merger
declares both fall back to the table-config-derived names, so removing
either guard fails a unit test instead of resurfacing the NPE. The
unresolvable case must not use the all-zeros uuid -- that is
PAYLOAD_BASED_MERGE_STRATEGY_UUID, which always resolves; the first
version of the test passed vacuously for exactly that reason until a
mutation check caught it. Shared fixture helpers dedup the CUSTOM tests.
@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

@voonhous

Copy link
Copy Markdown
Member Author

@wombatu-kun Can i request for another round of review please? For the comments addressed and if you have no further comments, can you please help to "resolve" them so they are collapsed?

@wombatu-kun wombatu-kun 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.

Verified the CUSTOM-branch guards are pinned: without the isNullOrEmpty half the null-id test does reach createValidRecordMerger's leading equals(), and the strategy id the unresolvable-merger test uses is neither the payload-based uuid nor one any configured merger declares, so neither test is vacuous. No further comments from me - all my threads are resolved.

@voonhous
voonhous merged commit 3b146ce into apache:master Jul 29, 2026
75 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL PR with lines of changes > 1000

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] hudi-trino: resolve native-log required columns from the full table schema instead of synthesizing meta-column handles

3 participants