Skip to content

Commit 59b36ea

Browse files
authored
HIVE-14609: HS2 cannot drop a function whose associated jar file has been removed (#6447)
1 parent 3f98d61 commit 59b36ea

3 files changed

Lines changed: 184 additions & 3 deletions

File tree

ql/src/java/org/apache/hadoop/hive/ql/ddl/function/AbstractFunctionAnalyzer.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.apache.hadoop.hive.ql.hooks.ReadEntity;
3131
import org.apache.hadoop.hive.ql.hooks.WriteEntity;
3232
import org.apache.hadoop.hive.ql.hooks.Entity.Type;
33+
import org.apache.hadoop.hive.ql.metadata.Hive;
3334
import org.apache.hadoop.hive.ql.metadata.HiveException;
3435
import org.apache.hadoop.hive.ql.parse.BaseSemanticAnalyzer;
3536
import org.apache.hadoop.hive.ql.parse.SemanticException;
@@ -44,6 +45,10 @@ public AbstractFunctionAnalyzer(QueryState queryState) throws SemanticException
4445
super(queryState);
4546
}
4647

48+
protected AbstractFunctionAnalyzer(QueryState queryState, Hive db) throws SemanticException {
49+
super(queryState, db);
50+
}
51+
4752
/**
4853
* Add write entities to the semantic analyzer to restrict function creation to privileged users.
4954
*/

ql/src/java/org/apache/hadoop/hive/ql/ddl/function/drop/DropFunctionAnalyzer.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,19 @@
2424
import org.apache.hadoop.hive.ql.QueryState;
2525
import org.apache.hadoop.hive.ql.exec.FunctionInfo;
2626
import org.apache.hadoop.hive.ql.exec.FunctionRegistry;
27+
import org.apache.hadoop.hive.ql.exec.FunctionUtils;
2728
import org.apache.hadoop.hive.ql.exec.TaskFactory;
2829
import org.apache.hadoop.hive.ql.ddl.DDLSemanticAnalyzerFactory.DDLType;
2930
import org.apache.hadoop.hive.ql.ddl.function.AbstractFunctionAnalyzer;
3031
import org.apache.hadoop.hive.ql.ddl.DDLWork;
32+
import org.apache.hadoop.hive.ql.metadata.Hive;
33+
import org.apache.hadoop.hive.ql.metadata.HiveException;
3134
import org.apache.hadoop.hive.ql.parse.ASTNode;
3235
import org.apache.hadoop.hive.ql.parse.HiveParser;
3336
import org.apache.hadoop.hive.ql.parse.SemanticException;
3437

38+
import java.util.List;
39+
3540
/**
3641
* Analyzer for function dropping commands.
3742
*/
@@ -41,6 +46,10 @@ public DropFunctionAnalyzer(QueryState queryState) throws SemanticException {
4146
super(queryState);
4247
}
4348

49+
public DropFunctionAnalyzer(QueryState queryState, Hive db) throws SemanticException {
50+
super(queryState, db);
51+
}
52+
4453
@Override
4554
public void analyzeInternal(ASTNode root) throws SemanticException {
4655
String functionName = root.getChild(0).getText();
@@ -50,10 +59,16 @@ public void analyzeInternal(ASTNode root) throws SemanticException {
5059

5160
FunctionInfo info = FunctionRegistry.getFunctionInfo(functionName);
5261
if (info == null) {
53-
if (throwException) {
62+
// getFunctionInfo returns null when the function's JAR resource cannot be loaded (e.g. the
63+
// HDFS file was deleted). For permanent functions fall back to a direct metastore lookup so
64+
// that an orphaned definition can still be removed without the JAR being present.
65+
if (!isTemporary && functionExistsInMetastore(functionName)) {
66+
LOG.warn("Function {} has unavailable resources; proceeding with drop using metastore metadata only.",
67+
functionName);
68+
} else if (throwException) {
5469
throw new SemanticException(ErrorMsg.INVALID_FUNCTION.getMsg(functionName));
5570
} else {
56-
return; // Fail silently
71+
return;
5772
}
5873
} else if (info.isBuiltIn()) {
5974
throw new SemanticException(ErrorMsg.DROP_NATIVE_FUNCTION.getMsg(functionName));
@@ -62,6 +77,17 @@ public void analyzeInternal(ASTNode root) throws SemanticException {
6277
DropFunctionDesc desc = new DropFunctionDesc(functionName, isTemporary, null);
6378
rootTasks.add(TaskFactory.get(new DDLWork(getInputs(), getOutputs(), desc)));
6479

65-
addEntities(functionName, info.getClassName(), isTemporary, null);
80+
String className = info != null ? info.getClassName() : null;
81+
addEntities(functionName, className, isTemporary, null);
82+
}
83+
84+
private boolean functionExistsInMetastore(String functionName) {
85+
try {
86+
String[] parts = FunctionUtils.getQualifiedFunctionNameParts(functionName.toLowerCase());
87+
List<String> functions = db.getFunctions(parts[0], parts[1]);
88+
return functions != null && functions.contains(parts[1]);
89+
} catch (HiveException e) {
90+
return false;
91+
}
6692
}
6793
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.hadoop.hive.ql.ddl.function.drop;
20+
21+
import static org.junit.jupiter.api.Assertions.assertEquals;
22+
import static org.junit.jupiter.api.Assertions.assertThrows;
23+
import static org.mockito.ArgumentMatchers.any;
24+
import static org.mockito.ArgumentMatchers.anyString;
25+
import static org.mockito.ArgumentMatchers.eq;
26+
import static org.mockito.Mockito.mock;
27+
import static org.mockito.Mockito.mockStatic;
28+
import static org.mockito.Mockito.when;
29+
30+
import java.util.Collections;
31+
import java.util.List;
32+
33+
import org.apache.hadoop.hive.conf.HiveConf;
34+
import org.apache.hadoop.hive.conf.HiveConf.ConfVars;
35+
import org.apache.hadoop.hive.conf.HiveConfForTest;
36+
import org.apache.hadoop.hive.metastore.api.Database;
37+
import org.apache.hadoop.hive.metastore.api.Function;
38+
import org.apache.hadoop.hive.metastore.api.FunctionType;
39+
import org.apache.hadoop.hive.metastore.api.PrincipalType;
40+
import org.apache.hadoop.hive.ql.Context;
41+
import org.apache.hadoop.hive.ql.QueryState;
42+
import org.apache.hadoop.hive.ql.exec.FunctionRegistry;
43+
import org.apache.hadoop.hive.ql.metadata.Hive;
44+
import org.apache.hadoop.hive.ql.plan.HiveOperation;
45+
import org.apache.hadoop.hive.ql.parse.ASTNode;
46+
import org.apache.hadoop.hive.ql.parse.ParseUtils;
47+
import org.apache.hadoop.hive.ql.parse.SemanticException;
48+
import org.apache.hadoop.hive.ql.session.SessionState;
49+
import org.junit.jupiter.api.AfterEach;
50+
import org.junit.jupiter.api.BeforeEach;
51+
import org.junit.jupiter.api.Test;
52+
import org.mockito.MockedStatic;
53+
54+
/**
55+
* Tests for DropFunctionAnalyzer focusing on the case where the function's JAR resource is
56+
* unavailable (e.g. deleted from HDFS after the function was registered).
57+
*/
58+
class TestDropFunctionAnalyzer {
59+
60+
private HiveConf conf;
61+
62+
@BeforeEach
63+
void setUp() {
64+
conf = new HiveConfForTest(getClass());
65+
SessionState.start(conf);
66+
}
67+
68+
@AfterEach
69+
void tearDown() throws Exception {
70+
SessionState ss = SessionState.get();
71+
if (ss != null) {
72+
ss.close();
73+
}
74+
}
75+
76+
private DropFunctionAnalyzer createAnalyzer(Hive mockDb) throws SemanticException {
77+
QueryState queryState = QueryState.getNewQueryState(conf, null);
78+
queryState.setCommandType(HiveOperation.DROPFUNCTION);
79+
return new DropFunctionAnalyzer(queryState, mockDb);
80+
}
81+
82+
/**
83+
* When FunctionRegistry.getFunctionInfo returns null (JAR unavailable) but the function still
84+
* exists in the metastore, DROP FUNCTION must proceed and emit a drop task so the orphaned
85+
* definition is actually removed.
86+
*/
87+
@Test
88+
void testDropSucceedsWhenJarUnavailableButFunctionInMetastore() throws Exception {
89+
Hive mockDb = mock(Hive.class);
90+
Database msDatabase = new Database("default", "", "/tmp", Collections.emptyMap());
91+
Function msFunction = new Function("dummy", "default", "com.example.DummyUDF",
92+
"user", PrincipalType.USER, 0, FunctionType.JAVA, Collections.emptyList());
93+
94+
when(mockDb.getFunctions("default", "dummy")).thenReturn(List.of("dummy"));
95+
when(mockDb.getFunction("default", "dummy")).thenReturn(msFunction);
96+
when(mockDb.getDatabase("default")).thenReturn(msDatabase);
97+
when(mockDb.getDatabase(any(), eq("default"))).thenReturn(msDatabase);
98+
99+
try (MockedStatic<FunctionRegistry> registry = mockStatic(FunctionRegistry.class)) {
100+
registry.when(() -> FunctionRegistry.getFunctionInfo("dummy")).thenReturn(null);
101+
102+
DropFunctionAnalyzer analyzer = createAnalyzer(mockDb);
103+
analyzer.analyzeInternal(parse("drop function dummy"));
104+
105+
assertEquals(1, analyzer.getRootTasks().size(), "Expected one DROP task even when JAR is unavailable");
106+
}
107+
}
108+
109+
/**
110+
* When the function does not exist in either the session registry or the metastore, DROP FUNCTION
111+
* (without IF EXISTS) must surface the error to the client.
112+
*/
113+
@Test
114+
void testDropThrowsWhenFunctionNotInMetastore() throws Exception {
115+
conf.setBoolVar(ConfVars.DROP_IGNORES_NON_EXISTENT, false);
116+
Hive mockDb = mock(Hive.class);
117+
when(mockDb.getFunctions(anyString(), anyString())).thenReturn(Collections.emptyList());
118+
119+
try (MockedStatic<FunctionRegistry> registry = mockStatic(FunctionRegistry.class)) {
120+
registry.when(() -> FunctionRegistry.getFunctionInfo("dummy")).thenReturn(null);
121+
122+
DropFunctionAnalyzer analyzer = createAnalyzer(mockDb);
123+
assertThrows(SemanticException.class, () -> analyzer.analyzeInternal(parse("drop function dummy")),
124+
"Expected SemanticException when function not in registry or metastore");
125+
}
126+
}
127+
128+
/**
129+
* DROP FUNCTION IF EXISTS must silently succeed (no task, no exception) when the function is
130+
* absent from both the session registry and the metastore.
131+
*/
132+
@Test
133+
void testDropIfExistsSilentWhenFunctionAbsent() throws Exception {
134+
Hive mockDb = mock(Hive.class);
135+
when(mockDb.getFunctions(anyString(), anyString())).thenReturn(Collections.emptyList());
136+
137+
try (MockedStatic<FunctionRegistry> registry = mockStatic(FunctionRegistry.class)) {
138+
registry.when(() -> FunctionRegistry.getFunctionInfo("dummy")).thenReturn(null);
139+
140+
DropFunctionAnalyzer analyzer = createAnalyzer(mockDb);
141+
analyzer.analyzeInternal(parse("drop function if exists dummy"));
142+
143+
assertEquals(0, analyzer.getRootTasks().size(), "Expected no tasks when function is absent and IF EXISTS is set");
144+
}
145+
}
146+
147+
private ASTNode parse(String sql) throws Exception {
148+
return ParseUtils.parse(sql, new Context(conf));
149+
}
150+
}

0 commit comments

Comments
 (0)