-
Notifications
You must be signed in to change notification settings - Fork 2.5k
feat(writer): add hoodie.meta.fields.mode for selective meta-field population on CoW tables #19205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
2b11a4f
0b25918
77f4e91
13b39db
c78dc96
358fbfd
2b92165
664ff2e
c266b82
7dcd9d1
9495687
4134dac
a308a37
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,6 +45,7 @@ | |
| import org.apache.hudi.common.model.HoodieRecord; | ||
| import org.apache.hudi.common.model.HoodieTableType; | ||
| import org.apache.hudi.common.model.HoodieWriteStat; | ||
| import org.apache.hudi.common.model.MetaFieldsMode; | ||
| import org.apache.hudi.common.model.TableServiceType; | ||
| import org.apache.hudi.common.model.WriteOperationType; | ||
| import org.apache.hudi.common.schema.HoodieSchema; | ||
|
|
@@ -1548,9 +1549,40 @@ public void validateAgainstTableProperties(HoodieTableConfig tableConfig, Hoodie | |
| // mismatch of table versions. | ||
| CommonClientUtils.validateTableVersion(tableConfig, writeConfig); | ||
|
|
||
| // Once meta fields are disabled, it cant be re-enabled for a given table. | ||
| if (!tableConfig.populateMetaFields() && writeConfig.populateMetaFields()) { | ||
| throw new HoodieException(HoodieTableConfig.POPULATE_META_FIELDS.key() + " already disabled for the table. Can't be re-enabled back"); | ||
| // Meta-field population is physical, so a writer must not claim columns the table does not | ||
| // have. Compare the full enum rather than the legacy booleans: those collapse every selective | ||
| // mode to false, so a writer claiming COMMIT_TIME_ONLY against a NONE table would slip through | ||
| // and advertise commit times that were never written. | ||
| // | ||
| // Two distinct cases, because writers routinely omit meta-field settings entirely: | ||
| // | ||
| // - Widening is always rejected. Enabling a column now would leave earlier commits without it, | ||
| // and readers cannot tell the two apart. | ||
| // - Any disagreement is rejected when the writer *explicitly* sets hoodie.meta.fields.mode. | ||
| // That covers narrowing too, e.g. an explicit NONE against a COMMIT_TIME_ONLY table, which | ||
| // would write null commit times while the table still advertises COMMIT_TIME_ONLY and make | ||
| // incremental queries silently miss those rows. | ||
| // | ||
| // A writer that never mentions the mode is left alone: resolving to NONE against an ALL table | ||
| // is long-standing behavior for callers that build a write config without restating the table's | ||
| // settings, and writing fewer meta columns cannot make a reader believe in absent data. | ||
| MetaFieldsMode tableMetaFieldsMode = tableConfig.getMetaFieldsMode(); | ||
| MetaFieldsMode writeMetaFieldsMode = writeConfig.getMetaFieldsMode(); | ||
| boolean writerStatedMode = writeConfig.contains(HoodieTableConfig.META_FIELDS_MODE) | ||
| && !StringUtils.isNullOrEmpty(writeConfig.getString(HoodieTableConfig.META_FIELDS_MODE)); | ||
| if (writeMetaFieldsMode.isWiderThan(tableMetaFieldsMode)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is narrower than allowed here? seems not from the check in line 1580
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, narrowing is deliberately allowed, and the code does match the comment. Full matrix, with S = "writer explicitly set
So Worth noting the relation is not a total order: |
||
| throw new HoodieException(String.format( | ||
| "%s cannot be widened for an existing table: table is %s but the writer requests %s. Meta " | ||
| + "columns are physical, so enabling one now would leave earlier commits without it. " | ||
| + "Set %s=%s on the writer, or recreate the table to change it.", | ||
| HoodieTableConfig.META_FIELDS_MODE.key(), tableMetaFieldsMode, writeMetaFieldsMode, | ||
| HoodieTableConfig.META_FIELDS_MODE.key(), tableMetaFieldsMode)); | ||
| } else if (writerStatedMode && writeMetaFieldsMode != tableMetaFieldsMode) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if writeMetaFieldsMode != tableMetaFieldsMode is true, then writerStatedMode must be true right? so we can eliminate writerStatedMode.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it is load-bearing, and this PR already has the test that proves it: That path has always been legal: the pre-PR check was one-directional ( That said, the flag is currently mis-scoped rather than redundant -- see my comment on |
||
| throw new HoodieException(String.format( | ||
| "%s mismatch: table is %s but the writer explicitly requests %s. Meta columns are physical, " | ||
| + "so the writer must match the table. Set %s=%s on the writer, or recreate the table.", | ||
| HoodieTableConfig.META_FIELDS_MODE.key(), tableMetaFieldsMode, writeMetaFieldsMode, | ||
| HoodieTableConfig.META_FIELDS_MODE.key(), tableMetaFieldsMode)); | ||
| } | ||
|
|
||
| // Meta fields can be disabled only when either {@code SimpleKeyGenerator}, {@code ComplexKeyGenerator}, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,6 +50,7 @@ | |
| import org.apache.hudi.common.model.HoodieRecordMerger; | ||
| import org.apache.hudi.common.model.HoodieRecordPayload; | ||
| import org.apache.hudi.common.model.HoodieTableType; | ||
| import org.apache.hudi.common.model.MetaFieldsMode; | ||
| import org.apache.hudi.common.model.WriteConcurrencyMode; | ||
| import org.apache.hudi.common.model.WriteOperationType; | ||
| import org.apache.hudi.common.table.HoodieTableConfig; | ||
|
|
@@ -1772,8 +1773,39 @@ public int getSmallFileGroupCandidatesLimit() { | |
| return getInt(MERGE_SMALL_FILE_GROUP_CANDIDATES_LIMIT); | ||
| } | ||
|
|
||
| /** | ||
| * @return true when every meta column is populated. | ||
| * | ||
| * <p>Derived from {@link #getMetaFieldsMode()} so that call sites still written against the | ||
| * deprecated {@code hoodie.populate.meta.fields} boolean observe the same answer as the enum: | ||
| * only {@link MetaFieldsMode#ALL} populates every meta column. | ||
| */ | ||
| public boolean populateMetaFields() { | ||
| return getBooleanOrDefault(HoodieTableConfig.POPULATE_META_FIELDS); | ||
| return getMetaFieldsMode().toLegacyPopulateMetaFields(); | ||
| } | ||
|
|
||
| /** | ||
| * @return the {@link MetaFieldsMode} resolved from the write config. | ||
| * {@code hoodie.meta.fields.mode} is the source of truth; configs written before that property | ||
| * existed fall back to {@link MetaFieldsMode#ALL} or {@link MetaFieldsMode#NONE} based on the | ||
| * deprecated {@code hoodie.populate.meta.fields} boolean. | ||
| */ | ||
| public MetaFieldsMode getMetaFieldsMode() { | ||
| return MetaFieldsMode.resolve(this); | ||
| } | ||
|
|
||
| /** | ||
| * @return true when {@code _hoodie_commit_time} is physically populated on every row. | ||
| */ | ||
| public boolean isCommitTimePopulated() { | ||
| return getMetaFieldsMode().isCommitTimePopulated(); | ||
| } | ||
|
|
||
| /** | ||
| * @return true when {@code _hoodie_file_name} is physically populated on every row. | ||
| */ | ||
| public boolean isFileNamePopulated() { | ||
| return getMetaFieldsMode().isFileNamePopulated(); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -3586,11 +3618,31 @@ public Builder withCanIgnorePostCommitFailures(boolean canIgnorePostCommitFailur | |
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * @deprecated since 1.3.0, use {@link #withMetaFieldsMode(MetaFieldsMode)} instead | ||
| * ({@code true} maps to {@link MetaFieldsMode#ALL}, {@code false} to {@link MetaFieldsMode#NONE}). | ||
| */ | ||
| @Deprecated | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. may not related with this patch, but setting a table config option though write config API does not look right, the table config option should be immutable once the table is created.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed on the principle, and you're right that it's pre-existing rather than introduced here — On the immutability concern specifically: the write config is the input to table creation, not a mutation channel for an existing table. Once a table exists, a differing value is rejected rather than applied — That said, the broader cleanup you're pointing at — write-config builders shouldn't carry table-config setters at all — seems worth doing properly across all of them rather than piecemeal. Happy to file a follow-up JIRA for that sweep if you think it's worth tracking; it would cover the existing |
||
| public Builder withPopulateMetaFields(boolean populateMetaFields) { | ||
| writeConfig.setValue(HoodieTableConfig.POPULATE_META_FIELDS, Boolean.toString(populateMetaFields)); | ||
| return this; | ||
| } | ||
|
|
||
| public Builder withMetaFieldsMode(MetaFieldsMode metaFieldsMode) { | ||
| // Leaving the mode unset defers to the deprecated populate.meta.fields boolean. Setting it | ||
| // also rewrites that boolean from the mode, so the two can never disagree — a config carrying | ||
| // a selective mode alongside populate.meta.fields=true would otherwise create a table whose | ||
| // hoodie.properties misleads pre-1.3.0 readers into treating it as ALL. | ||
| if (metaFieldsMode == null) { | ||
| writeConfig.setValue(HoodieTableConfig.META_FIELDS_MODE, ""); | ||
| } else { | ||
| writeConfig.setValue(HoodieTableConfig.META_FIELDS_MODE, metaFieldsMode.name()); | ||
| writeConfig.setValue(HoodieTableConfig.POPULATE_META_FIELDS, | ||
| Boolean.toString(metaFieldsMode.toLegacyPopulateMetaFields())); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The second order produces exactly the config Cheapest fix is to re-derive in |
||
| } | ||
| return this; | ||
| } | ||
|
|
||
| public Builder withAllowOperationMetadataField(boolean allowOperationMetadataField) { | ||
| writeConfig.setValue(ALLOW_OPERATION_METADATA_FIELD, Boolean.toString(allowOperationMetadataField)); | ||
| return this; | ||
|
|
@@ -3883,6 +3935,27 @@ private void validate() { | |
| checkArgument(ttlStatsMaxParallelism > 0, | ||
| String.format("%s must be positive, but was %d", | ||
| HoodieTTLConfig.STATS_MAX_PARALLELISM.key(), ttlStatsMaxParallelism)); | ||
|
|
||
| // hoodie.meta.fields.mode is the source of truth for meta-column population; the deprecated | ||
| // populate.meta.fields boolean is consulted only when the mode is absent. There is therefore | ||
| // no ambiguous combination to reject here — MetaFieldsMode.resolve throws on unrecognized | ||
| // values. | ||
| MetaFieldsMode metaFieldsMode = writeConfig.getMetaFieldsMode(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1]
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in Whenever an explicit mode is supplied, the legacy boolean is now derived from it rather than taken from the caller (
With no mode supplied, only the legacy boolean is recorded, so pre-1.3.0 on-disk behavior is unchanged. I went with "always persist the derived value" rather than "reject the combination" because rejecting would make On |
||
| // Selective meta-field modes are CoW-only in this release. MoR log-write path does not yet | ||
| // respect the mode, which would silently produce log records with null meta columns. | ||
| boolean isSelective = metaFieldsMode != MetaFieldsMode.ALL && metaFieldsMode != MetaFieldsMode.NONE; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can have a method MetaFieldsMode.isSelective |
||
| checkArgument(!(writeConfig.getTableType() == HoodieTableType.MERGE_ON_READ && isSelective), | ||
| String.format("%s=%s is currently supported for COPY_ON_WRITE tables only. MoR support is a follow-up. " | ||
| + "For MoR use %s=ALL or %s=NONE.", | ||
| HoodieTableConfig.META_FIELDS_MODE.key(), metaFieldsMode, | ||
| HoodieTableConfig.META_FIELDS_MODE.key(), HoodieTableConfig.META_FIELDS_MODE.key())); | ||
| // Selective meta-field modes are wired only for the Spark writer path in this release. Flink | ||
| // RowData / Java-client writers ignore the mode and would silently produce NONE-mode output. | ||
| checkArgument(!(engineType != EngineType.SPARK && isSelective), | ||
|
nsivabalan marked this conversation as resolved.
|
||
| String.format("%s=%s is currently supported for the Spark writer only. Support for engine=%s is a follow-up. " | ||
| + "Use %s=ALL or %s=NONE.", | ||
| HoodieTableConfig.META_FIELDS_MODE.key(), metaFieldsMode, engineType, | ||
| HoodieTableConfig.META_FIELDS_MODE.key(), HoodieTableConfig.META_FIELDS_MODE.key())); | ||
| } | ||
|
|
||
| public HoodieWriteConfig build() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,13 +18,25 @@ | |
|
|
||
| package org.apache.hudi.table.upgrade; | ||
|
|
||
| import org.apache.hudi.common.config.ConfigProperty; | ||
| import org.apache.hudi.common.engine.HoodieEngineContext; | ||
| import org.apache.hudi.common.model.MetaFieldsMode; | ||
| import org.apache.hudi.common.table.HoodieTableConfig; | ||
| import org.apache.hudi.config.HoodieWriteConfig; | ||
|
|
||
| import java.util.Collections; | ||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * Version 10 enables native log format by default for new writes. Existing version 9 | ||
| * inline log files remain readable by version 10 readers, so there is no table metadata | ||
| * rewrite required for the upgrade. | ||
| * | ||
| * <p>Version 10 also introduced {@code hoodie.meta.fields.mode}. Version 9 tables predate it and | ||
| * resolve to {@code ALL} / {@code NONE} from the deprecated {@code hoodie.populate.meta.fields} | ||
| * boolean. The upgrade records that derived value explicitly so upgraded tables describe their | ||
| * meta-field layout the same way newly created version 10 tables do, rather than depending on the | ||
| * legacy fallback. Behavior is unchanged either way — this only makes the on-disk state explicit. | ||
| */ | ||
| public class NineToTenUpgradeHandler implements UpgradeHandler { | ||
|
|
||
|
|
@@ -34,6 +46,12 @@ public UpgradeDowngrade.TableConfigChangeSet upgrade( | |
| HoodieEngineContext context, | ||
| String instantTime, | ||
| SupportsUpgradeDowngrade upgradeDowngradeHelper) { | ||
| return new UpgradeDowngrade.TableConfigChangeSet(); | ||
| HoodieTableConfig tableConfig = | ||
| upgradeDowngradeHelper.getTable(config, context).getMetaClient().getTableConfig(); | ||
| // Resolves from the legacy boolean for a version 9 table, since the mode property is absent. | ||
| MetaFieldsMode metaFieldsMode = tableConfig.getMetaFieldsMode(); | ||
| Map<ConfigProperty, String> propertiesToUpdate = Collections.singletonMap( | ||
| HoodieTableConfig.META_FIELDS_MODE, metaFieldsMode.name()); | ||
| return new UpgradeDowngrade.TableConfigChangeSet(propertiesToUpdate, Collections.emptySet()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the legacy "populate meta fields" property should be removed?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would keep it, and I think this one deserves an explicit comment saying why, because the symmetry argument points the other way.
So if the upgrade removed the boolean and the table were later downgraded, a v9 reader would find neither property and resolve to Suggest keeping it and noting the default-value exception in the class javadoc, so the deviation reads as deliberate rather than an oversight.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Didn't get it, for upgrade, the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a scenario for upgrade + downgrade round trip. State A -- v9 table today (created pre-1.3 with virtual keys): hoodie.properties: Data files on disk have no meta columns. Readers resolve State B -- after 9 -> 10 if the upgrade removed the legacy property (the symmetry that hoodie.properties: It is still correct -- at v10 the mode is the source of truth. State C -- after 10 -> 9 downgrade.
hoodie.properties: Neither property is present now. State D -- a v9 reader opens it
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. During downgrade, the We should fix both the upgrade/downgrade handler to translate the options and add/remove the option that does not need
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. reached concensus offline with @voonhous that my suggestions works. |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,25 +18,61 @@ | |
|
|
||
| package org.apache.hudi.table.upgrade; | ||
|
|
||
| import org.apache.hudi.common.config.ConfigProperty; | ||
| import org.apache.hudi.common.engine.HoodieEngineContext; | ||
| import org.apache.hudi.common.model.MetaFieldsMode; | ||
| import org.apache.hudi.common.table.HoodieTableConfig; | ||
| import org.apache.hudi.config.HoodieWriteConfig; | ||
|
|
||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import java.util.Collections; | ||
| import java.util.HashSet; | ||
| import java.util.Set; | ||
|
|
||
| /** | ||
| * Version 10 writes native log files by default. Downgrading to version 9 requires | ||
| * full compaction of native data/delete logs before the downgrade completes. | ||
| * | ||
| * <p>Version 10 also introduced {@code hoodie.meta.fields.mode}. Version 9 does not understand it, | ||
| * so the property is dropped here while {@code hoodie.populate.meta.fields} is left exactly as it | ||
| * stands — {@code ALL} and {@code NONE} tables round-trip unchanged because those are precisely the | ||
| * two states the legacy boolean can express. Selective modes cannot be expressed in version 9, so | ||
| * the table degrades to what its legacy boolean says (which is {@code false}, i.e. NONE) and we warn. | ||
| */ | ||
| public class TenToNineDowngradeHandler implements DowngradeHandler { | ||
|
|
||
| private static final Logger LOG = LoggerFactory.getLogger(TenToNineDowngradeHandler.class); | ||
|
|
||
| @Override | ||
| public UpgradeDowngrade.TableConfigChangeSet downgrade( | ||
| HoodieWriteConfig config, | ||
| HoodieEngineContext context, | ||
| String instantTime, | ||
| SupportsUpgradeDowngrade upgradeDowngradeHelper) { | ||
| Set<ConfigProperty> propertiesToDelete = new HashSet<>(); | ||
| propertiesToDelete.add(HoodieTableConfig.TABLE_STORAGE_LAYOUT); | ||
|
|
||
| // The warning is best-effort: dropping the property is what matters, and the helper is not | ||
| // always available (some callers drive the change set directly). | ||
| MetaFieldsMode metaFieldsMode = upgradeDowngradeHelper == null | ||
| ? MetaFieldsMode.ALL | ||
| : upgradeDowngradeHelper.getTable(config, context).getMetaClient().getTableConfig().getMetaFieldsMode(); | ||
| if (metaFieldsMode != MetaFieldsMode.ALL && metaFieldsMode != MetaFieldsMode.NONE) { | ||
| LOG.warn("Table is using {}={}, which table version 9 cannot express. The property is being " | ||
| + "removed and the table will behave as {}=false (no meta columns) to version 9 readers. " | ||
| + "Already-written files keep their populated meta columns, but incremental queries that " | ||
| + "relied on {} will stop returning rows. Recreate the table if you need that behavior back.", | ||
| HoodieTableConfig.META_FIELDS_MODE.key(), metaFieldsMode, | ||
| HoodieTableConfig.POPULATE_META_FIELDS.key(), metaFieldsMode); | ||
| } | ||
| // hoodie.populate.meta.fields is deliberately left untouched: whatever the table recorded before | ||
| // the downgrade stays, so ALL and NONE tables are bit-identical afterwards. | ||
| propertiesToDelete.add(HoodieTableConfig.META_FIELDS_MODE); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This breaks the symmetry convention this package already established, and the result is lossy. The precedent, from
Here the downgrade deletes
Suggest adding the derived boolean here, mirroring propertiesToUpdate.put(HoodieTableConfig.POPULATE_META_FIELDS.key(),
String.valueOf(metaFieldsMode.toLegacyPopulateMetaFields()));For Separately: a selective mode is silently destroyed here with only a |
||
|
|
||
| return new UpgradeDowngrade.TableConfigChangeSet( | ||
| Collections.emptyMap(), | ||
| Collections.singleton(HoodieTableConfig.TABLE_STORAGE_LAYOUT)); | ||
| propertiesToDelete); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the legacy "populate meta fields" property should be inferred and added here?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 Details on The precedent is For propertiesToUpdate.put(HoodieTableConfig.POPULATE_META_FIELDS.key(),
String.valueOf(metaFieldsMode.toLegacyPopulateMetaFields()));Separate but related: selective modes are destroyed here with only a |
||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is cshuo's StreamSync-restart scenario, and I do not think it is fixed. It was closed as resolved by
c78dc962, but that commit fixed table-config resolution, not this gate.Walk it through with table =
COMMIT_TIME_ONLYand a writer that sets onlyhoodie.populate.meta.fields=false:writeMetaFieldsModeresolves toNONENONE.isWiderThan(COMMIT_TIME_ONLY)isfalse, so the first branch is skippedwriterStatedModeisfalse, so the second branch is skippedThe writer then takes the mode from the write config, not the table config (
HoodieSparkFileWriterFactory.java:59-60), so it writes base files with null_hoodie_commit_time. The table still advertisesCOMMIT_TIME_ONLY, soIncrementalRelationV1.scala:92still admits incremental queries, and the range filter at:295-297silently drops every one of those rows.StreamSync is the concrete path: it builds its write config from
propsonly (StreamSync.java:1280-1291) and never merges the table config -- themetaClient != nullblock at:1297-1309only touches ordering fields. The +2 lines this PR adds live ininitializeEmptyTable, which runs only when the base path does not exist, so a restart against an existing table never re-reads the mode. The Spark datasource is safe only by accident, becauseHoodieSparkSqlWriter.scala:1102-1106copies table props into the write params.Same failure shape as
876a891979a3[HUDI-3544], where the fix was to detect the config/file disagreement and re-initialize rather than let mixed files accumulate.The narrowing carve-out only needs to cover
ALL -> NONE(the pre-PR behaviour fromd5026e9a2485). Suggest tightening to:and adding a
TestBaseHoodieWriteClientcase: tableCOMMIT_TIME_ONLY, unstated writer -> must throw.