Skip to content

fix(spark): make slash-separated date partitioning work on the row writer path - #19648

Open
SEPURI-SAI-KRISHNA wants to merge 3 commits into
apache:masterfrom
SEPURI-SAI-KRISHNA:fix-slash-separated-date-partitioning-row-writer
Open

fix(spark): make slash-separated date partitioning work on the row writer path#19648
SEPURI-SAI-KRISHNA wants to merge 3 commits into
apache:masterfrom
SEPURI-SAI-KRISHNA:fix-slash-separated-date-partitioning-row-writer

Conversation

@SEPURI-SAI-KRISHNA

@SEPURI-SAI-KRISHNA SEPURI-SAI-KRISHNA commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Describe the issue this Pull Request addresses

Slash-separated date partitioning did not work on the Spark row-writer path. The single-field
fast path in PartitionPathFormatterBase#combine cast the value to String before substituting
dashes, so the UTF8String formatter backing the InternalRow write path threw a
ClassCastException, and a null partition value threw a NullPointerException on both string
flavors because the branch bypassed handleEmpty.

Summary and Changelog

  • PartitionPathFormatterBase#combine now runs the single-field value through handleEmpty and
    tryEncode like every other branch, and delegates the substitution to a new
    replaceDashesWithSlashes implemented by each formatter on its own string representation --
    String#replace(char, char) and UTF8String#replace -- instead of casting.
  • The multi-field branch no longer applies the substitution, matching
    KeyGenUtils#getRecordPartitionPath, which guards it on a single partition field.
  • Tests: a new TestPartitionPathFormatter asserting both formatters agree across
    slash/encode/hive-style/null combinations; keygen-level Row and InternalRow asserts in
    TestSimpleKeyGenerator and TestCustomKeyGenerator; and row-writer plus DATE-typed partition
    column cases in TestSlashSeparatedPartitionValue.

No code was copied.

Impact

Records with a null or empty date partition value, and every record written through the row
writer, no longer fail on a slash-partitioned table.

Three consumers of the String formatter now agree with what the Avro writer has always
produced, rather than changing any layout:

  • SparkHoodieTableFileIndex#composeRelativePartitionPath (query-side prefix pruning) and
    RowRecordKeyExtractor (bucket routing) previously skipped handleEmpty and tryEncode on the
    single-field slash path, while KeyGenUtils always applied both.
  • No on-disk layout changes for existing tables: every slash partition directory in existence was
    written by the Avro path, since the row-writer path could not produce one at all.
  • The multi-field change lands in prefix pruning and bucket routing, not in any path that names a
    directory.

Scope is Spark-only -- no Flink or Java-client code reads
hoodie.datasource.write.slash.separated.date.partitioning.

No public API, config or storage-format change.

Risk Level

low

The formatter is exercised by the new unit tests across both string flavors, and the SQL-level
suite covers insert, bulk_insert, null values and DATE partition columns. Follow-ups filed during
review for the configurations this PR deliberately does not change: #19666 (multi-field),
#19667 (TIMESTAMP), #19668 (hive sync of the default-partition directory) and #19669
(hive-style plus slash outside SQL DDL).

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

@github-actions github-actions Bot added the size:M PR with lines of changes in (100, 300] label Aug 17, 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! The PR fixes slash-separated date partitioning on the Spark row-writer (InternalRow/UTF8String) path by replacing the hard (String) cast with an abstract replaceDashesWithSlashes, and now routes single-field values through handleEmpty/tryEncode so nulls land in __HIVE_DEFAULT_PARTITION__ and encoding is preserved. The core logic and the new tests look sound; I traced the encoding-before-slash ordering and the multi-field behavior and they line up with the Avro path. One cross-path consistency corner (hive-style + slash) 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. One small naming nit in the new test helper; otherwise the refactoring is clean and well-commented.

@SEPURI-SAI-KRISHNA

SEPURI-SAI-KRISHNA commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

CI: the only failure is TestValidateHoodieSyncProcedure > "sync_validate record-count modes fail fast on an unreachable HiveServer2", which fails identically on #19646 (a completely unrelated change) in the same phase, alongside Free memory is lower than 5% warnings. Looks pre-existing/flaky on master rather than related to this PR. The tests added here all pass: TestSlashSeparatedPartitionValue ran 3/3 green in build 16339, including the new row-writer case.

@SEPURI-SAI-KRISHNA

Copy link
Copy Markdown
Contributor Author

Thanks — both addressed.

Renamed the test parameter unsafe -> useRowWriterPath.

On hive-style + slash-separated: the facts are right, but that combination is rejected at table creation, so it is unreachable rather than divergent —
HoodieCatalogTable#extraTableConfig has ValidationUtils.checkArgument(!(sqlOptions.contains(HIVE_STYLE_PARTITIONING_ENABLE.key) && sqlOptions.contains(SLASH_SEPARATED_DATE_PARTITIONING.key)), "Table configs cannot contain both ..."), matching the config docs ("Cannot be used together with hive-style
partitioning").

I also would not want to mirror the Avro behavior here: it applies replace(char, char) to the whole field=value string, so a partition field named e.g. order-date yields
order/date=2026/01/05 — an extra spurious directory level. Copying that would propagate a bug rather than remove a divergence.

Left the behavior as-is and added a comment on testHiveStylePartitioningTakesPrecedence recording that it pins pre-existing behavior for an unreachable combination, not a claim
about what that combination should produce.

@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 fixes slash-separated date partitioning on the Spark row-writer path by making the replaceDashesWithSlashes substitution operate on the concrete string representation (String/UTF8String) rather than hard-casting, and aligns the row-writer path with the Avro path by only slashing single-field partitions and routing through handleEmpty/tryEncode for null/empty and encoding. I traced the single-field and multi-field branches against KeyGenUtils.getRecordPartitionPath/getPartitionPath and the encode/slash ordering and null handling line up. The one remaining hive-style + slash + single-field divergence is already noted in the inline discussion. 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.

cc @yihua

@voonhous voonhous left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the formatter change against its callers (SimpleKeyGenerator/ComplexKeyGenerator/CustomKeyGenerator, SqlKeyGenerator, HoodieDatasetBulkInsertHelper, SparkHoodieTableFileIndex) and the Avro path in KeyGenUtils. The core fix is right: Avro = Row = InternalRow for every non-hive combination probed (single field, multi field, null/empty, a-b/c with encoding, CustomKeyGenerator per-field), no revert of #17787/#18195, and UTF8String.replace returns this on no match. Inline comments cover the rest.

One note for a follow-up issue rather than this PR: SlashEncodedDayPartitionValueExtractor (auto-inferred for slash tables by HoodieTableConfigUtils:110-112) throws on any path that is not 3 segments, so hive sync of a slash table containing the __HIVE_DEFAULT_PARTITION__ directory this test now creates will fail.

- assert the InternalRow path for a null partition value at keygen level.
  [[KeyGeneratorTestUtilities#getInternalRow]] builds a flat GenericInternalRow,
  so the nested "nested_col.prop1" value stays a Row and reading it back as a
  struct fails; the conversion goes through Spark's CatalystTypeConverters
  instead. A null on a top-level field is not an option either -- every other
  field of the example schema is non-nullable and HoodieUnsafeRowUtils rejects
  it before the formatter is reached.
- assert Row + InternalRow for CustomKeyGenerator, which builds one single-field
  sub-key-generator per partition field. The test partitioned on "timestamp",
  a long, which cannot survive the conversion into a Row, so it now uses the
  string-typed "ts_ms".
- pin the url-encoding behaviour: encoding runs before the substitution, so an
  already slash-separated value is escaped rather than turned into directories.
- cover a DATE typed partition column across insert and bulk_insert.
- project datestr in the row-writer assertion so the read-back and the null row
  are covered, and drop the deprecated bulk-insert configs for
  hoodie.spark.sql.insert.into.operation.
- drop two formatter assertions already pinned by TestComplexKeyGenerator.
- correct the NOTE on the single-field fast path: CustomKeyGenerator is not an
  exception to it, and getPartitionPath governs the single-field Avro case.
- correct the hive-style comment: the mutual exclusion is documented on
  SLASH_SEPARATED_DATE_PARTITIONING but only enforced by HoodieCatalogTable for
  SQL options, so df.write and HoodieStreamer still accept the combination.
@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

@codecov-commenter

codecov-commenter commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.83%. Comparing base (5a11a4f) to head (8d52393).

Additional details and impacted files
@@            Coverage Diff            @@
##             master   #19648   +/-   ##
=========================================
  Coverage     77.83%   77.83%           
- Complexity    33140    33141    +1     
=========================================
  Files          2527     2527           
  Lines        139942   139940    -2     
  Branches      16849    16848    -1     
=========================================
  Hits         108926   108926           
- Misses        23396    23397    +1     
+ Partials       7620     7617    -3     
Components Coverage Δ
hudi-common 83.33% <ø> (-0.01%) ⬇️
hudi-client 82.85% <100.00%> (+0.02%) ⬆️
hudi-flink 85.75% <ø> (-0.01%) ⬇️
hudi-spark-datasource 72.28% <ø> (+<0.01%) ⬆️
hudi-utilities 74.03% <ø> (-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.99% <0.00%> (+<0.01%) ⬆️
flink-integration-tests 49.14% <ø> (+<0.01%) ⬆️
hadoop-mr-java-client 43.91% <ø> (-0.01%) ⬇️
integration-tests 13.64% <0.00%> (-0.01%) ⬇️
spark-client-hadoop-common 50.65% <100.00%> (+0.01%) ⬆️
spark-java-tests 51.94% <62.50%> (+<0.01%) ⬆️
spark-scala-tests 46.46% <87.50%> (+<0.01%) ⬆️
utilities 36.61% <37.50%> (-0.01%) ⬇️

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

Files with missing lines Coverage Δ
...apache/hudi/keygen/PartitionPathFormatterBase.java 95.45% <100.00%> (+20.45%) ⬆️
...ache/hudi/keygen/StringPartitionPathFormatter.java 100.00% <100.00%> (ø)
.../hudi/keygen/UTF8StringPartitionPathFormatter.java 100.00% <100.00%> (ø)

... and 12 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.

@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 fixes slash-separated date partitioning on the Spark row-writer path by routing the single-field value through handleEmpty/tryEncode and delegating dash→slash substitution to a per-formatter replaceDashesWithSlashes, and drops the substitution from the multi-field branch to match the Avro KeyGenUtils path. I traced the ordering parity against KeyGenUtils#getPartitionPath/getRecordPartitionPath, the CustomKeyGenerator per-field sub-keygen behavior, and the two formatter subclasses, and the change looks correct and consistent across the Avro, Row, and InternalRow paths. 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

@SEPURI-SAI-KRISHNA

Copy link
Copy Markdown
Contributor Author

Azure build 16390 is red on one unrelated test: TestValidateHoodieSyncProcedure > "Test Call sync_validate record-count modes fail fast on an unreachable HiveServer2" (TestValidateHoodieSyncProcedure.scala:250). It asserts a SQLException in the cause chain and got a NoClassDefFoundError instead, in a run where the agent logged "Free memory is lower than 5%; Currently used: 96.35%" three times — the Hive JDBC driver class failing to load under memory pressure rather than the connection failing as the test expects.

Everything else in that phase passed (527 succeeded, 1 failed), including all four TestSlashSeparatedPartitionValue cases this PR adds, and all 22 GitHub Actions checks are green. This PR touches only hudi-spark-client keygen classes and their tests — nothing in Hive sync, JDBC, or the procedures.

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.

5 participants