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

danny0405 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 d6c871bc4d71 fix(hive-sync): fail HiveQL-mode sync when Hive rejects a 
statement (#20145)
d6c871bc4d71 is described below

commit d6c871bc4d710d1ead43970adcdd7b952e28c490
Author: Shihuan Liu <[email protected]>
AuthorDate: Wed Sep 30 03:45:47 2026 -0700

    fix(hive-sync): fail HiveQL-mode sync when Hive rejects a statement (#20145)
    
    * fix(hive-sync): fail HiveQL-mode sync when Hive rejects a statement
    
    Hive's Driver.run() reports compile and execution failures through a
    non-zero CommandProcessorResponse rather than by throwing. HiveQL-mode
    sync discarded the response on both the sequential path and the
    HiveDriverPool path, so a rejected ALTER TABLE ... ADD PARTITION or
    REPLACE COLUMNS was logged by Hive and otherwise ignored: the sync
    reported success, advanced last_commit_time_sync past the failed DDL,
    and recreate_table_on_error never triggered.
    
    Check the response code for every statement and throw
    HoodieHiveSyncException with Hive's error message, SQLState and the
    (abbreviated) statement, matching how the JDBC and HMS modes already
    fail on SQLException and TException.
    
    * Let the sequential path wrap the rejection like the pool path does
    * Rename HiveDriverUtil.runOrThrow to HiveStatementExecutor.executeOrThrow
    * Drop the unreachable null check on the session Driver
    * Use ExceptionUtil.validateErrorMsg in the HiveQL rejection test
---
 .../apache/hudi/hive/ddl/HiveQueryDDLExecutor.java | 10 ++--
 .../org/apache/hudi/hive/util/HiveDriverPool.java  |  4 +-
 .../hudi/hive/util/HiveStatementExecutor.java      | 63 ++++++++++++++++++++++
 .../org/apache/hudi/hive/TestHiveSyncTool.java     | 29 ++++++++++
 .../hive/ddl/TestHiveQueryDDLExecutorSession.java  | 35 +++++++++++-
 .../apache/hudi/hive/util/TestHiveDriverPool.java  | 55 ++++++++++++++++---
 6 files changed, 180 insertions(+), 16 deletions(-)

diff --git 
a/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/ddl/HiveQueryDDLExecutor.java
 
b/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/ddl/HiveQueryDDLExecutor.java
index 32ebee0ead00..ec674db4daba 100644
--- 
a/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/ddl/HiveQueryDDLExecutor.java
+++ 
b/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/ddl/HiveQueryDDLExecutor.java
@@ -26,6 +26,7 @@ import org.apache.hudi.hive.HoodieHiveSyncException;
 import org.apache.hudi.hive.util.HiveDriverPool;
 import org.apache.hudi.hive.util.HiveMetaStoreClientPool;
 import org.apache.hudi.hive.util.HivePartitionUtil;
+import org.apache.hudi.hive.util.HiveStatementExecutor;
 
 import lombok.extern.slf4j.Slf4j;
 import org.apache.hadoop.hive.conf.HiveConf;
@@ -34,7 +35,6 @@ import org.apache.hadoop.hive.metastore.api.FieldSchema;
 import org.apache.hadoop.hive.metastore.api.Table;
 import org.apache.hadoop.hive.ql.Driver;
 import org.apache.hadoop.hive.ql.metadata.Hive;
-import org.apache.hadoop.hive.ql.processors.CommandProcessorResponse;
 import org.apache.hadoop.hive.ql.session.SessionState;
 import org.apache.hadoop.security.UserGroupInformation;
 
@@ -181,17 +181,14 @@ public class HiveQueryDDLExecutor extends 
QueryBasedDDLExecutor {
    * bound for the duration, and the thread handed back as found: it may 
belong to another executor
    * or to an application that embeds this sync and holds a session of its own.
    */
-  private List<CommandProcessorResponse> updateHiveSQLs(List<String> sqls) {
-    List<CommandProcessorResponse> responses = new ArrayList<>();
+  private void updateHiveSQLs(List<String> sqls) {
     HoodieTimer timer = HoodieTimer.start();
     SessionState previousSession = SessionState.get();
     ClassLoader previousLoader = 
Thread.currentThread().getContextClassLoader();
     try {
       SessionState.setCurrentSessionState(sessionState);
       for (String sql : sqls) {
-        if (hiveDriver != null) {
-          responses.add(hiveDriver.run(sql));
-        }
+        HiveStatementExecutor.executeOrThrow(hiveDriver, sql);
       }
     } catch (Exception e) {
       throw new HoodieHiveSyncException("Failed in executing SQL", e);
@@ -199,7 +196,6 @@ public class HiveQueryDDLExecutor extends 
QueryBasedDDLExecutor {
       restoreThread(previousSession, previousLoader);
     }
     log.info("Executed {} SQL statements sequentially in {} ms", sqls.size(), 
timer.endTimer());
-    return responses;
   }
 
   //TODO Duplicating it here from HMSDLExecutor as HiveQueryQL has no way of 
doing it on its own currently. Need to refactor it
diff --git 
a/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/util/HiveDriverPool.java
 
b/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/util/HiveDriverPool.java
index e73eee19cf5e..303af59e699c 100644
--- 
a/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/util/HiveDriverPool.java
+++ 
b/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/util/HiveDriverPool.java
@@ -122,7 +122,7 @@ public class HiveDriverPool implements AutoCloseable {
     for (Worker worker : workers) {
       dispatch.add(worker.executor.submit(dispatch.guard(() -> {
         for (String sql : setupSqls) {
-          worker.driver.run(sql);
+          HiveStatementExecutor.executeOrThrow(worker.driver, sql);
         }
         return null;
       }, "Skipped after an earlier setup statement failed")));
@@ -153,7 +153,7 @@ public class HiveDriverPool implements AutoCloseable {
       String sql = sqls.get(i);
       Worker worker = workers.get(i % workers.size());
       dispatch.add(worker.executor.submit(dispatch.guard(() -> {
-        worker.driver.run(sql);
+        HiveStatementExecutor.executeOrThrow(worker.driver, sql);
         return null;
       }, "Skipped after an earlier statement failed")));
     }
diff --git 
a/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/util/HiveStatementExecutor.java
 
b/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/util/HiveStatementExecutor.java
new file mode 100644
index 000000000000..b852a00ce3ef
--- /dev/null
+++ 
b/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/util/HiveStatementExecutor.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hudi.hive.util;
+
+import org.apache.hudi.hive.HoodieHiveSyncException;
+
+import org.apache.hadoop.hive.ql.CommandNeedRetryException;
+import org.apache.hadoop.hive.ql.Driver;
+import org.apache.hadoop.hive.ql.processors.CommandProcessorResponse;
+
+/**
+ * Runs HiveQL through a Hive {@link Driver}.
+ */
+public final class HiveStatementExecutor {
+
+  /**
+   * Batched partition statements can run to many kilobytes; the error only 
needs enough of the
+   * statement to identify it.
+   */
+  static final int MAX_STATEMENT_LENGTH_IN_ERROR = 1000;
+
+  private HiveStatementExecutor() {
+  }
+
+  /**
+   * Runs the statement and throws if Hive rejects it. {@link 
Driver#run(String)} reports compile
+   * and execution failures (a partition spec that names a non-partition 
column, a DDL task the
+   * metastore refuses) through a non-zero response code rather than by 
throwing, so a caller that
+   * only catches exceptions goes on as though the statement had been applied.
+   */
+  public static void executeOrThrow(Driver driver, String sql) throws 
CommandNeedRetryException {
+    CommandProcessorResponse response = driver.run(sql);
+    if (response.getResponseCode() != 0) {
+      throw new HoodieHiveSyncException(String.format(
+          "Hive rejected the statement with response code %d (SQLState %s): 
%s%nStatement: %s",
+          response.getResponseCode(), response.getSQLState(), 
response.getErrorMessage(), abbreviate(sql)),
+          response.getException());
+    }
+  }
+
+  private static String abbreviate(String sql) {
+    if (sql.length() <= MAX_STATEMENT_LENGTH_IN_ERROR) {
+      return sql;
+    }
+    return sql.substring(0, MAX_STATEMENT_LENGTH_IN_ERROR) + "... (" + 
sql.length() + " characters)";
+  }
+}
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 15029094ef75..0bb527d0b013 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
@@ -41,6 +41,7 @@ import org.apache.hudi.common.util.Option;
 import org.apache.hudi.common.util.StringUtils;
 import org.apache.hudi.common.util.collection.ImmutablePair;
 import org.apache.hudi.common.util.collection.Pair;
+import org.apache.hudi.exception.ExceptionUtil;
 import org.apache.hudi.exception.HoodieException;
 import org.apache.hudi.hadoop.HoodieParquetInputFormat;
 import org.apache.hudi.hadoop.realtime.HoodieParquetRealtimeInputFormat;
@@ -403,6 +404,34 @@ public class TestHiveSyncTool {
         "Incremental add via parallel HiveQL batching should sync the new 
partitions");
   }
 
+  /**
+   * Hive's Driver reports a rejected statement through its response code 
rather than by throwing.
+   * A table registered with a different partition column than the one the 
sync is configured with
+   * rejects every ADD PARTITION, and the sync must fail instead of recording 
the commit as synced.
+   */
+  @ParameterizedTest
+  @ValueSource(booleans = {false, true})
+  void testHiveQLSyncFailsWhenHiveRejectsAStatement(boolean batchingEnabled) 
throws Exception {
+    hiveSyncProps.setProperty(HIVE_SYNC_MODE.key(), 
HiveSyncMode.HIVEQL.name());
+    hiveSyncProps.setProperty(HIVE_SYNC_BATCHING_ENABLED.key(), 
String.valueOf(batchingEnabled));
+    hiveSyncProps.setProperty(META_SYNC_PARTITION_FIELDS.key(), 
HoodieRecord.PARTITION_PATH_METADATA_FIELD);
+    HiveTestUtil.createCOWTable("100", 1, true);
+    reInitHiveSyncClient();
+    reSyncHiveTable();
+    assertEquals("100", 
hiveClient.getLastCommitTimeSynced(HiveTestUtil.TABLE_NAME).get());
+
+    hiveSyncProps.setProperty(META_SYNC_PARTITION_FIELDS.key(), "datestr");
+    HiveTestUtil.addCOWPartition("2050/01/01", true, true, "101");
+    reInitHiveSyncClient();
+
+    HoodieException ex = assertThrows(HoodieException.class, 
this::reSyncHiveTable);
+    assertTrue(ExceptionUtil.validateErrorMsg(ex, "Hive rejected the 
statement"),
+        "The sync must fail with Hive's rejection of the ADD PARTITION");
+    assertTrue(ExceptionUtil.validateErrorMsg(ex, "datestr"));
+    assertEquals("100", 
hiveClient.getLastCommitTimeSynced(HiveTestUtil.TABLE_NAME).get(),
+        "A commit whose partitions Hive rejected must not be recorded as 
synced");
+  }
+
   /**
    * Exercises the DROP path in HiveQL mode with batching on. DROP routes 
through
    * IMetaStoreClient.dropPartition (Thrift, not Hive Driver), so when 
batching is
diff --git 
a/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/ddl/TestHiveQueryDDLExecutorSession.java
 
b/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/ddl/TestHiveQueryDDLExecutorSession.java
index 74687d081ea0..2c5947e10014 100644
--- 
a/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/ddl/TestHiveQueryDDLExecutorSession.java
+++ 
b/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/ddl/TestHiveQueryDDLExecutorSession.java
@@ -28,6 +28,7 @@ import org.apache.hadoop.hive.conf.HiveConf;
 import org.apache.hadoop.hive.metastore.IMetaStoreClient;
 import org.apache.hadoop.hive.ql.Driver;
 import org.apache.hadoop.hive.ql.metadata.Hive;
+import org.apache.hadoop.hive.ql.processors.CommandProcessorResponse;
 import org.apache.hadoop.hive.ql.session.SessionState;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
@@ -158,7 +159,7 @@ class TestHiveQueryDDLExecutorSession {
     when(driver.run(anyString())).thenAnswer(invocation -> {
       sessionsSeenByDriver.add(SessionState.get());
       loadersSeenByDriver.add(Thread.currentThread().getContextClassLoader());
-      return null;
+      return new CommandProcessorResponse(0);
     });
     HiveQueryDDLExecutor executor = executorWith(driver, sessionState);
     SessionState.detachSession();
@@ -186,6 +187,7 @@ class TestHiveQueryDDLExecutorSession {
     when(otherSession.getConf()).thenReturn(new HiveConf());
     SessionState sessionState = mock(SessionState.class);
     Driver driver = mock(Driver.class);
+    when(driver.run(anyString())).thenReturn(new CommandProcessorResponse(0));
     HiveQueryDDLExecutor executor = executorWith(driver, sessionState);
     SessionState.setCurrentSessionState(otherSession);
     ClassLoader callerLoader = Thread.currentThread().getContextClassLoader();
@@ -198,6 +200,37 @@ class TestHiveQueryDDLExecutorSession {
     verify(driver, times(1)).run("SHOW TABLES");
   }
 
+  /**
+   * Driver.run() reports a statement Hive rejects through its response rather 
than by throwing.
+   * The sync has to stop there: the statements after it assume it was 
applied, and a sync that
+   * carries on records the commit as synced to a table that never received it.
+   */
+  @Test
+  void statementHiveRejectsFailsTheSyncAndHandsTheThreadBack() throws 
Exception {
+    SessionState otherSession = mock(SessionState.class);
+    when(otherSession.getConf()).thenReturn(new HiveConf());
+    SessionState sessionState = mock(SessionState.class);
+    Driver driver = mock(Driver.class);
+    String rejected = "ALTER TABLE `tbl` ADD IF NOT EXISTS PARTITION 
(`datestr`='2026-09-13')";
+    when(driver.run(rejected)).thenReturn(new CommandProcessorResponse(10,
+        "FAILED: ValidationFailureSemanticException Partition spec 
{datestr=2026-09-13} contains non-partition columns",
+        "42000"));
+    HiveQueryDDLExecutor executor = executorWith(driver, sessionState);
+    SessionState.setCurrentSessionState(otherSession);
+    ClassLoader callerLoader = Thread.currentThread().getContextClassLoader();
+
+    HoodieHiveSyncException ex = assertThrows(HoodieHiveSyncException.class,
+        () -> executor.runSQLs(Arrays.asList(rejected, "ALTER TABLE `tbl` SET 
TBLPROPERTIES ('k'='v')")));
+
+    String message = ex.getCause().getMessage();
+    assertTrue(message.contains("contains non-partition columns"), message);
+    assertTrue(message.contains(rejected), "The error must name the statement; 
was " + message);
+    verify(driver, times(1)).run(anyString());
+    assertSame(otherSession, SessionState.get(), "The session held before the 
statements must be put back");
+    assertSame(callerLoader, Thread.currentThread().getContextClassLoader(),
+        "The class loader held before the statements must be put back");
+  }
+
   /**
    * Driver.close() clears the current session's lineage state and 
Driver.destroy() takes its
    * transaction manager to release locks, so both have to see the session 
that owns the Driver.
diff --git 
a/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/util/TestHiveDriverPool.java
 
b/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/util/TestHiveDriverPool.java
index efb99c5d5bbe..6244f7005400 100644
--- 
a/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/util/TestHiveDriverPool.java
+++ 
b/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/util/TestHiveDriverPool.java
@@ -24,6 +24,7 @@ import org.apache.hudi.hive.HoodieHiveSyncException;
 
 import org.apache.hadoop.hive.conf.HiveConf;
 import org.apache.hadoop.hive.ql.Driver;
+import org.apache.hadoop.hive.ql.processors.CommandProcessorResponse;
 import org.junit.jupiter.api.Test;
 import org.mockito.invocation.InvocationOnMock;
 
@@ -56,6 +57,8 @@ import static org.mockito.Mockito.when;
  */
 class TestHiveDriverPool {
 
+  private static final CommandProcessorResponse SUCCESS = new 
CommandProcessorResponse(0);
+
   private static HiveSyncConfig configWithEmptyHiveConf() {
     HiveSyncConfig config = mock(HiveSyncConfig.class);
     doAnswer(inv -> new HiveConf()).when(config).getHiveConf();
@@ -104,7 +107,7 @@ class TestHiveDriverPool {
       seenThreadsByDriver.put(d, ConcurrentHashMap.newKeySet());
       doAnswer((InvocationOnMock inv) -> {
         seenThreadsByDriver.get(d).add(Thread.currentThread().getName());
-        return null;
+        return SUCCESS;
       }).when(d).run(anyString());
       return d;
     };
@@ -132,7 +135,7 @@ class TestHiveDriverPool {
         if (sql.equals("FAIL")) {
           throw new RuntimeException("boom: " + sql);
         }
-        return null;
+        return SUCCESS;
       }).when(d).run(anyString());
       return d;
     };
@@ -145,6 +148,46 @@ class TestHiveDriverPool {
     }
   }
 
+  /**
+   * Driver.run() reports a statement Hive rejects through its response rather 
than by throwing,
+   * so a batch that only watched for exceptions would report the sync as 
applied.
+   */
+  @Test
+  void awaitAllThrowsWhenHiveRejectsAStatement() throws Exception {
+    HiveSyncConfig config = configWithEmptyHiveConf();
+    HiveDriverPool.DriverFactory factory = (db) -> {
+      Driver d = mock(Driver.class);
+      doAnswer(inv -> "REJECTED".equals(inv.getArgument(0))
+          ? new CommandProcessorResponse(10, "FAILED: SemanticException table 
is not partitioned", "42000")
+          : SUCCESS).when(d).run(anyString());
+      return d;
+    };
+    try (HiveDriverPool pool = new HiveDriverPool(config, 2, factory)) {
+      ParallelDispatch dispatch = pool.dispatchAll(Arrays.asList("OK", 
"REJECTED", "OK"));
+      HoodieHiveSyncException ex = assertThrows(HoodieHiveSyncException.class,
+          () -> pool.awaitAll(dispatch));
+      String message = ex.getCause().getMessage();
+      assertTrue(message.contains("SemanticException table is not 
partitioned"), message);
+      assertTrue(message.contains("REJECTED"), "The error must name the 
statement; was " + message);
+    }
+  }
+
+  @Test
+  void runOnEachWorkerThrowsWhenHiveRejectsASetupStatement() throws Exception {
+    HiveSyncConfig config = configWithEmptyHiveConf();
+    HiveDriverPool.DriverFactory factory = (db) -> {
+      Driver d = mock(Driver.class);
+      when(d.run(anyString())).thenReturn(
+          new CommandProcessorResponse(10, "FAILED: SemanticException Database 
does not exist: db1", "42000"));
+      return d;
+    };
+    try (HiveDriverPool pool = new HiveDriverPool(config, 2, factory)) {
+      HoodieHiveSyncException ex = assertThrows(HoodieHiveSyncException.class,
+          () -> pool.runOnEachWorker(Collections.singletonList("USE `db1`")));
+      assertTrue(ex.getCause().getMessage().contains("Database does not exist: 
db1"));
+    }
+  }
+
   @Test
   void concurrentDispatchBoundedByPoolSize() throws Exception {
     HiveSyncConfig config = configWithEmptyHiveConf();
@@ -158,7 +201,7 @@ class TestHiveDriverPool {
         maxInFlight.updateAndGet(prev -> Math.max(prev, now));
         hold.await(2, TimeUnit.SECONDS);
         inFlight.decrementAndGet();
-        return null;
+        return SUCCESS;
       }).when(d).run(anyString());
       return d;
     };
@@ -258,7 +301,7 @@ class TestHiveDriverPool {
       sqlsByDriver.put(d, java.util.Collections.synchronizedList(new 
java.util.ArrayList<>()));
       doAnswer((InvocationOnMock inv) -> {
         sqlsByDriver.get(d).add(inv.getArgument(0));
-        return null;
+        return SUCCESS;
       }).when(d).run(anyString());
       return d;
     };
@@ -296,7 +339,7 @@ class TestHiveDriverPool {
         if (sql.equals("FAIL")) {
           throw new RuntimeException("boom");
         }
-        return null;
+        return SUCCESS;
       }).when(d).run(anyString());
       return d;
     };
@@ -346,7 +389,7 @@ class TestHiveDriverPool {
           // parked on future 0 at the moment worker 1 would pick up 
AFTER_FAIL.
           releaseSlow.await(5, TimeUnit.SECONDS);
         }
-        return null;
+        return SUCCESS;
       }).when(d).run(anyString());
       return d;
     };

Reply via email to