Skip to content

fix(trino): skip predicate pushdown on type-evolved parquet columns - #19467

Open
wombatu-kun wants to merge 2 commits into
apache:masterfrom
wombatu-kun:issue/19457-trino-evolved-column-predicate
Open

fix(trino): skip predicate pushdown on type-evolved parquet columns#19467
wombatu-kun wants to merge 2 commits into
apache:masterfrom
wombatu-kun:issue/19457-trino-evolved-column-predicate

Conversation

@wombatu-kun

Copy link
Copy Markdown
Contributor

Describe the issue this Pull Request addresses

Closes #19457

A predicate on a column whose type was widened after a base file was written fails the query, for as long as any base file still stores the original physical type:

Malformed Parquet file. Corrupted statistics for column "[c7] optional float c7"
Caused by: java.lang.ClassCastException: class java.lang.Float cannot be cast to class java.lang.Double

TupleDomainParquetPredicate.getDomain picks its branch from the pushed-down domain's type and reads the parquet statistics as that type. The domain's type comes from the metastore, the statistics come from the file, and a type evolution is what makes the two disagree. Reading such a column always worked; only the statistics side was blind.

This is schema-on-write, not schema-on-read: HoodieSchemaCompatibilityChecker.getCompatibility permits double <- float, float <- int|long, long <- int and string <- any numeric, and hoodie.avro.schema.validate defaults to false.

Summary and Changelog

The connector now leaves a domain out of the parquet predicate when the file's physical type cannot answer it. The engine still applies that predicate above the scan, so failing queries return their rows, and the only loss is row group pruning on the evolved column, for files written before the evolution.

  • ParquetStatisticsDomains (new): hasComparableStatistics mirrors getDomain's dispatch, dropIncomparableDomains filters the descriptor-keyed tuple domain. Filtering on the descriptor rather than on the handle is what stops the check and the evaluation disagreeing about which column and type is meant, and covers both hudi.parquet.use-column-names modes in one pass.
  • HudiPageSourceProvider.createPageSource: wraps the existing getParquetTupleDomain call, which feeds both buildPredicate and getFilteredRowGroups.
  • Dropping is sound because HudiMetadata.applyFilter returns the whole regular predicate as the remaining filter, so pushdown here is an optimization only. It is the trade remapPredicateColumnIndicesToPhysical and HudiColumnStatsIndexSupport.getDomainFromColumnStats already make.
  • Two rejected pairs are silent wrong answers rather than crashes, a decimal column read as varchar and a string column read as decimal, which is why this is a type check and not a try/catch.

Tests, where hudi-trino had no schema evolution coverage at all: TestParquetStatisticsDomains (30 type pairs, each checked against the real getDomain), TestHudiEvolvedColumnPredicates (reads a pre-evolution base file through createPageSource), and TestHudiSchemaEvolutionPredicates with its positional subclass at SQL level. Both column-resolution modes throughout.

Impact

Queries with a predicate on a type-evolved column succeed instead of failing with HUDI_BAD_DATA. Pruning is unchanged everywhere else, including int -> long, which the statistics can answer and the guard keeps. Pushdown is only enabled for base-file-only splits, so the merge path is untouched. No config or public API change.

Risk Level

low

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

@codecov-commenter

codecov-commenter commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.40%. Comparing base (0db5246) to head (cd48bb1).
⚠️ Report is 4 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19467      +/-   ##
============================================
+ Coverage     76.39%   76.40%   +0.01%     
- Complexity    32101    32109       +8     
============================================
  Files          2520     2520              
  Lines        138985   138995      +10     
  Branches      16695    16699       +4     
============================================
+ Hits         106171   106195      +24     
+ Misses        25178    25168      -10     
+ Partials       7636     7632       -4     
Components Coverage Δ
hudi-common 82.36% <ø> (+0.02%) ⬆️
hudi-client 81.96% <ø> (-0.02%) ⬇️
hudi-flink 83.99% <ø> (+0.06%) ⬆️
hudi-spark-datasource 70.62% <ø> (+<0.01%) ⬆️
hudi-utilities 73.67% <ø> (+0.03%) ⬆️
hudi-cli 15.32% <ø> (ø)
hudi-hadoop 63.50% <ø> (+0.01%) ⬆️
hudi-sync 70.92% <ø> (-0.05%) ⬇️
hudi-io 79.36% <ø> (-0.10%) ⬇️
hudi-timeline-service 83.44% <ø> (ø)
hudi-cloud 64.06% <ø> (ø)
hudi-kafka-connect 53.20% <ø> (ø)
Flag Coverage Δ
common-and-other-modules 49.57% <ø> (+0.01%) ⬆️
flink-integration-tests 48.81% <ø> (+0.01%) ⬆️
hadoop-mr-java-client 43.76% <ø> (-0.01%) ⬇️
integration-tests 13.58% <ø> (-0.01%) ⬇️
spark-client-hadoop-common 49.63% <ø> (ø)
spark-java-tests 51.23% <ø> (+<0.01%) ⬆️
spark-scala-tests 45.99% <ø> (ø)
utilities 36.60% <ø> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.
see 19 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.

@github-actions github-actions Bot added the size:XL PR with lines of changes > 1000 label Aug 3, 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 fixes a Trino query failure (#19457) where a predicate on a type-widened column crashed the whole split, by dropping domains from the parquet statistics pushdown when the file's physical type cannot answer them, while the engine still re-applies the predicate above the scan. I verified the load-bearing soundness assumption directly in the code — HudiMetadata.applyFilter returns the full regular predicate as the unenforced remaining filter, so dropping a domain only costs row-group pruning and never a row — and the new guard is pinned against the real TupleDomainParquetPredicate.getDomain in unit tests. 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.
. Code looks clean overall — one minor readability nit below.

cc @yihua

return Types.primitive(primitiveTypeName, OPTIONAL).as(annotation).named("c");
}

private static PrimitiveType decimal(PrimitiveTypeName primitiveTypeName, int precision, int scale)

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.

🤖 nit: the INT96 branch in plain() returns exactly the same expression as the fallthrough, so it doesn't change behaviour but does invite a reader to wonder whether INT96 needs special treatment here. Could you remove it, leaving only the FIXED_LEN_BYTE_ARRAY guard and the default?

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done cd48bb1

@voonhous

voonhous commented Aug 3, 2026

Copy link
Copy Markdown
Member

I'll hold off on review for this until the E2E branch is merged in. I'm trying to think of a way where we can clearly segregate the schema-on-write and schema-on-read tests, while also testing this E2E with a Spark-write and a Trino-read.

@wombatu-kun
wombatu-kun force-pushed the issue/19457-trino-evolved-column-predicate branch from 0b33546 to dac69a5 Compare August 6, 2026 02:34

@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 adds a guard (ParquetStatisticsDomains.hasComparableStatistics / dropIncomparableDomains) that keeps a metastore-typed pushdown domain from being matched against parquet statistics of a column still physically stored under its pre-evolution type, which previously failed queries with Corrupted statistics for column .... I verified the guard branch-by-branch against Trino 481's actual TupleDomainParquetPredicate.getDomain dispatch (extracted from the trino-parquet sources): every kept (domainType, fileType) pair is one getDomain reads correctly without throwing (including the long-decimal-over-INT32/INT64 rescale path, which uses asLong and does not throw), and every dropped pair is one getDomain would either throw or misprune on — so there are no false positives and no pruning regression on unevolved data. 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 dead-code branch in a test helper worth cleaning up; otherwise the code is clean and well-documented.

cc @yihua


private static PrimitiveType plain(PrimitiveTypeName primitiveTypeName)
{
if (primitiveTypeName == INT96) {

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.

🤖 nit: the INT96 branch returns the same expression as the fallthrough, so it's dead code — could you remove it? A future reader will wonder what was supposed to be different about INT96.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done cd48bb1

@wombatu-kun

Copy link
Copy Markdown
Contributor Author

@voonhous #19217 is merged now and this branch is rebased on top of it. Could you take a look when you get a chance?

@hudi-bot

hudi-bot commented Aug 6, 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

@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 adds a connector-side guard (ParquetStatisticsDomains.dropIncomparableDomains) that removes pushed-down domains whose metastore type can't be compared against a base file's pre-evolution physical statistics, fixing the Corrupted statistics/ClassCastException failure on type-evolved columns. The change is correctness-safe by construction: it only ever returns a subset of the existing parquet tuple domain, so it can never prune more row groups or introduce new data loss, and the hasComparableStatistics dispatch is pinned against the real TupleDomainParquetPredicate.getDomain for the risky pairs. No issues flagged from this automated pass — a Hudi committer or PMC member can take it from here for a final review.

cc @yihua

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.

[BUG] hudi-trino: pushed-down predicate on a float -> double evolved column fails the query

5 participants