Skip to content

[lake/hudi] Use a clean lake table schema for Hudi lake storage - #4077

Merged
luoyuxia merged 4 commits into
apache:mainfrom
fhan688:Support-clean-and-legacy-Hudi-lake-table-schemas-
Aug 26, 2026
Merged

luoyuxia merged 4 commits into
apache:mainfrom
fhan688:Support-clean-and-legacy-Hudi-lake-table-schemas-

Conversation

@fhan688

@fhan688 fhan688 commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor

Purpose

Sub-task of the FIP-27 umbrella (#2411): Remove Mandatory System Columns From Fluss Lake Tables.

Until now, every Hudi lake table Fluss created was forced to carry three mandatory system columns (__bucket, __offset, __timestamp) as its last physical columns. They pollute the schema users see from Hudi and other engines.

This PR implements the Hudi part of FIP-27: newly created Hudi lake tables use a clean physical schema containing only user-defined columns.

Unlike Paimon (#3902) and Iceberg (#3903), the Hudi lake storage has never been exposed in any publicly released Fluss version, so there are no existing "legacy" Hudi tables that carry the three system columns. This PR therefore switches Hudi directly to the clean layout only, without any clean/legacy layout detection or legacy read/write compatibility. This keeps the Hudi implementation considerably simpler than the Paimon and Iceberg ones, which must support both layouts.

Brief change log

  • Create: HudiConversions builds the Hudi/Flink schema from user columns only; the three system columns are no longer appended. The name-conflict check is kept: user columns may not be named __bucket, __offset, or __timestamp (these names remain reserved for Fluss internal use).
  • Tiering writer: FlussRecordAsHudiRow writes the business columns only and validates that the Fluss record's field count matches the Hudi table field count. No system values are injected.
  • Reader / projection: HudiRecordReader selects the Hudi metadata columns and the projected business columns only, and emits a sentinel -1L log offset / timestamp, consistent with the Paimon and Iceberg readers and the LakeRecordRecordEmitter contract. Nested rows (HudiArrayAsFlussArray) and sorted primary-key reads (HudiSortedRecordReader) are aligned with the clean layout.
  • Compaction: unchanged. Hudi compaction merges delta log files into base files by file group and does not depend on the __bucket column, so it works for the clean layout without changes.

Tests

  • HudiLakeCatalogTest: create-table assertions expect tmns only).
  • FlussRecordAsHudiRowTest: verifies the clean writer odation.
  • HudiRecordReaderTest: verifies the reader strips Hudis the -1L offset/timestamp sentinel.
  • HudiRowAsFlussRowTest: uses a clean row fixture.
  • FlinkHudiTieringTestBase / HudiTieringITCase: read olumns only.
  • The full fluss-lake-hudi module test suite passes (90

API and Format

Storage format: Hudi lake tables created by Fluss containo trailing __bucket/__offset/__timestamp system
columns. No public Java API change.

Documentation

`website/docs/streaming-lakehouse/datalake-formats/hudi.mt the Hudi table schema contains only user columns, andthat the three system-column names remain reserved.

* {@code _hoodie_*} metadata columns, which are not part of the logical row type). A projected
* schema must not be passed to this method.
*/
public static boolean isLegacyTable(RowType rowType) {

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.

we don't need to consider compatiblity since hudi is never exposed to users in any public version.

Copilot AI 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.

Pull request overview

This PR implements the Hudi portion of FIP-27 in fluss-lake-hudi, introducing “clean” newly-created Hudi lake tables (user columns only) while retaining full read/write compatibility for existing legacy tables that still contain the trailing Fluss system columns (__bucket, __offset, __timestamp).

Changes:

  • Stop appending legacy system columns when creating new Hudi lake tables; add explicit legacy-schema construction for compatibility checks.
  • Add legacy-vs-clean layout detection and use it to drive tiering writer output and lake reader projection/trimming behavior.
  • Update unit/integration tests to cover clean/legacy layouts, re-enabling tiering on legacy tables, and sentinel -1L offset/timestamp behavior in readers.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
fluss-lake/fluss-lake-hudi/src/main/java/org/apache/fluss/lake/hudi/HudiLakeCatalog.java Detect legacy layout for existing tables and validate schema compatibility against the correct expected layout.
fluss-lake/fluss-lake-hudi/src/main/java/org/apache/fluss/lake/hudi/source/HudiRecordReader.java Drive projection/trimming based on legacy detection and emit sentinel -1L offset/timestamp.
fluss-lake/fluss-lake-hudi/src/main/java/org/apache/fluss/lake/hudi/source/HudiRowAsFlussRow.java Make system-column stripping conditional so clean tables don’t lose trailing fields.
fluss-lake/fluss-lake-hudi/src/main/java/org/apache/fluss/lake/hudi/source/HudiSortedRecordReader.java Treat only legacy system columns as skippable when mapping user-field positions.
fluss-lake/fluss-lake-hudi/src/main/java/org/apache/fluss/lake/hudi/tiering/writer/FlussRecordAsHudiRow.java Emit legacy system-column values only for legacy tables and validate business-field counts accordingly.
fluss-lake/fluss-lake-hudi/src/main/java/org/apache/fluss/lake/hudi/utils/HudiConversions.java Build clean vs legacy Flink/Hudi schemas and catalog tables; keep name-conflict checks for system column names.
fluss-lake/fluss-lake-hudi/src/main/java/org/apache/fluss/lake/hudi/utils/HudiUtils.java Introduce Hudi legacy-layout detection helper used by writer/reader code paths.
fluss-lake/fluss-lake-hudi/src/test/java/org/apache/fluss/lake/hudi/HudiLakeCatalogTest.java Add re-enable-tiering legacy-table test and update expected schema assertions for clean tables.
fluss-lake/fluss-lake-hudi/src/test/java/org/apache/fluss/lake/hudi/source/HudiRecordReaderTest.java Update reader assertions to sentinel -1L and add clean-table iterator coverage.
fluss-lake/fluss-lake-hudi/src/test/java/org/apache/fluss/lake/hudi/testutils/FlinkHudiTieringTestBase.java Adjust integration assertions to compare business columns only for clean tables.
fluss-lake/fluss-lake-hudi/src/test/java/org/apache/fluss/lake/hudi/tiering/HudiTieringITCase.java Update IT case to use revised clean-table row comparison helper.
fluss-lake/fluss-lake-hudi/src/test/java/org/apache/fluss/lake/hudi/tiering/writer/FlussRecordAsHudiRowTest.java Split writer tests into clean vs legacy cases and assert correct arity/system-column behavior.
fluss-lake/fluss-lake-hudi/src/test/java/org/apache/fluss/lake/hudi/utils/HudiUtilsTest.java Add tests for legacy detection with clean, legacy, and partial-name-match schemas.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +48 to +56
public static boolean isLegacyTable(RowType rowType) {
for (String systemColumn : LEGACY_SYSTEM_COLUMNS.keySet()) {
if (!rowType.getFieldNames().contains(systemColumn)) {
return false;
}
}
return true;
}
}
Comment on lines +214 to +221
@VisibleForTesting
boolean isLegacyTable(CatalogBaseTable existingTable) {
Set<String> columnNames = new HashSet<>();
for (ColumnSignature column : extractColumns(existingTable)) {
columnNames.add(column.name);
}
return columnNames.containsAll(LEGACY_SYSTEM_COLUMNS.keySet());
}

Copilot AI 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.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Suppressed comments (1)

fluss-lake/fluss-lake-hudi/src/main/java/org/apache/fluss/lake/hudi/source/HudiSortedRecordReader.java:173

  • resolveRecordKeyInfo currently counts every physical column from the Hudi table schema as a user field. For legacy tables that still have the trailing __bucket/__offset/__timestamp columns, this shifts userFieldPosition and can produce wrong key positions (or fail to find the record-key field in projections), breaking sorted reads.
        Map<String, DataType> dataTypesByName = new HashMap<>();
        Map<String, Integer> userFieldPositionsByName = new HashMap<>();
        List<DataType> userDataTypes = new ArrayList<>();
        int userFieldPosition = 0;
        for (Schema.UnresolvedColumn column :
                hudiTableInfo.getHudiTable().getUnresolvedSchema().getColumns()) {
            DataType dataType = getDataType(column);
            dataTypesByName.put(column.getName(), dataType);
            userFieldPositionsByName.put(column.getName(), userFieldPosition++);
            userDataTypes.add(dataType);
        }

Comment on lines +39 to 43
public HudiRowAsFlussRow() {}

public HudiRowAsFlussRow(RowData rowData) {
this(rowData, true);
}

HudiRowAsFlussRow(RowData rowData, boolean stripSystemColumns) {
this.rowData = rowData;
this.stripSystemColumns = stripSystemColumns;
}

private HudiRowAsFlussRow(boolean stripSystemColumns) {
this.stripSystemColumns = stripSystemColumns;
}
Comment on lines 41 to +48
public void setFlussRecord(LogRecord logRecord) {
this.logRecord = logRecord;
this.internalRow = logRecord.getRow();
this.originRowFieldCount = internalRow.getFieldCount();
checkState(
originRowFieldCount == rowType.getFieldCount() - SYSTEM_COLUMNS.size(),
"Hudi table field count must equal Fluss LogRecord field count plus system fields.");
internalRow.getFieldCount() == fieldCount,
"The Fluss record's field count (%s) must equal the Hudi table field count (%s).",
internalRow.getFieldCount(),
fieldCount);
Comment on lines +225 to 230
// FIP-27: the physical schema is metadata columns followed by user columns only; strip
// the Hudi metadata columns so only the business columns are emitted.
this.projectedRow =
ProjectedRow.from(
IntStream.range(
metadataFieldCount,
schema.getFields().size() - systemFieldCount)
IntStream.range(metadataFieldCount, schema.getFields().size())
.toArray());

@luoyuxia luoyuxia 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.

@fhan688 Overall LGTM from the implementation perspective. The clean-only schema is handled consistently across table creation, writer, reader/projection, nested rows, sorted reads, and Hudi file-group-based compaction. One remaining item before merge: the current documentation and PR framing still describe behavior that was removed in the follow-up commits. website/docs/streaming-lakehouse/datalake-formats/hudi.md still says Fluss appends __bucket, __offset, and __timestamp, while the current implementation creates Hudi tables with user columns only. The PR title and description also still claim clean/legacy layout detection, legacy read/write support, and re-enable tests, none of which remain in the current head. Since Hudi has not been publicly released and this PR intentionally switches directly to clean-only, could you update hudi.md and revise the PR title/description to make that decision explicit? The guidance reserving the three system-column names can remain if that is still intended.

@fhan688 fhan688 changed the title [lake/hudi] Support clean and legacy Hudi lake table schemas [lake/hudi] Use a clean lake table schema for Hudi lake storage Aug 25, 2026
@luoyuxia
luoyuxia merged commit 93e0ac4 into apache:main Aug 26, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants