hudi-agent commented on code in PR #19206:
URL: https://github.com/apache/hudi/pull/19206#discussion_r3810296261


##########
hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestTableCommand.java:
##########
@@ -304,4 +307,316 @@ private String getFileContent(String fileToReadStr) 
throws IOException {
     fis.close();
     return fromUTF8Bytes(data);
   }
+
+  // 
---------------------------------------------------------------------------
+  // set-meta-fields-mode
+  // 
---------------------------------------------------------------------------
+
+  @Test
+  public void testSetMetaFieldsModeOnFreshTableToCommitTimeOnly() {
+    assertTrue(prepareTable());
+    // Default table is ALL — no commits yet, so the safety check must let 
this through.
+    Object result = shell.evaluate(() ->
+        "table set-meta-fields-mode --target-mode COMMIT_TIME_ONLY");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(result));
+    // Rendered diff must surface the changed properties so the operator can 
confirm the write.
+    String rendered = result.toString();
+    assertTrue(rendered.contains(HoodieTableConfig.POPULATE_META_FIELDS.key()),
+        "expected rendered diff to mention populate.meta.fields, got: " + 
rendered);
+    assertTrue(rendered.contains(HoodieTableConfig.META_FIELDS_MODE.key()),
+        "expected rendered diff to mention meta.fields.mode, got: " + 
rendered);
+    HoodieTableMetaClient client = HoodieCLI.getTableMetaClient();
+    assertEquals(MetaFieldsMode.COMMIT_TIME_ONLY, 
client.getTableConfig().getMetaFieldsMode());
+    assertFalse(client.getTableConfig().populateMetaFields());
+  }
+
+  @Test
+  public void testSetMetaFieldsModeOnFreshTableToFileNameOnly() {
+    assertTrue(prepareTable());
+    Object result = shell.evaluate(() ->
+        "table set-meta-fields-mode --target-mode FILE_NAME_ONLY");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(result));
+    assertEquals(MetaFieldsMode.FILE_NAME_ONLY,
+        HoodieCLI.getTableMetaClient().getTableConfig().getMetaFieldsMode());
+  }
+
+  @Test
+  public void testSetMetaFieldsModeOnFreshTableToCombinedMode() {
+    assertTrue(prepareTable());
+    Object result = shell.evaluate(() ->
+        "table set-meta-fields-mode --target-mode COMMIT_TIME_AND_FILE_NAME");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(result));
+    assertEquals(MetaFieldsMode.COMMIT_TIME_AND_FILE_NAME,
+        HoodieCLI.getTableMetaClient().getTableConfig().getMetaFieldsMode());
+  }
+
+  @Test
+  public void testSetMetaFieldsModeOnFreshTableToNone() {
+    assertTrue(prepareTable());
+    Object result = shell.evaluate(() ->
+        "table set-meta-fields-mode --target-mode NONE");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(result));
+    HoodieTableMetaClient client = HoodieCLI.getTableMetaClient();
+    assertEquals(MetaFieldsMode.NONE, 
client.getTableConfig().getMetaFieldsMode());
+    assertFalse(client.getTableConfig().populateMetaFields());
+  }
+
+  @Test
+  public void testSetMetaFieldsModeToAllWritesTheModeExplicitly() throws 
IOException {
+    assertTrue(prepareTable());
+    // First move to a selective mode, then back to ALL. Legal here only 
because the table has no
+    // commits — on a populated table this would be a widening and refused 
outright.
+    shell.evaluate(() -> "table set-meta-fields-mode --target-mode 
COMMIT_TIME_ONLY");
+    Object result = shell.evaluate(() -> "table set-meta-fields-mode 
--target-mode ALL");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(result));
+
+    HoodieTableMetaClient client = HoodieCLI.getTableMetaClient();
+    assertEquals(MetaFieldsMode.ALL, 
client.getTableConfig().getMetaFieldsMode());
+    assertTrue(client.getTableConfig().populateMetaFields());
+    // The mode is written explicitly rather than deleted. Deleting it would 
leave the table
+    // resolving through the legacy fallback -- indistinguishable from a table 
predating the
+    // property -- and would make this command's effect invisible in 
hoodie.properties. The v10->v9
+    // downgrade handler also reads the mode to derive the boolean it writes 
back.
+    assertEquals(MetaFieldsMode.ALL.name(),
+        client.getTableConfig().getString(HoodieTableConfig.META_FIELDS_MODE));
+  }
+
+  @Test
+  public void 
testSetMetaFieldsModeRefusesWideningOnPopulatedTableEvenWithForce() throws 
Exception {
+    assertTrue(prepareTable());
+    shell.evaluate(() -> "table set-meta-fields-mode --target-mode 
COMMIT_TIME_ONLY");
+    createDummyCommitFile("20260101000000000");
+    HoodieCLI.refreshTableMetadata();
+
+    // Widening is one-way-forbidden and --force does not override it: 
existing files are not
+    // rewritten, so the table would advertise a column that is null for every 
row written so far,
+    // and incremental queries would then admit the table and silently skip 
those rows. There is no
+    // consequence for an operator to knowingly accept, so this is a hard 
failure.
+    for (String command : new String[] {
+        "table set-meta-fields-mode --target-mode ALL",
+        "table set-meta-fields-mode --target-mode ALL --force true",
+        "table set-meta-fields-mode --target-mode COMMIT_TIME_AND_FILE_NAME 
--force true"}) {
+      Object result = shell.evaluate(() -> command);
+      assertFalse(ShellEvaluationResultUtil.isSuccess(result), "expected 
refusal for: " + command);
+      assertTrue(result.toString().contains("widen"),
+          "expected a widening refusal for '" + command + "', got: " + result);
+      assertEquals(MetaFieldsMode.COMMIT_TIME_ONLY,
+          HoodieCLI.getTableMetaClient().getTableConfig().getMetaFieldsMode(),
+          "mode must be unchanged after a refused widening");
+    }
+  }
+
+  @Test
+  public void testSetMetaFieldsModeAllowsNarrowingOnPopulatedTableWithForce() 
throws Exception {
+    assertTrue(prepareTable());
+    shell.evaluate(() -> "table set-meta-fields-mode --target-mode 
COMMIT_TIME_AND_FILE_NAME");
+    createDummyCommitFile("20260101000000000");
+    HoodieCLI.refreshTableMetadata();
+
+    // Narrowing is the direction the CLI exists to allow. It still needs 
--force, because it leaves
+    // mixed-mode files, but it is not refused outright the way widening is.
+    Object refused = shell.evaluate(() ->
+        "table set-meta-fields-mode --target-mode COMMIT_TIME_ONLY");
+    assertFalse(ShellEvaluationResultUtil.isSuccess(refused));
+
+    Object forced = shell.evaluate(() ->
+        "table set-meta-fields-mode --target-mode COMMIT_TIME_ONLY --force 
true");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(forced), "narrowing with 
--force must succeed: " + forced);
+    assertEquals(MetaFieldsMode.COMMIT_TIME_ONLY,
+        HoodieCLI.getTableMetaClient().getTableConfig().getMetaFieldsMode());
+  }
+
+  @Test
+  public void testSetMetaFieldsModeAcceptsLowercaseTargetMode() {
+    assertTrue(prepareTable());
+    // Routed through MetaFieldsMode.resolve rather than valueOf, so an 
operator typing the mode in
+    // lower case is not rejected.
+    Object result = shell.evaluate(() ->
+        "table set-meta-fields-mode --target-mode commit_time_only");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(result), "expected 
lowercase to be accepted: " + result);
+    assertEquals(MetaFieldsMode.COMMIT_TIME_ONLY,
+        HoodieCLI.getTableMetaClient().getTableConfig().getMetaFieldsMode());
+  }
+
+  @Test
+  public void testSetMetaFieldsModeKeepsBothPropertiesInAgreement() {
+    assertTrue(prepareTable());
+    // hoodie.properties must never contradict itself: the legacy boolean is 
derived from the mode,
+    // never taken from the caller, so a pre-1.3.0 reader that sees only the 
boolean treats a
+    // selective table as NONE rather than assuming meta columns that are 
physically null.
+    for (MetaFieldsMode mode : MetaFieldsMode.values()) {
+      shell.evaluate(() -> "table set-meta-fields-mode --target-mode " + 
mode.name() + " --force true");
+      HoodieTableConfig tableConfig = 
HoodieCLI.getTableMetaClient().getTableConfig();
+      if (tableConfig.getMetaFieldsMode() == mode) {
+        assertEquals(mode.toLegacyPopulateMetaFields(), 
tableConfig.populateMetaFields(),
+            "populate.meta.fields must be the derived value for mode " + mode);
+      }
+    }
+  }
+
+  @Test
+  public void testSetMetaFieldsModeNoOpWhenAlreadyInTargetMode() {
+    assertTrue(prepareTable());
+    Object first = shell.evaluate(() ->
+        "table set-meta-fields-mode --target-mode COMMIT_TIME_ONLY");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(first));
+    // Second call — same target — should be a no-op message.
+    Object second = shell.evaluate(() ->
+        "table set-meta-fields-mode --target-mode COMMIT_TIME_ONLY");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(second));
+    assertTrue(second.toString().contains("already in COMMIT_TIME_ONLY"),
+        "expected no-op message, got: " + second);
+  }
+
+  @Test
+  public void testSetMetaFieldsModeRejectsUnknownValue() {
+    assertTrue(prepareTable());
+    Object result = shell.evaluate(() ->
+        "table set-meta-fields-mode --target-mode BOGUS_MODE");
+    // Shell evaluate returns the exception object on failure.
+    assertFalse(ShellEvaluationResultUtil.isSuccess(result));
+    assertTrue(result.toString().contains("BOGUS_MODE"),
+        "expected error message to name the rejected value, got: " + result);
+  }
+
+  @Test
+  public void testSetMetaFieldsModeRefusesOnPopulatedTable() throws Exception {
+    assertTrue(prepareTable());
+    createDummyCommitFile("20260101000000000");
+    HoodieCLI.refreshTableMetadata();
+
+    Object result = shell.evaluate(() ->
+        "table set-meta-fields-mode --target-mode COMMIT_TIME_ONLY");
+    assertFalse(ShellEvaluationResultUtil.isSuccess(result));
+    assertTrue(result.toString().contains("Refusing to change") || 
result.toString().contains("--force"),
+        "expected refusal message, got: " + result);
+
+    // Mode must not have changed.
+    assertEquals(MetaFieldsMode.ALL,
+        HoodieCLI.getTableMetaClient().getTableConfig().getMetaFieldsMode());
+  }
+
+  @Test
+  public void testSetMetaFieldsModeWithForceOnPopulatedTable() throws 
Exception {
+    assertTrue(prepareTable());
+    createDummyCommitFile("20260101000000000");
+    HoodieCLI.refreshTableMetadata();
+
+    Object result = shell.evaluate(() ->
+        "table set-meta-fields-mode --target-mode COMMIT_TIME_ONLY --force 
true");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(result));
+    assertEquals(MetaFieldsMode.COMMIT_TIME_ONLY,
+        HoodieCLI.getTableMetaClient().getTableConfig().getMetaFieldsMode());
+  }
+
+  /**
+   * Pins the whole migration matrix rather than spot-checking it. Twenty 
ordered pairs, each either
+   * a legal narrowing or a refused widening, on a table that already has 
commits.
+   *
+   * <p>The subtle entries are the mutually-wider siblings: {@code 
COMMIT_TIME_ONLY} and
+   * {@code FILE_NAME_ONLY} each populate a column the other does not, so 
neither can migrate to the
+   * other in either direction. A spot-check of "narrowing works, widening 
does not" would miss that
+   * the relation is a lattice rather than a chain.
+   *
+   * <p>Uses a fresh table per pair so the starting mode can be set without 
tripping the very guard
+   * under test -- setting the initial mode happens before any commit exists.
+   */
+  @Test
+  public void testSetMetaFieldsModeMigrationMatrixOnPopulatedTable() throws 
Exception {
+    for (MetaFieldsMode from : MetaFieldsMode.values()) {
+      for (MetaFieldsMode to : MetaFieldsMode.values()) {
+        if (from == to) {
+          continue;
+        }
+        // Fresh table per pair; connect to it so HoodieCLI points at the 
right one.
+        String pairName = tableName + "_" + from.name() + "_to_" + to.name();
+        String pairPath = tablePath(pairName);
+        assertTrue(ShellEvaluationResultUtil.isSuccess(
+            shell.evaluate(() -> "create --path " + pairPath + " --tableName " 
+ pairName)));
+
+        // Establish the starting mode while the table is still empty, then 
make it "populated".
+        assertTrue(ShellEvaluationResultUtil.isSuccess(
+            shell.evaluate(() -> "table set-meta-fields-mode --target-mode " + 
from.name())),
+            "setting the initial mode on an empty table must succeed: " + 
from);
+        createDummyCommitFileAt(pairPath, "20260101000000000");
+        HoodieCLI.refreshTableMetadata();
+        assertEquals(from, 
HoodieCLI.getTableMetaClient().getTableConfig().getMetaFieldsMode());
+
+        boolean widening = to.isWiderThan(from);
+        Object result = shell.evaluate(() ->
+            "table set-meta-fields-mode --target-mode " + to.name() + " 
--force true");
+
+        if (widening) {
+          assertFalse(ShellEvaluationResultUtil.isSuccess(result),
+              from + " -> " + to + " adds a meta column and must be refused 
even with --force");
+          assertTrue(result.toString().contains("widen"),
+              "expected a widening refusal for " + from + " -> " + to + ", 
got: " + result);
+          assertEquals(from, 
HoodieCLI.getTableMetaClient().getTableConfig().getMetaFieldsMode(),
+              "a refused migration must leave the mode untouched: " + from + " 
-> " + to);
+        } else {
+          assertTrue(ShellEvaluationResultUtil.isSuccess(result),
+              from + " -> " + to + " drops meta columns and must be allowed 
with --force, got: " + result);
+          HoodieTableConfig tableConfig = 
HoodieCLI.getTableMetaClient().getTableConfig();
+          assertEquals(to, tableConfig.getMetaFieldsMode(), from + " -> " + 
to);
+          assertEquals(to.toLegacyPopulateMetaFields(), 
tableConfig.populateMetaFields(),
+              "the derived boolean must follow the new mode for " + from + " 
-> " + to);
+        }
+      }
+    }
+  }
+
+  /** Both mutually-wider directions between the two single-column modes are 
refused. */
+  @Test
+  public void testSetMetaFieldsModeRefusesBothSiblingDirections() throws 
Exception {
+    assertTrue(prepareTable());
+    shell.evaluate(() -> "table set-meta-fields-mode --target-mode 
COMMIT_TIME_ONLY");
+    createDummyCommitFile("20260101000000000");
+    HoodieCLI.refreshTableMetadata();
+
+    Object toSibling = shell.evaluate(() ->
+        "table set-meta-fields-mode --target-mode FILE_NAME_ONLY --force 
true");
+    assertFalse(ShellEvaluationResultUtil.isSuccess(toSibling),
+        "COMMIT_TIME_ONLY -> FILE_NAME_ONLY adds _hoodie_file_name and must be 
refused");
+    assertEquals(MetaFieldsMode.COMMIT_TIME_ONLY,
+        HoodieCLI.getTableMetaClient().getTableConfig().getMetaFieldsMode());
+
+    // ...and the reverse, on a table that starts the other way round.
+    String otherName = tableName + "_sibling_reverse";
+    String otherPath = tablePath(otherName);
+    assertTrue(ShellEvaluationResultUtil.isSuccess(
+        shell.evaluate(() -> "create --path " + otherPath + " --tableName " + 
otherName)));
+    shell.evaluate(() -> "table set-meta-fields-mode --target-mode 
FILE_NAME_ONLY");
+    createDummyCommitFileAt(otherPath, "20260101000000000");
+    HoodieCLI.refreshTableMetadata();
+
+    Object toOther = shell.evaluate(() ->
+        "table set-meta-fields-mode --target-mode COMMIT_TIME_ONLY --force 
true");
+    assertFalse(ShellEvaluationResultUtil.isSuccess(toOther),
+        "FILE_NAME_ONLY -> COMMIT_TIME_ONLY adds _hoodie_commit_time and must 
be refused");
+    assertEquals(MetaFieldsMode.FILE_NAME_ONLY,
+        HoodieCLI.getTableMetaClient().getTableConfig().getMetaFieldsMode());
+  }
+
+  private void createDummyCommitFileAt(String tableBasePath, String 
instantTime) throws IOException {
+    java.nio.file.Path timelineDir =
+        Paths.get(tableBasePath, METAFOLDER_NAME, "timeline");
+    if (!timelineDir.toFile().exists()) {
+      timelineDir.toFile().mkdirs();
+    }
+    String completionTime = instantTime + "1";
+    java.nio.file.Files.createFile(timelineDir.resolve(instantTime + "_" + 
completionTime + ".commit"));

Review Comment:
   🤖 nit: could `createDummyCommitFile` just delegate to 
`createDummyCommitFileAt(tablePath, instantTime)`? The two methods are 
identical except for how they arrive at the timeline directory, and `tablePath` 
is already a field on the test class.
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



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