nsivabalan commented on code in PR #19205:
URL: https://github.com/apache/hudi/pull/19205#discussion_r3738674429


##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/config/HoodieWriteConfig.java:
##########
@@ -3883,6 +3932,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();

Review Comment:
   **Reopening: the behavior changed after you read my reply, and it changed on 
exactly the surface your P1 was about.**
   
   What you read: a config carrying `populate.meta.fields=true` alongside a 
selective mode was *accepted*, with the boolean silently derived to `false`. 
Your P1 was that the two properties must not be able to contradict each other 
on disk, and derivation achieved that.
   
   What it does now: that combination is **rejected at table creation**. 
@nsivabalan pushed back on the derive-silently answer with a question I did not 
have a good response to — if `populate=true` can never be persisted alongside a 
selective mode, why accept it as input at all? A user who states it believes 
they asked for something, and we were discarding half their request without a 
word.
   
   So the invariant you asked for is unchanged, but it is now upheld by 
*rejection* rather than by *override*:
   
   | | |
   |---|---|
   | selective mode + unset boolean | derived as `false` (the ordinary case) |
   | selective mode + `false` | coherent restatement, accepted |
   | selective mode + `true` | **rejected** |
   | `ALL` + `true` | coherent restatement, accepted |
   | `ALL` + `false` | **rejected** |
   
   Note this is deliberately *narrower* than the check you originally proposed 
and that `664ff2e` removed: that one rejected `populate=true` + **any** mode, 
including `ALL` + `true`, which is a perfectly coherent request. Only genuine 
contradictions fail now.
   
   It also makes creation consistent with the write path, which already rejects 
an explicitly-set boolean that disagrees with the table rather than overriding 
it.
   
   One implementation note, since it is the part that could have broken things: 
`setPopulateMetaFields` took a primitive `boolean`, and all three creation 
paths read the property via `getBooleanOrDefault` and always passed it — so 
every writer that never mentioned the property looked like it had explicitly 
asked for `true`, and a plain `hoodie.meta.fields.mode=COMMIT_TIME_ONLY` would 
have failed with a spurious conflict. The setter now takes a nullable 
`Boolean`, and `HoodieSparkSqlWriter` (both paths) and `StreamSync` pass `null` 
when unstated. The other callers — both bootstrap executors, the metadata 
table, Flink — never set a mode, so the check cannot fire for them.
   
   Worth a fresh look given it is your P1.
   



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/config/HoodieWriteConfig.java:
##########
@@ -1772,8 +1773,40 @@ 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(getStringOrDefault(HoodieTableConfig.META_FIELDS_MODE),

Review Comment:
   **Reopening: this changed after you read my reply.**
   
   Your P1 was that a direct `SparkRDDWriteClient` could open a persisted 
`COMMIT_TIME_ONLY` table with `populate=false` and no mode, resolve to `NONE`, 
pass the boolean-only check, and write null commit times while the table still 
advertised `COMMIT_TIME_ONLY`. My reply said `validateAgainstTableProperties` 
now compares the full enum, which caught it.
   
   That is still true, but the shape has changed underneath it. 
`hoodie.meta.fields.mode` is now a **table property** that a write can never 
change, and the client applies one rule for every engine:
   
   - writer states **neither** meta-field property → **inherits** the table's 
mode; no comparison
   - writer states **either** one → compared, rejected on mismatch, in both 
directions
   
   So your exact scenario now resolves one of two ways rather than reaching an 
enum comparison at all. If the writer stated nothing, it inherits 
`COMMIT_TIME_ONLY` and keeps writing commit times. If it stated 
`populate=false`, that is an explicit disagreement with the table and it fails 
loudly. Either way it can no longer silently write null commit times.
   
   Two consequences worth your attention:
   
   1. This retires the HUDI-2161 carve-out (`d5026e9a2485`) that let an 
unstated writer narrow an `ALL` table to `NONE`. Inheritance is what makes that 
safe — table services and a restarted `HoodieStreamer` state neither property, 
so they inherit rather than being rejected. It also let me delete 
`StreamSync`'s own back-fill, which was redundant and keyed only on the mode 
key.
   2. It is a **breaking change**: passing `hoodie.populate.meta.fields=false` 
against an `ALL` table now throws where it used to narrow silently. Called out 
in the description for release notes. Only when *explicitly set*.
   
   `TestBaseHoodieWriteClient` covers these combinations at 110 tests, 
including the one you described.
   



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/upgrade/TenToNineDowngradeHandler.java:
##########
@@ -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);
+
     return new UpgradeDowngrade.TableConfigChangeSet(
         Collections.emptyMap(),
-        Collections.singleton(HoodieTableConfig.TABLE_STORAGE_LAYOUT));
+        propertiesToDelete);

Review Comment:
   **Reopening: the downgrade behavior reversed twice since you read this, and 
it has landed somewhere different from what my reply described.**
   
   The write-back you both asked for is in and unchanged — downgrade derives 
`populate.meta.fields` from the mode and deletes the mode, mirroring 
`NineToEightDowngradeHandler:116-117`. That part is settled.
   
   What changed is the selective-mode case:
   
   1. My reply said selective modes degrade to `NONE` with a `LOG.warn`. 
@voonhous objected that this was the only one-way, unrecoverable handler pair 
in the package.
   2. I then made it **throw**, and told you so.
   3. @nsivabalan pointed out the throw was unnecessary, and he was right — I 
had missed the reason.
   
   **Every selective mode already persists `populate.meta.fields=false`**, 
because the boolean is always derived from the mode rather than taken from the 
caller. So there is nothing to decide at downgrade time: drop the mode, restate 
the `false` that is already on disk, done. No special case at all.
   
   | Table mode | v9 sees | Round trip |
   |---|---|---|
   | `ALL` | `populate=true` | lossless |
   | `NONE` | `populate=false` | lossless |
   | any selective mode | `populate=false` | mode is lost |
   
   That is the correct reading for v9, which has no code that reads 
`_hoodie_commit_time` selectively — presenting the table as having no meta 
columns is the only honest thing it can say. Files keep their populated 
columns; only how the table advertises itself changes. Critically the direction 
is safe: the table **under-claims**. Over-claiming is the bug, and that is 
exactly what the write-back prevents (@voonhous's original catch on the `true` 
default).
   
   @voonhous — on your one-way concern specifically: a selective mode still 
cannot survive a v10 → v9 → v10 round trip. It returns as `NONE`, and hudi-cli 
cannot widen it back, so recreating the table is the only recovery. I think 
that is better handled as a documented, warned, and *tested* lossy path than as 
a hard failure the operator cannot work around — but it is your call, and 
`reUpgradeAfterDowngradingASelectiveModeResolvesToNone` now pins it rather than 
leaving it implicit.
   
   `TestTenToNineDowngradeHandler` is 14/14 and the full upgrade/downgrade 
suite 84/84.
   



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/upgrade/TenToNineDowngradeHandler.java:
##########
@@ -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);

Review Comment:
   **Reopening: the downgrade behavior reversed twice since you read this, and 
it has landed somewhere different from what my reply described.**
   
   The write-back you both asked for is in and unchanged — downgrade derives 
`populate.meta.fields` from the mode and deletes the mode, mirroring 
`NineToEightDowngradeHandler:116-117`. That part is settled.
   
   What changed is the selective-mode case:
   
   1. My reply said selective modes degrade to `NONE` with a `LOG.warn`. 
@voonhous objected that this was the only one-way, unrecoverable handler pair 
in the package.
   2. I then made it **throw**, and told you so.
   3. @nsivabalan pointed out the throw was unnecessary, and he was right — I 
had missed the reason.
   
   **Every selective mode already persists `populate.meta.fields=false`**, 
because the boolean is always derived from the mode rather than taken from the 
caller. So there is nothing to decide at downgrade time: drop the mode, restate 
the `false` that is already on disk, done. No special case at all.
   
   | Table mode | v9 sees | Round trip |
   |---|---|---|
   | `ALL` | `populate=true` | lossless |
   | `NONE` | `populate=false` | lossless |
   | any selective mode | `populate=false` | mode is lost |
   
   That is the correct reading for v9, which has no code that reads 
`_hoodie_commit_time` selectively — presenting the table as having no meta 
columns is the only honest thing it can say. Files keep their populated 
columns; only how the table advertises itself changes. Critically the direction 
is safe: the table **under-claims**. Over-claiming is the bug, and that is 
exactly what the write-back prevents (@voonhous's original catch on the `true` 
default).
   
   @voonhous — on your one-way concern specifically: a selective mode still 
cannot survive a v10 → v9 → v10 round trip. It returns as `NONE`, and hudi-cli 
cannot widen it back, so recreating the table is the only recovery. I think 
that is better handled as a documented, warned, and *tested* lossy path than as 
a hard failure the operator cannot work around — but it is your call, and 
`reUpgradeAfterDowngradingASelectiveModeResolvesToNone` now pins it rather than 
leaving it implicit.
   
   `TestTenToNineDowngradeHandler` is 14/14 and the full upgrade/downgrade 
suite 84/84.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to