Skip to content

feat(utilities): select more than one file extension in cloud incremental sources - #19672

Open
rahil-c wants to merge 2 commits into
apache:masterfrom
rahil-c:eng-47287-pr1-multi-ext
Open

feat(utilities): select more than one file extension in cloud incremental sources#19672
rahil-c wants to merge 2 commits into
apache:masterfrom
rahil-c:eng-47287-pr1-multi-ext

Conversation

@rahil-c

@rahil-c rahil-c commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Describe the issue this Pull Request addresses

closes #19671

hoodie.streamer.source.cloud.data.select.file.extension is interpolated into a single SQL predicate, so a bucket holding more than one file type cannot be ingested by one streamer. Setting a list looks like it should work and fails quietly: select.file.extension=json,jsonl generates s3.object.key like '%json,jsonl', a literal match on the joined string that selects nothing, and the job then commits nothing without reporting an error.

Summary and Changelog

A comma separated value now renders one like predicate per extension, OR'd inside parentheses so it composes correctly with the size and relative-path predicates. A single value renders exactly the predicate it did before, and the fallback to the data file format when the config is unset is unchanged. generateFilter had no test coverage, so six cases were added to the existing suite: single and multiple extensions, blank and empty list entries, the format fallback, the GCS column names, and composition with the path prefix filter.

Impact

Additive and backwards compatible. Existing single-extension pipelines produce a byte-identical filter, and the config keeps its current default. Users with mixed-extension buckets can now select them in one pipeline instead of running one per extension.

Risk Level

low. The change is confined to rendering one config value into a filter string, and the previously untested single-value and format-fallback behaviours are now pinned by tests. Verified by running the new cases against the unmodified code first, where the four multiple-extension cases fail as expected (expected: ...like '%pdf' or ...like '%docx' but was: ...like '%pdf,docx') while the two backwards-compatibility cases pass both before and after. TestCloudObjectsSelectorCommon (27), TestS3EventsHoodieIncrSource (16), TestGcsEventsHoodieIncrSource (11), TestCloudObjectsSelector (50) and TestDeprecatedCloudIngestionConfigs (3) all pass, with checkstyle clean.

Documentation Update

The config documentation on CLOUD_DATAFILE_EXTENSION is updated in this PR and the reference tables generate from it, so no separate website change is needed.

Contributor's checklist

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

hoodie.streamer.source.cloud.data.select.file.extension was interpolated into a
single SQL predicate, so a comma separated value produced
`s3.object.key like '%json,jsonl'`: a literal match on the joined string that
selects nothing, with no error to explain it.

A comma separated value now renders one predicate per extension, combined with
OR inside parentheses so it composes with the size and path filters. A single
value renders exactly the predicate it did before, so existing pipelines are
unaffected, as is the long standing fallback to the data file format when the
config is unset.

generateFilter had no test coverage. Six cases were added to the existing suite,
covering the single and multiple forms, blank and empty entries, the format
fallback, the GCS column names, and composition with the path prefix filter.
@github-actions github-actions Bot added the size:S PR with lines of changes in (10, 100] label Aug 19, 2026
@codecov-commenter

codecov-commenter commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.86%. Comparing base (5a11a4f) to head (4f57edf).
⚠️ Report is 66 commits behind head on master.

Files with missing lines Patch % Lines
...es/sources/helpers/CloudObjectsSelectorCommon.java 81.81% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19672      +/-   ##
============================================
+ Coverage     77.83%   77.86%   +0.02%     
- Complexity    33140    33195      +55     
============================================
  Files          2527     2532       +5     
  Lines        139942   140066     +124     
  Branches      16849    16908      +59     
============================================
+ Hits         108926   109064     +138     
+ Misses        23396    23381      -15     
- Partials       7620     7621       +1     
Components Coverage Δ
hudi-common 83.35% <ø> (+<0.01%) ⬆️
hudi-client 82.95% <ø> (+0.12%) ⬆️
hudi-flink 85.71% <ø> (-0.04%) ⬇️
hudi-spark-datasource 72.29% <ø> (+0.01%) ⬆️
hudi-utilities 74.03% <83.33%> (-0.01%) ⬇️
hudi-cli 15.06% <ø> (ø)
hudi-hadoop 69.08% <ø> (-0.02%) ⬇️
hudi-sync 75.58% <ø> (ø)
hudi-io 79.85% <ø> (ø)
hudi-timeline-service 83.44% <ø> (ø)
hudi-cloud 64.33% <ø> (ø)
hudi-kafka-connect 53.20% <ø> (ø)
Flag Coverage Δ
common-and-other-modules 50.97% <83.33%> (-0.02%) ⬇️
flink-integration-tests 49.14% <ø> (+<0.01%) ⬆️
hadoop-mr-java-client 43.91% <ø> (-0.01%) ⬇️
integration-tests 13.63% <0.00%> (-0.01%) ⬇️
spark-client-hadoop-common 50.56% <ø> (-0.07%) ⬇️
spark-java-tests 52.03% <0.00%> (+0.09%) ⬆️
spark-scala-tests 46.44% <0.00%> (-0.01%) ⬇️
utilities 36.59% <8.33%> (-0.02%) ⬇️

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

Files with missing lines Coverage Δ
...pache/hudi/utilities/config/CloudSourceConfig.java 99.27% <100.00%> (ø)
...es/sources/helpers/CloudObjectsSelectorCommon.java 79.71% <81.81%> (+0.01%) ⬆️

... and 34 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.
@rahil-c

rahil-c commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

The Azure failure here looks unrelated to this change. The failing phase is UT spark-datasource DDL & Others, and the recorded issue is an infrastructure one:

Free memory is lower than 5%; Currently used: 95.76%

This PR only touches CloudSourceConfig, CloudObjectsSelectorCommon and its test, all in hudi-utilities, so it cannot reach the Scala spark-datasource DDL suite. Needs a re-run rather than a fix.

Reviewed the change itself as part of a pass over the stack and found nothing to raise: extensionPredicate renders a byte-identical filter to the previous code for the single-extension case, so existing sources see no behaviour change, and the multi-extension and trimming cases are covered by tests.

.withAlternatives(DELTA_STREAMER_CONFIG_PREFIX + "source.cloud.data.select.file.extension")
.markAdvanced()
.withDocumentation("Only match files with this extension. By default, this is the same as hoodie.streamer.source.hoodieincr.file.format");
.withDocumentation("Only match files with this extension. Accepts a comma separated list, "

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

is this true did you check to see this matches behavior of "hoodie.streamer.source.hoodieincr.file.format"?

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.

🤖 Traced it — the doc is close but slightly imprecise. When CLOUD_DATAFILE_EXTENSION is unset, CloudObjectsSelectorCommon falls back to fileFormat = CloudDataFetcher.getFileFormat(props) (line 306). getFileFormat returns DATAFILE_FORMAT (...cloud.data.datafile.format) when that's set, and only falls back to SOURCE_FILE_FORMAT (hoodie.streamer.source.hoodieincr.file.format, default parquet) when it isn't. So the extension default really tracks the effective data file format, which equals hoodieincr.file.format only when cloud.data.datafile.format is also unset. Might be worth wording the doc as "defaults to the resolved data file format" to capture that.

@rahil-c
rahil-c marked this pull request as ready for review August 21, 2026 23:20
@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

@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! The PR makes hoodie.streamer.source.cloud.data.select.file.extension accept a comma-separated list, rendering one OR'd like predicate per extension so mixed-extension buckets can be ingested in a single sync. The SQL precedence handling and the single-value/format-fallback backwards compatibility look correct and are now well covered by tests. One edge-case around all-blank list entries is worth double-checking in the inline comment. Please take a look, and this should be ready for a Hudi committer or PMC member to take it from here.

.filter(extension -> !extension.isEmpty())
.map(extension -> String.format("%s like '%%%s'", objectKey, extension))
.collect(Collectors.toList());
if (predicates.isEmpty()) {

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.

🤖 When the config is set but every entry trims to empty (e.g. " , " or ,), extensionClause returns "" so no extension predicate is emitted at all — and because getPropVal already returned present, the fallback to the data file format is skipped too. The net effect is that objects of every extension get selected. Is that the intended behavior for a fully-blank list, or would falling back to the default format be safer here so a misconfiguration doesn't silently ingest non-data files?

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

.withAlternatives(DELTA_STREAMER_CONFIG_PREFIX + "source.cloud.data.select.file.extension")
.markAdvanced()
.withDocumentation("Only match files with this extension. By default, this is the same as hoodie.streamer.source.hoodieincr.file.format");
.withDocumentation("Only match files with this extension. Accepts a comma separated list, "

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.

🤖 Traced it — the doc is close but slightly imprecise. When CLOUD_DATAFILE_EXTENSION is unset, CloudObjectsSelectorCommon falls back to fileFormat = CloudDataFetcher.getFileFormat(props) (line 306). getFileFormat returns DATAFILE_FORMAT (...cloud.data.datafile.format) when that's set, and only falls back to SOURCE_FILE_FORMAT (hoodie.streamer.source.hoodieincr.file.format, default parquet) when it isn't. So the extension default really tracks the effective data file format, which equals hoodieincr.file.format only when cloud.data.datafile.format is also unset. Might be worth wording the doc as "defaults to the resolved data file format" to capture that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S PR with lines of changes in (10, 100]

4 participants