This is an automated email from the ASF dual-hosted git repository.

voonhous pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git


The following commit(s) were added to refs/heads/master by this push:
     new cb0f9b18c3da feat(hive-sync): add config to force-recreate the 
metastore table on every sync (#19426)
cb0f9b18c3da is described below

commit cb0f9b18c3da23c6ddb9d6d45f64a216f11fb122
Author: Y Ethan Guo <[email protected]>
AuthorDate: Fri Jul 31 02:06:22 2026 -0700

    feat(hive-sync): add config to force-recreate the metastore table on every 
sync (#19426)
    
    * feat(hive-sync): add config to force-recreate the metastore table on 
every sync
    
    Add hoodie.meta.sync.force.recreate.table to HoodieSyncConfig. When true,
    HiveSyncTool always drops and recreates the metastore table via the existing
    createOrReplaceTable path on every sync of a table that already exists,
    regardless of whether the incremental sync would otherwise succeed or be a
    no-op. This gives operators a way to force a full resync of the table schema
    and properties, e.g. after the table drifted from the Hoodie table
    definition out-of-band.
    
    * Address review comments: republish hudi_writer_version on recreate, 
rename shouldRecreateTable to shouldRecreateTableBeforeSync
    
    * Address review: assert hudi_writer_version is republished by 
force-recreate
---
 .../java/org/apache/hudi/hive/HiveSyncTool.java    | 25 ++++++++++++--
 .../org/apache/hudi/hive/TestHiveSyncTool.java     | 38 +++++++++++++++++++++-
 .../apache/hudi/sync/common/HoodieSyncConfig.java  | 10 ++++++
 3 files changed, 69 insertions(+), 4 deletions(-)

diff --git 
a/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/HiveSyncTool.java 
b/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/HiveSyncTool.java
index 1c6360db3580..21b361d16dfc 100644
--- 
a/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/HiveSyncTool.java
+++ 
b/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/HiveSyncTool.java
@@ -73,6 +73,7 @@ import static 
org.apache.hudi.hive.util.HiveSchemaUtil.getSchemaDifference;
 import static 
org.apache.hudi.sync.common.HoodieSyncConfig.META_SYNC_BASE_FILE_FORMAT;
 import static org.apache.hudi.sync.common.HoodieSyncConfig.META_SYNC_BASE_PATH;
 import static 
org.apache.hudi.sync.common.HoodieSyncConfig.META_SYNC_CONDITIONAL_SYNC;
+import static 
org.apache.hudi.sync.common.HoodieSyncConfig.META_SYNC_FORCE_RECREATE_TABLE;
 import static 
org.apache.hudi.sync.common.HoodieSyncConfig.META_SYNC_INCREMENTAL;
 import static 
org.apache.hudi.sync.common.HoodieSyncConfig.META_SYNC_PARTITION_FIELDS;
 import static 
org.apache.hudi.sync.common.HoodieSyncConfig.META_SYNC_SNAPSHOT_WITH_TABLE_NAME;
@@ -239,9 +240,9 @@ public class HiveSyncTool extends HoodieSyncTool implements 
AutoCloseable {
     log.info("Trying to sync hoodie table {} with base path {} of type {}", 
tableName, syncClient.getBasePath(), syncClient.getTableType());
 
     final boolean tableExists = syncClient.tableExists(tableName);
-    // if table exists and location of the metastore table doesn't match the 
hoodie base path, recreate the table
-    if (tableExists && 
!FSUtils.comparePathsWithoutScheme(syncClient.getBasePath(), 
syncClient.getTableLocation(tableName))) {
-      log.info("basepath is updated for the table {}", tableName);
+    // recreate the table if it exists and either its metastore location no 
longer matches the hoodie base path,
+    // or a full recreate was explicitly requested
+    if (tableExists && shouldRecreateTableBeforeSync(tableName)) {
       recreateAndSyncHiveTable(tableName, useRealtimeInputFormat, 
readAsOptimized);
       return;
     }
@@ -360,6 +361,23 @@ public class HiveSyncTool extends HoodieSyncTool 
implements AutoCloseable {
     return config.getBooleanOrDefault(RECREATE_HIVE_TABLE_ON_ERROR);
   }
 
+  /**
+   * Whether the metastore table (assumed to already exist) should be dropped 
and recreated before the normal
+   * sync runs, either because a full recreate was explicitly requested, or 
because its stored location no
+   * longer matches the hoodie base path.
+   */
+  private boolean shouldRecreateTableBeforeSync(String tableName) {
+    if (config.getBooleanOrDefault(META_SYNC_FORCE_RECREATE_TABLE)) {
+      log.info("Force recreating the table {} since {} is set to true", 
tableName, META_SYNC_FORCE_RECREATE_TABLE.key());
+      return true;
+    }
+    if (!FSUtils.comparePathsWithoutScheme(syncClient.getBasePath(), 
syncClient.getTableLocation(tableName))) {
+      log.info("basepath is updated for the table {}", tableName);
+      return true;
+    }
+    return false;
+  }
+
   private void recreateAndSyncHiveTable(String tableName, boolean 
useRealtimeInputFormat, boolean readAsOptimized) {
     log.info("recreating and syncing the table {}", tableName);
     Timer.Context timerContext = metrics.getRecreateAndSyncTimer();
@@ -368,6 +386,7 @@ public class HiveSyncTool extends HoodieSyncTool implements 
AutoCloseable {
       createOrReplaceTable(tableName, useRealtimeInputFormat, readAsOptimized, 
schema);
       syncAllPartitions(tableName);
       syncClient.updateLastCommitTimeSynced(tableName);
+      syncClient.updateHoodieWriterVersion(tableName);
       if (Objects.nonNull(timerContext)) {
         long durationInNs = timerContext.stop();
         metrics.updateRecreateAndSyncDurationInMs(durationInNs);
diff --git 
a/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/TestHiveSyncTool.java
 
b/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/TestHiveSyncTool.java
index 4f109c9cdd64..50384913fb82 100644
--- 
a/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/TestHiveSyncTool.java
+++ 
b/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/TestHiveSyncTool.java
@@ -116,6 +116,7 @@ import static 
org.apache.hudi.hive.testutils.HiveTestUtil.hiveSyncProps;
 import static org.apache.hudi.sync.common.HoodieSyncConfig.META_SYNC_BASE_PATH;
 import static 
org.apache.hudi.sync.common.HoodieSyncConfig.META_SYNC_CONDITIONAL_SYNC;
 import static 
org.apache.hudi.sync.common.HoodieSyncConfig.META_SYNC_DATABASE_NAME;
+import static 
org.apache.hudi.sync.common.HoodieSyncConfig.META_SYNC_FORCE_RECREATE_TABLE;
 import static 
org.apache.hudi.sync.common.HoodieSyncConfig.META_SYNC_INCREMENTAL;
 import static 
org.apache.hudi.sync.common.HoodieSyncConfig.META_SYNC_PARTITION_EXTRACTOR_CLASS;
 import static 
org.apache.hudi.sync.common.HoodieSyncConfig.META_SYNC_PARTITION_FIELDS;
@@ -271,7 +272,42 @@ public class TestHiveSyncTool {
       throw new HoodieHiveSyncException("Failed to get the metastore location 
from the table " + tableName, e);
     }
   }
-  
+
+  @ParameterizedTest
+  @MethodSource("syncMode")
+  void testForceRecreateTable(String syncMode) throws Exception {
+    hiveSyncProps.setProperty(HIVE_SYNC_MODE.key(), syncMode);
+    String instantTime = "100";
+    HiveTestUtil.createCOWTable(instantTime, 1, true);
+    reInitHiveSyncClient();
+    reSyncHiveTable();
+    assertTrue(hiveClient.tableExists(HiveTestUtil.TABLE_NAME));
+
+    // simulate manual drift on the metastore table that a fully-synced 
incremental sync would never touch
+    ddlExecutor.runSQL("ALTER TABLE `" + HiveTestUtil.TABLE_NAME + "` SET 
TBLPROPERTIES ('drift_marker'='true')");
+    IMetaStoreClient client = IMetaStoreClientUtil.getMSC(getHiveConf());
+    assertTrue(client.getTable(HiveTestUtil.DB_NAME, 
HiveTestUtil.TABLE_NAME).getParameters().containsKey("drift_marker"));
+
+    // nothing changed on the Hoodie timeline, so a normal sync is a no-op and 
leaves the drift behind
+    reInitHiveSyncClient();
+    reSyncHiveTable();
+    client.reconnect();
+    assertTrue(client.getTable(HiveTestUtil.DB_NAME, 
HiveTestUtil.TABLE_NAME).getParameters().containsKey("drift_marker"),
+        "a no-op incremental sync must not touch the table");
+
+    // forcing recreation drops and rebuilds the table from scratch, even 
though nothing else changed
+    hiveSyncProps.setProperty(META_SYNC_FORCE_RECREATE_TABLE.key(), "true");
+    reInitHiveSyncClient();
+    reSyncHiveTable();
+    client.reconnect();
+    assertFalse(client.getTable(HiveTestUtil.DB_NAME, 
HiveTestUtil.TABLE_NAME).getParameters().containsKey("drift_marker"),
+        "force-recreate must drop and recreate the table even when the 
incremental sync would otherwise be a no-op");
+    assertEquals(HoodieVersion.get(), client.getTable(HiveTestUtil.DB_NAME, 
HiveTestUtil.TABLE_NAME).getParameters().get(HoodieVersion.HOODIE_WRITER_VERSION),
+        "force-recreate must republish the hudi writer version on the 
recreated table");
+    assertEquals(instantTime, 
hiveClient.getLastCommitTimeSynced(HiveTestUtil.TABLE_NAME).get());
+    client.close();
+  }
+
   @ParameterizedTest
   @MethodSource("syncMode")
   public void testSyncAllPartition() throws Exception {
diff --git 
a/hudi-sync/hudi-sync-common/src/main/java/org/apache/hudi/sync/common/HoodieSyncConfig.java
 
b/hudi-sync/hudi-sync-common/src/main/java/org/apache/hudi/sync/common/HoodieSyncConfig.java
index 2e46a3007b72..6952d245711f 100644
--- 
a/hudi-sync/hudi-sync-common/src/main/java/org/apache/hudi/sync/common/HoodieSyncConfig.java
+++ 
b/hudi-sync/hudi-sync-common/src/main/java/org/apache/hudi/sync/common/HoodieSyncConfig.java
@@ -182,6 +182,16 @@ public class HoodieSyncConfig extends HoodieConfig {
       .withDocumentation("If true, TOUCH partition events will be emitted 
during meta sync. "
           + "TOUCH events indicate partitions that exist in both storage and 
metastore, no schema or location change, but the partition has received data.");
 
+  public static final ConfigProperty<Boolean> META_SYNC_FORCE_RECREATE_TABLE = 
ConfigProperty
+      .key("hoodie.meta.sync.force.recreate.table")
+      .defaultValue(false)
+      .sinceVersion("1.3.0")
+      .markAdvanced()
+      .withDocumentation("If true, always drop and recreate the table on every 
sync, regardless of whether "
+          + "the incremental sync would otherwise succeed. Useful for forcing 
a full resync of the table schema "
+          + "and properties to the metastore, e.g. after the table drifted 
from the Hoodie table definition "
+          + "out-of-band.");
+
   @Getter
   @Setter
   private Configuration hadoopConf;

Reply via email to