feat(writer): add hoodie.meta.fields.mode for selective meta-field population on CoW tables#19205
feat(writer): add hoodie.meta.fields.mode for selective meta-field population on CoW tables#19205nsivabalan wants to merge 3 commits into
Conversation
nsivabalan
left a comment
There was a problem hiding this comment.
Can we confirm that we can update meta fields mode value on the fly during writes.
and do we have tests across HoodieStreamer, spark datasource writes towards this.
| if (populateCommitTime) { | ||
| metaFields[HoodieRecord.COMMIT_TIME_METADATA_FIELD_ORD] = shouldPreserveHoodieMetadata | ||
| ? row.getUTF8String(HoodieRecord.COMMIT_TIME_METADATA_FIELD_ORD) : commitTime; | ||
| metaFields[HoodieRecord.COMMIT_SEQNO_METADATA_FIELD_ORD] = shouldPreserveHoodieMetadata |
There was a problem hiding this comment.
why did we include COMMIT_SEQNO_METADATA_FIELD_ORD also here?
we wanted only the commit time meta field.. since thats whats is required for incremental queries.
There was a problem hiding this comment.
Addressed in cfd1fea706ed: dropped _hoodie_commit_seqno population from the selective path. Only _hoodie_commit_time is populated. Downstream consumers that assume seqno is populated whenever commit_time is (if any exist) are out of scope for this PR — the seqno column simply stays null in COMMIT_TIME_ONLY / COMMIT_TIME_AND_FILE_NAME. Rebased on latest upstream/master.
| } else if (populateCommitTime || populateFileName) { | ||
| if (populateCommitTime) { | ||
| row.update(COMMIT_TIME_METADATA_FIELD.ordinal(), instantTime); | ||
| row.update(COMMIT_SEQNO_METADATA_FIELD.ordinal(), |
There was a problem hiding this comment.
same comment as above.
There was a problem hiding this comment.
Addressed in cfd1fea706ed — same as above. Seqno population dropped from the selective path in HoodieSparkParquetWriter#writeRowWithMetadata.
| @@ -89,7 +89,7 @@ protected HoodieFileWriter newParquetFileWriter( | |||
| hoodieConfig.getLongOrDefault(HoodieStorageConfig.PARQUET_MAX_FILE_SIZE), | |||
| storageConfiguration, hoodieConfig.getDoubleOrDefault(HoodieStorageConfig.PARQUET_COMPRESSION_RATIO_FRACTION), | |||
| hoodieConfig.getBooleanOrDefault(HoodieStorageConfig.PARQUET_DICTIONARY_ENABLED)); | |||
| return new HoodieAvroParquetWriter(path, parquetConfig, instantTime, taskContextSupplier, populateMetaFields); | |||
| return new HoodieAvroParquetWriter(path, parquetConfig, instantTime, taskContextSupplier, populateMetaFields, metaFieldsMode); | |||
There was a problem hiding this comment.
can we not fold both populateMetaFields arg and metaFieldsMode into one argument. do we really need both?
not just here. in all writer factory and other places, where we are adding metaFieldsMode as new arg in addition to populateMetaFields.
There was a problem hiding this comment.
Addressed in cfd1fea706ed: folded populateMetaFields + metaFieldsMode into a single MetaFieldsMode enum (ALL / NONE / COMMIT_TIME_ONLY / FILE_NAME_ONLY / COMMIT_TIME_AND_FILE_NAME). The enum is now the sole meta-field argument passed through every writer factory and writer constructor. populate.meta.fields remains on the config surface for backward compat and is auto-derived from the enum. The predicate helpers isCommitTimePopulated() / isFileNamePopulated() on the enum are used in write branches. Also note: the follow-up CLI PR (#19206) uses the same enum for the table set-meta-fields-mode command surface.
| // population is intentionally skipped — that requires the record-key column. | ||
| GenericRecord genericRecord = (GenericRecord) avroRecord; | ||
| if (populateCommitTime) { | ||
| String seqId = HoodieRecord.generateSequenceId(instantTime, |
There was a problem hiding this comment.
we don't need to add commit seq no
There was a problem hiding this comment.
Addressed in cfd1fea706ed — same as above. Seqno population dropped from the selective path in HoodieAvroParquetWriter#writeAvroWithMetadata.
| throw new HoodieException("Incremental queries are not supported when meta fields are disabled") | ||
| if (!metaClient.getTableConfig.isCommitTimePopulated()) { | ||
| throw new HoodieException("Incremental queries are not supported when _hoodie_commit_time is not populated. " | ||
| + "Either keep hoodie.populate.meta.fields=true or include _hoodie_commit_time in hoodie.meta.fields.mode.") |
There was a problem hiding this comment.
lets also call out that, a table once disabled for commit time, can never be switched back. So, users got to re-create a new table.
just setting these writer props during next write may not suffice.
lets capture this in the msg.
There was a problem hiding this comment.
Addressed in cfd1fea706ed: extended the error message in IncrementalRelationV1 / IncrementalRelationV2 to state that hoodie.meta.fields.mode is a physical-storage decision baked into files at write time — it cannot be flipped by changing writer options later, and a table that was created without _hoodie_commit_time populated must be recreated to enable incremental queries. The runtime-immutability guard in HoodieWriterUtils.validateTableConfig blocks silent flips (both null → non-empty and non-empty → different) with a message pointing at the CLI / upgrade path. The follow-up CLI PR (#19206) adds table set-meta-fields-mode as the sanctioned mutation path with a --force guard for tables that already have commits.
…enum Addresses review feedback on apache#19205: - Fold (populateMetaFields boolean, comma-separated mode) into a single MetaFieldsMode enum (ALL / NONE / COMMIT_TIME_ONLY / FILE_NAME_ONLY / COMMIT_TIME_AND_FILE_NAME). The enum is the sole argument passed through every writer factory and writer constructor. populate.meta.fields stays on the config surface for backward compat and is auto-derived from the enum. - On-disk representation is now the enum name (e.g. "COMMIT_TIME_ONLY") instead of a comma-separated token list. Existing tables without the property continue to fall back to ALL or NONE based on the legacy hoodie.populate.meta.fields boolean. - Drop _hoodie_commit_seqno population from the selective write path — only _hoodie_commit_time is needed for incremental queries. (Review comments on HoodieRowCreateHandle:193, HoodieSparkParquetWriter:100, HoodieAvroParquetWriter:103.) - Extend the IncrementalRelation error message to state that hoodie.meta.fields.mode is physical-storage and cannot be flipped by changing write options — existing tables must be recreated to change it. - StreamSync.initializeEmptyTable() was missing the mode pass-through — wired via setMetaFieldsModeFromString. All other non-test initTable call sites either use fromProperties() (which now routes the mode through) or intentionally don't touch meta fields (CLI table create, MDT, sample writes). Test coverage additions: - TestMetaFieldsMode (hudi-spark): end-to-end assertions read parquet back and verify which meta columns are populated / null per mode. Covers row-writer path (bulk_insert with row.writer.enable=true, default) AND non-row-writer path (insert with row.writer.enable=false). Adds inline-clustering coverage for all 5 modes verifying that clustered files preserve the mode's column population semantics. - TestHoodieStreamerMetaFieldsMode (hudi-utilities): parameterized end-to-end test through the HoodieStreamer entrypoint covering all 5 modes plus MoR+selective rejection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pulation on CoW tables
Introduces `hoodie.meta.fields.mode` — a comma-separated list of meta columns to populate
when `hoodie.populate.meta.fields=false`. Allowed tokens are `_hoodie_commit_time` and
`_hoodie_file_name`; any other token is rejected up-front. The mode is immutable at runtime
(settable only at table creation, via hudi-cli, or during upgrade).
Motivation: the existing all-or-nothing `populate.meta.fields=false` disables incremental
queries (needs `_hoodie_commit_time`) and file-level pruning / debugging lookups
(needs `_hoodie_file_name`). This lets a table opt into either or both without paying for
the remaining three meta columns.
Resulting modes:
- populate.meta.fields=true → ALL (default; mode list ignored)
- populate.meta.fields=false, mode="" → NONE
- populate.meta.fields=false, mode=<subset> → selective (COMMIT_TIME_ONLY /
FILE_NAME_ONLY / COMMIT_TIME_AND_FILE_NAME)
- populate.meta.fields=true, mode=<subset> → rejected at writer init
Scope in this patch:
- Spark writer path (Avro + Row): HoodieAvroParquetWriter, HoodieSparkParquetWriter,
HoodieRowCreateHandle take a Set<String> meta-fields mode and populate the columns
selectively; bloom filter / record-key index registration stays skipped when record key
is not populated.
- Incremental read path: CoW IncrementalRelationV1/V2 accept the new mode via
isCommitTimePopulated(); MoR incremental relations keep the strict populate=true guard
until the MoR log-write path is updated in a follow-up.
Fail-fast at writer init:
- Unknown token in the mode list (parser throws).
- populate.meta.fields=true combined with a non-empty mode (ambiguous).
- MERGE_ON_READ + non-empty mode (log-write path not yet wired; follow-up).
- Non-Spark engine + non-empty mode (Flink RowData / Java-client not yet wired; follow-up).
Runtime immutability:
- HoodieWriterUtils.validateTableConfig now explicitly rejects a null-on-disk → non-empty
transition on the mode property (existing loop only flagged the mismatch when the on-disk
value was non-null, which let this silent-drop path slip through).
Tests:
- TestHoodieMetaFieldsMode (hudi-hadoop-common) — accessor coverage for every combination
plus unknown-token rejection and whitespace tolerance.
- TestHoodieWriteConfigMetaFieldsMode (hudi-client-common) — builder + validate() coverage.
- TestMetaFieldsMode (hudi-spark) — end-to-end write / re-read for every mode plus the
three rejection paths (populate=true+mode, unknown token, MoR+mode).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…enum Addresses review feedback on apache#19205: - Fold (populateMetaFields boolean, comma-separated mode) into a single MetaFieldsMode enum (ALL / NONE / COMMIT_TIME_ONLY / FILE_NAME_ONLY / COMMIT_TIME_AND_FILE_NAME). The enum is the sole argument passed through every writer factory and writer constructor. populate.meta.fields stays on the config surface for backward compat and is auto-derived from the enum. - On-disk representation is now the enum name (e.g. "COMMIT_TIME_ONLY") instead of a comma-separated token list. Existing tables without the property continue to fall back to ALL or NONE based on the legacy hoodie.populate.meta.fields boolean. - Drop _hoodie_commit_seqno population from the selective write path — only _hoodie_commit_time is needed for incremental queries. (Review comments on HoodieRowCreateHandle:193, HoodieSparkParquetWriter:100, HoodieAvroParquetWriter:103.) - Extend the IncrementalRelation error message to state that hoodie.meta.fields.mode is physical-storage and cannot be flipped by changing write options — existing tables must be recreated to change it. - StreamSync.initializeEmptyTable() was missing the mode pass-through — wired via setMetaFieldsModeFromString. All other non-test initTable call sites either use fromProperties() (which now routes the mode through) or intentionally don't touch meta fields (CLI table create, MDT, sample writes). Test coverage additions: - TestMetaFieldsMode (hudi-spark): end-to-end assertions read parquet back and verify which meta columns are populated / null per mode. Covers row-writer path (bulk_insert with row.writer.enable=true, default) AND non-row-writer path (insert with row.writer.enable=false). Adds inline-clustering coverage for all 5 modes verifying that clustered files preserve the mode's column population semantics. - TestHoodieStreamerMetaFieldsMode (hudi-utilities): parameterized end-to-end test through the HoodieStreamer entrypoint covering all 5 modes plus MoR+selective rejection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cfd1fea to
7b139c9
Compare
|
Addressed the round of self-review comments in cfd1fea706ed (branch now rebased on latest upstream/master, force-pushed):
Re: "can we confirm the mode is immutable at runtime + tests across HoodieStreamer / spark datasource"
|
Previously the populateMetaFields=false branch of HoodieDatasetBulkInsertHelper projected the five Hudi meta columns as Alias(Literal(UTF8String.EMPTY_UTF8, StringType), name). A non-null Literal derives a non-null Alias, so the resulting StructField had nullable=false. Under ALL mode this was harmless because HoodieRowCreateHandle.writeRow unconditionally fills every meta slot with a non-null value. Once selective / NONE modes started leaving the opted-out columns null on the row (per hoodie.meta.fields.mode), the non-nullable StructType led SparkToParquetSchemaConverter to declare the columns as `required binary` on disk, and any subsequent read failed with ParquetDecodingException: could not read bytes at offset 0. Switch the stub to Alias(Literal.create(null, StringType), name) so the projected StructField is nullable and the physical parquet column is written as OPTIONAL. Handle-level population (writeRow / writeRowNoMetaFields / writeRowSelectiveMetaFields) is unchanged — the stub value is never observed on disk; the fix only affects the parquet schema. Fixes all row-writer + clustering selective-mode failures in TestMetaFieldsMode (commitTimeOnly / fileNameOnly / commitTimeAndFileName / none, plus clustering variants). 18/18 tests pass locally.
Describe the issue this Pull Request addresses
Today the only way to opt out of populating Hudi's five meta columns is the all-or-nothing
hoodie.populate.meta.fields=false. That saves storage but disables incremental queries (which require_hoodie_commit_time) and file-level pruning / investigation lookups (which need_hoodie_file_name).A community user surfaced this trade-off (#18383, also discussed at #17959). This PR proposes a scoped, list-based property that lets a table opt into
_hoodie_commit_timeand/or_hoodie_file_namewithout paying for the remaining three meta columns.This PR supersedes #19047 (the interim
hoodie.meta.fields.commit.time.enabledboolean approach). Closes #18383.Summary and Changelog
Adds a new table property
hoodie.meta.fields.mode— a comma-separated list of meta columns to populate whenhoodie.populate.meta.fields=false. Allowed tokens are_hoodie_commit_timeand_hoodie_file_name; any other token is rejected up-front. The mode is immutable at runtime (settable only at table creation, via hudi-cli, or during upgrade).Resulting modes:
Why a list instead of a boolean or enum
Plug points
Config + accessors (`hudi-common` / `hudi-client-common`):
Writer engines:
Read path (incremental query rejection):
Runtime immutability guard (`HoodieWriterUtils.validateTableConfig`):
Scope
Impact
Risk Level
low
Additive change with a narrow scope. The default path is untouched. Fail-fast validation at writer init rejects every ambiguous combination loudly. Runtime property changes are blocked.
Documentation Update
Contributor's checklist
🤖 Generated with Claude Code