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


##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/BaseHoodieWriteClient.java:
##########
@@ -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)) {
+      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) 
{

Review Comment:
   No, it is load-bearing, and this PR already has the test that proves it: 
`TestBaseHoodieWriteClient.java:129-141` 
(`validateAgainstTablePropertiesAllowsUnstatedWriterToNarrow`) -- table `ALL`, 
writer sets only `withPopulateMetaFields(false)`, so `writeMetaFieldsMode` is 
`NONE` and differs from the table's `ALL` while `writerStatedMode` is `false`.
   
   That path has always been legal: the pre-PR check was one-directional 
(`!table.populateMetaFields() && write.populateMetaFields()`), added in 
`d5026e9a2485` [HUDI-2161]. Dropping the flag would start throwing for every 
caller that builds a write config without restating the table's meta-field 
settings.
   
   That said, the flag is currently **mis-scoped** rather than redundant -- see 
my comment on `:1572`. It should gate only `ALL` tables; for a selective table 
the disagreement has to be rejected whether or not the writer stated a mode.



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/BaseHoodieWriteClient.java:
##########
@@ -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)) {

Review Comment:
   Yes, narrowing is deliberately allowed, and the code does match the comment. 
Full matrix, with S = "writer explicitly set `hoodie.meta.fields.mode`":
   
   - **S = true:** passes iff writer mode == table mode; all 20 off-diagonal 
cells throw (that is the `:1580` branch).
   - **S = false:** the writer mode can only be `ALL` or `NONE`, since it is 
derived from the legacy boolean.
     - `ALL`: throws for every table mode except `ALL` (caught by `isWiderThan` 
via the record-key clause).
     - `NONE`: passes against **all five** table modes.
   
   So `:1573` is what allows narrowing, and it is only reached when S is false. 
Of those `NONE` cells, `ALL` and `NONE` are correct and long-standing. The 
three selective ones are the bug I flagged on `:1572` -- they let a writer 
silently write null `_hoodie_commit_time` onto a table that still advertises 
`COMMIT_TIME_ONLY`.
   
   Worth noting the relation is not a total order: `COMMIT_TIME_ONLY` and 
`FILE_NAME_ONLY` are each wider than the other, so transitions between them are 
rejected in both directions. Nothing currently tests that.



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

Review Comment:
   I would keep it, and I think this one deserves an explicit comment saying 
why, because the symmetry argument points the other way.
   
   `EightToNineUpgradeHandler` does remove the legacy property (`:171-172`, 
`:240-241`), so the convention says remove it here too. The difference is the 
default: `PAYLOAD_CLASS_NAME` and `PRECOMBINE_FIELD` have no default, but 
`POPULATE_META_FIELDS` defaults to `true` (`HoodieTableConfig.java:338-341`).
   
   So if the upgrade removed the boolean and the table were later downgraded, a 
v9 reader would find neither property and resolve to `ALL` -- claiming meta 
columns on files that do not have them. That is exactly the widening this PR 
bans everywhere else.
   
   Suggest keeping it and noting the default-value exception in the class 
javadoc, so the deviation reads as deliberate rather than an oversight.



##########
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:
   Yes, and I think this is the right call -- it also closes a hole. Details on 
`:72`.
   
   The precedent is `NineToEightDowngradeHandler.java:116-117` and `:152-153`: 
downgrade restores the legacy property from the new one, then removes the new 
one. Here the mode is deleted and nothing is written back.
   
   For `ALL`/`NONE` writing the derived boolean is a no-op, so it costs 
nothing. What it fixes is the case where a table carries the mode without the 
boolean -- `POPULATE_META_FIELDS` then falls back to its `true` default and the 
table downgrades to `ALL`, i.e. Hudi believes `_hoodie_record_key` is populated 
on files where it is null.
   
   ```java
   propertiesToUpdate.put(HoodieTableConfig.POPULATE_META_FIELDS.key(),
       String.valueOf(metaFieldsMode.toLegacyPopulateMetaFields()));
   ```
   
   Separate but related: selective modes are destroyed here with only a 
`LOG.warn`, and they cannot be restored afterwards, because a re-upgrade 
derives `NONE` and `isWiderThan` then rejects setting the mode back. No other 
handler pair in this package is one-way like that -- worth either throwing or 
documenting it as intentional.



##########
hudi-client/hudi-client-common/src/test/java/org/apache/hudi/table/upgrade/TestTenToNineDowngradeHandler.java:
##########
@@ -24,18 +24,25 @@
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 class TestTenToNineDowngradeHandler {
 
   @Test
-  void testDowngradeRemovesStorageLayoutOnly() {
+  void testDowngradeRemovesStorageLayoutAndMetaFieldsMode() {
     UpgradeDowngrade.TableConfigChangeSet changeSet =
         new TenToNineDowngradeHandler().downgrade(null, null, null, null);
 
     assertTrue(changeSet.propertiesToUpdate().isEmpty());
-    assertEquals(1, changeSet.propertiesToDelete().size());
+    assertEquals(2, changeSet.propertiesToDelete().size());
     
assertTrue(changeSet.propertiesToDelete().contains(HoodieTableConfig.TABLE_STORAGE_LAYOUT));
+    // Version 9 does not understand hoodie.meta.fields.mode, so it is 
dropped...
+    
assertTrue(changeSet.propertiesToDelete().contains(HoodieTableConfig.META_FIELDS_MODE));
+    // ...while hoodie.populate.meta.fields is deliberately left in place, so 
ALL and NONE tables
+    // round-trip unchanged — those are exactly the two states the legacy 
boolean can express.

Review Comment:
   Keep both and translate on both edges -- that is what the rest of the 
package does (`EightToNineUpgradeHandler.java:171-172` on the way up, 
`NineToEightDowngradeHandler.java:116-117` on the way down).
   
   The one exception is that the upgrade should **not** delete 
`hoodie.populate.meta.fields`, because unlike `PAYLOAD_CLASS_NAME` it has a 
`true` default -- deleting it makes a v9 reader see `ALL`. I have written that 
up on `NineToTenUpgradeHandler.java:55`.
   
   So: upgrade writes the mode and leaves the boolean; downgrade writes the 
derived boolean and deletes the mode. That makes the round trip lossless for 
`ALL`/`NONE` and removes the silent-widening case.
   
   This test will also need a selective-mode case -- right now it passes `null` 
for the helper, so the handler short-circuits to `ALL` and the whole selective 
branch is never executed.



-- 
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