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

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


The following commit(s) were added to refs/heads/master by this push:
     new 59b36ea7031 HIVE-14609: HS2 cannot drop a function whose associated 
jar file has been removed (#6447)
59b36ea7031 is described below

commit 59b36ea7031e5275f8d6fe3577fc73cf1c40db59
Author: Bodor Laszlo <[email protected]>
AuthorDate: Thu Jun 4 11:01:05 2026 +0200

    HIVE-14609: HS2 cannot drop a function whose associated jar file has been 
removed (#6447)
---
 .../ql/ddl/function/AbstractFunctionAnalyzer.java  |   5 +
 .../ql/ddl/function/drop/DropFunctionAnalyzer.java |  32 ++++-
 .../function/drop/TestDropFunctionAnalyzer.java    | 150 +++++++++++++++++++++
 3 files changed, 184 insertions(+), 3 deletions(-)

diff --git 
a/ql/src/java/org/apache/hadoop/hive/ql/ddl/function/AbstractFunctionAnalyzer.java
 
b/ql/src/java/org/apache/hadoop/hive/ql/ddl/function/AbstractFunctionAnalyzer.java
index 4d8cae0a7ca..ee90b1af105 100644
--- 
a/ql/src/java/org/apache/hadoop/hive/ql/ddl/function/AbstractFunctionAnalyzer.java
+++ 
b/ql/src/java/org/apache/hadoop/hive/ql/ddl/function/AbstractFunctionAnalyzer.java
@@ -30,6 +30,7 @@
 import org.apache.hadoop.hive.ql.hooks.ReadEntity;
 import org.apache.hadoop.hive.ql.hooks.WriteEntity;
 import org.apache.hadoop.hive.ql.hooks.Entity.Type;
+import org.apache.hadoop.hive.ql.metadata.Hive;
 import org.apache.hadoop.hive.ql.metadata.HiveException;
 import org.apache.hadoop.hive.ql.parse.BaseSemanticAnalyzer;
 import org.apache.hadoop.hive.ql.parse.SemanticException;
@@ -44,6 +45,10 @@ public AbstractFunctionAnalyzer(QueryState queryState) 
throws SemanticException
     super(queryState);
   }
 
+  protected AbstractFunctionAnalyzer(QueryState queryState, Hive db) throws 
SemanticException {
+    super(queryState, db);
+  }
+
   /**
    * Add write entities to the semantic analyzer to restrict function creation 
to privileged users.
    */
diff --git 
a/ql/src/java/org/apache/hadoop/hive/ql/ddl/function/drop/DropFunctionAnalyzer.java
 
b/ql/src/java/org/apache/hadoop/hive/ql/ddl/function/drop/DropFunctionAnalyzer.java
index 23e76d89771..a5dd6188e63 100644
--- 
a/ql/src/java/org/apache/hadoop/hive/ql/ddl/function/drop/DropFunctionAnalyzer.java
+++ 
b/ql/src/java/org/apache/hadoop/hive/ql/ddl/function/drop/DropFunctionAnalyzer.java
@@ -24,14 +24,19 @@
 import org.apache.hadoop.hive.ql.QueryState;
 import org.apache.hadoop.hive.ql.exec.FunctionInfo;
 import org.apache.hadoop.hive.ql.exec.FunctionRegistry;
+import org.apache.hadoop.hive.ql.exec.FunctionUtils;
 import org.apache.hadoop.hive.ql.exec.TaskFactory;
 import org.apache.hadoop.hive.ql.ddl.DDLSemanticAnalyzerFactory.DDLType;
 import org.apache.hadoop.hive.ql.ddl.function.AbstractFunctionAnalyzer;
 import org.apache.hadoop.hive.ql.ddl.DDLWork;
+import org.apache.hadoop.hive.ql.metadata.Hive;
+import org.apache.hadoop.hive.ql.metadata.HiveException;
 import org.apache.hadoop.hive.ql.parse.ASTNode;
 import org.apache.hadoop.hive.ql.parse.HiveParser;
 import org.apache.hadoop.hive.ql.parse.SemanticException;
 
+import java.util.List;
+
 /**
  * Analyzer for function dropping commands.
  */
@@ -41,6 +46,10 @@ public DropFunctionAnalyzer(QueryState queryState) throws 
SemanticException {
     super(queryState);
   }
 
+  public DropFunctionAnalyzer(QueryState queryState, Hive db) throws 
SemanticException {
+    super(queryState, db);
+  }
+
   @Override
   public void analyzeInternal(ASTNode root) throws SemanticException {
     String functionName = root.getChild(0).getText();
@@ -50,10 +59,16 @@ public void analyzeInternal(ASTNode root) throws 
SemanticException {
 
     FunctionInfo info = FunctionRegistry.getFunctionInfo(functionName);
     if (info == null) {
-      if (throwException) {
+      // getFunctionInfo returns null when the function's JAR resource cannot 
be loaded (e.g. the
+      // HDFS file was deleted). For permanent functions fall back to a direct 
metastore lookup so
+      // that an orphaned definition can still be removed without the JAR 
being present.
+      if (!isTemporary && functionExistsInMetastore(functionName)) {
+        LOG.warn("Function {} has unavailable resources; proceeding with drop 
using metastore metadata only.",
+            functionName);
+      } else if (throwException) {
         throw new 
SemanticException(ErrorMsg.INVALID_FUNCTION.getMsg(functionName));
       } else {
-        return; // Fail silently
+        return;
       }
     } else if (info.isBuiltIn()) {
       throw new 
SemanticException(ErrorMsg.DROP_NATIVE_FUNCTION.getMsg(functionName));
@@ -62,6 +77,17 @@ public void analyzeInternal(ASTNode root) throws 
SemanticException {
     DropFunctionDesc desc = new DropFunctionDesc(functionName, isTemporary, 
null);
     rootTasks.add(TaskFactory.get(new DDLWork(getInputs(), getOutputs(), 
desc)));
 
-    addEntities(functionName, info.getClassName(), isTemporary, null);
+    String className = info != null ? info.getClassName() : null;
+    addEntities(functionName, className, isTemporary, null);
+  }
+
+  private boolean functionExistsInMetastore(String functionName) {
+    try {
+      String[] parts = 
FunctionUtils.getQualifiedFunctionNameParts(functionName.toLowerCase());
+      List<String> functions = db.getFunctions(parts[0], parts[1]);
+      return functions != null && functions.contains(parts[1]);
+    } catch (HiveException e) {
+      return false;
+    }
   }
 }
diff --git 
a/ql/src/test/org/apache/hadoop/hive/ql/ddl/function/drop/TestDropFunctionAnalyzer.java
 
b/ql/src/test/org/apache/hadoop/hive/ql/ddl/function/drop/TestDropFunctionAnalyzer.java
new file mode 100644
index 00000000000..d3ae91b7641
--- /dev/null
+++ 
b/ql/src/test/org/apache/hadoop/hive/ql/ddl/function/drop/TestDropFunctionAnalyzer.java
@@ -0,0 +1,150 @@
+/*
+ * 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.hadoop.hive.ql.ddl.function.drop;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.apache.hadoop.hive.conf.HiveConf.ConfVars;
+import org.apache.hadoop.hive.conf.HiveConfForTest;
+import org.apache.hadoop.hive.metastore.api.Database;
+import org.apache.hadoop.hive.metastore.api.Function;
+import org.apache.hadoop.hive.metastore.api.FunctionType;
+import org.apache.hadoop.hive.metastore.api.PrincipalType;
+import org.apache.hadoop.hive.ql.Context;
+import org.apache.hadoop.hive.ql.QueryState;
+import org.apache.hadoop.hive.ql.exec.FunctionRegistry;
+import org.apache.hadoop.hive.ql.metadata.Hive;
+import org.apache.hadoop.hive.ql.plan.HiveOperation;
+import org.apache.hadoop.hive.ql.parse.ASTNode;
+import org.apache.hadoop.hive.ql.parse.ParseUtils;
+import org.apache.hadoop.hive.ql.parse.SemanticException;
+import org.apache.hadoop.hive.ql.session.SessionState;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+/**
+ * Tests for DropFunctionAnalyzer focusing on the case where the function's 
JAR resource is
+ * unavailable (e.g. deleted from HDFS after the function was registered).
+ */
+class TestDropFunctionAnalyzer {
+
+  private HiveConf conf;
+
+  @BeforeEach
+  void setUp() {
+    conf = new HiveConfForTest(getClass());
+    SessionState.start(conf);
+  }
+
+  @AfterEach
+  void tearDown() throws Exception {
+    SessionState ss = SessionState.get();
+    if (ss != null) {
+      ss.close();
+    }
+  }
+
+  private DropFunctionAnalyzer createAnalyzer(Hive mockDb) throws 
SemanticException {
+    QueryState queryState = QueryState.getNewQueryState(conf, null);
+    queryState.setCommandType(HiveOperation.DROPFUNCTION);
+    return new DropFunctionAnalyzer(queryState, mockDb);
+  }
+
+  /**
+   * When FunctionRegistry.getFunctionInfo returns null (JAR unavailable) but 
the function still
+   * exists in the metastore, DROP FUNCTION must proceed and emit a drop task 
so the orphaned
+   * definition is actually removed.
+   */
+  @Test
+  void testDropSucceedsWhenJarUnavailableButFunctionInMetastore() throws 
Exception {
+    Hive mockDb = mock(Hive.class);
+    Database msDatabase = new Database("default", "", "/tmp", 
Collections.emptyMap());
+    Function msFunction = new Function("dummy", "default", 
"com.example.DummyUDF",
+        "user", PrincipalType.USER, 0, FunctionType.JAVA, 
Collections.emptyList());
+
+    when(mockDb.getFunctions("default", "dummy")).thenReturn(List.of("dummy"));
+    when(mockDb.getFunction("default", "dummy")).thenReturn(msFunction);
+    when(mockDb.getDatabase("default")).thenReturn(msDatabase);
+    when(mockDb.getDatabase(any(), eq("default"))).thenReturn(msDatabase);
+
+    try (MockedStatic<FunctionRegistry> registry = 
mockStatic(FunctionRegistry.class)) {
+      registry.when(() -> 
FunctionRegistry.getFunctionInfo("dummy")).thenReturn(null);
+
+      DropFunctionAnalyzer analyzer = createAnalyzer(mockDb);
+      analyzer.analyzeInternal(parse("drop function dummy"));
+
+      assertEquals(1, analyzer.getRootTasks().size(), "Expected one DROP task 
even when JAR is unavailable");
+    }
+  }
+
+  /**
+   * When the function does not exist in either the session registry or the 
metastore, DROP FUNCTION
+   * (without IF EXISTS) must surface the error to the client.
+   */
+  @Test
+  void testDropThrowsWhenFunctionNotInMetastore() throws Exception {
+    conf.setBoolVar(ConfVars.DROP_IGNORES_NON_EXISTENT, false);
+    Hive mockDb = mock(Hive.class);
+    when(mockDb.getFunctions(anyString(), 
anyString())).thenReturn(Collections.emptyList());
+
+    try (MockedStatic<FunctionRegistry> registry = 
mockStatic(FunctionRegistry.class)) {
+      registry.when(() -> 
FunctionRegistry.getFunctionInfo("dummy")).thenReturn(null);
+
+      DropFunctionAnalyzer analyzer = createAnalyzer(mockDb);
+      assertThrows(SemanticException.class, () -> 
analyzer.analyzeInternal(parse("drop function dummy")),
+          "Expected SemanticException when function not in registry or 
metastore");
+    }
+  }
+
+  /**
+   * DROP FUNCTION IF EXISTS must silently succeed (no task, no exception) 
when the function is
+   * absent from both the session registry and the metastore.
+   */
+  @Test
+  void testDropIfExistsSilentWhenFunctionAbsent() throws Exception {
+    Hive mockDb = mock(Hive.class);
+    when(mockDb.getFunctions(anyString(), 
anyString())).thenReturn(Collections.emptyList());
+
+    try (MockedStatic<FunctionRegistry> registry = 
mockStatic(FunctionRegistry.class)) {
+      registry.when(() -> 
FunctionRegistry.getFunctionInfo("dummy")).thenReturn(null);
+
+      DropFunctionAnalyzer analyzer = createAnalyzer(mockDb);
+      analyzer.analyzeInternal(parse("drop function if exists dummy"));
+
+      assertEquals(0, analyzer.getRootTasks().size(), "Expected no tasks when 
function is absent and IF EXISTS is set");
+    }
+  }
+
+  private ASTNode parse(String sql) throws Exception {
+    return ParseUtils.parse(sql, new Context(conf));
+  }
+}

Reply via email to