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

dengzhhu653 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 648ac43ee70 HIVE-29740 : Fix SHOW TABLES operation taking 300 seconds+ 
on very large databases (#6610)
648ac43ee70 is described below

commit 648ac43ee70235620e484d196af311336d6c58a7
Author: Vikram Ahuja <[email protected]>
AuthorDate: Mon Jul 20 16:32:22 2026 +0530

    HIVE-29740 : Fix SHOW TABLES operation taking 300 seconds+ on very large 
databases (#6610)
---
 .../plugin/AuthorizationMetaStoreFilterHook.java   |  51 ++--
 .../TestAuthorizationMetaStoreFilterHook.java      | 329 +++++++++++++++++++++
 2 files changed, 350 insertions(+), 30 deletions(-)

diff --git 
a/ql/src/java/org/apache/hadoop/hive/ql/security/authorization/plugin/AuthorizationMetaStoreFilterHook.java
 
b/ql/src/java/org/apache/hadoop/hive/ql/security/authorization/plugin/AuthorizationMetaStoreFilterHook.java
index a88f61d3a62..389855c32a6 100644
--- 
a/ql/src/java/org/apache/hadoop/hive/ql/security/authorization/plugin/AuthorizationMetaStoreFilterHook.java
+++ 
b/ql/src/java/org/apache/hadoop/hive/ql/security/authorization/plugin/AuthorizationMetaStoreFilterHook.java
@@ -33,9 +33,6 @@
 import static 
org.apache.hadoop.hive.ql.security.authorization.plugin.HivePrivilegeObjectUtils.TablePrivilegeLookup;
 import org.apache.hadoop.hive.ql.session.SessionState;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 /**
  * Metastore filter hook for filtering out the list of objects that the 
current authorization
  * implementation does not allow user to see
@@ -56,43 +53,37 @@ public List<String> filterTableNames(String catName, String 
dbName, List<String>
     return getFilteredObjectNames(getFilteredObjects(listObjs));
   }
 
+  /**
+   * Filters the given list of tables down to those the current user is 
authorized to see.
+   *
+   * <p>The method delegates authorization decisions to {@link 
HiveAuthorizer#filterListCmdObjects},
+   * then uses a {@link TablePrivilegeLookup} (binary-search index) to match 
the returned
+   * privilege objects back to the original {@link Table} objects in O(n log 
n), replacing an
+   * earlier O(n²) nested-loop implementation that was prohibitively slow for 
large table lists.
+   *
+   * <p>A table's catName is normalized to the default catalog before the 
lookup when it is
+   * {@code null}, consistent with how {@code tablesToPrivilegeObjs} builds 
the privilege objects
+   * that are sent to the authorizer.
+   *
+   * @param tableList the full list of tables returned by the MetaStore before 
authorization
+   * @return the sub-list of tables the current user is permitted to list
+   * @throws MetaException if the authorizer throws an exception
+   */
   @Override
   public List<Table> filterTables(List<Table> tableList) throws MetaException {
     List<HivePrivilegeObject> listObjs = tablesToPrivilegeObjs(tableList);
-    return getFilteredTableList(getFilteredObjects(listObjs),tableList);
-  }
-
-  private List<Table> getFilteredTableList(List<HivePrivilegeObject> 
hivePrivilegeObjects, List<Table> tableList) {
+    TablePrivilegeLookup index = new 
TablePrivilegeLookup(getFilteredObjects(listObjs));
     List<Table> ret = new ArrayList<>();
-    for(HivePrivilegeObject hivePrivilegeObject:hivePrivilegeObjects) {
-      String catName = hivePrivilegeObject.getCatName();
-      String dbName  = hivePrivilegeObject.getDbname();
-      String tblName = hivePrivilegeObject.getObjectName();
-      Table  table   = getFilteredTable(catName, dbName, tblName, tableList);
-      if (table != null) {
+    String defaultCatName = 
MetaStoreUtils.getDefaultCatalog(HivePrivilegeObject.getConf());
+    for (Table table : tableList) {
+      String catName = table.getCatName() != null ? table.getCatName() : 
defaultCatName;
+      if (index.lookup(catName, table.getDbName(), table.getTableName()) != 
null) {
         ret.add(table);
       }
     }
     return ret;
   }
 
-  private Table getFilteredTable(String catName, String dbName, String 
tblName, List<Table> tableList) {
-    Table ret = null;
-    for (Table table: tableList) {
-      // do not check catalog name if catName is null
-      if (catName != null && table.getCatName() != null && 
!catName.equals(table.getCatName())) {
-        continue;
-      }
-      String databaseName = table.getDbName();
-      String tableName = table.getTableName();
-      if (dbName.equals(databaseName) && tblName.equals(tableName)) {
-        ret = table;
-        break;
-      }
-    }
-    return ret;
-  }
-
   @Override
   public List<String> filterDatabases(String catName, List<String> dbList) 
throws MetaException {
     List<HivePrivilegeObject> listObjs = 
HivePrivilegeObjectUtils.getHivePrivDbObjects(catName, dbList);
diff --git 
a/ql/src/test/org/apache/hadoop/hive/ql/security/authorization/plugin/TestAuthorizationMetaStoreFilterHook.java
 
b/ql/src/test/org/apache/hadoop/hive/ql/security/authorization/plugin/TestAuthorizationMetaStoreFilterHook.java
new file mode 100644
index 00000000000..d6d7fd9dab4
--- /dev/null
+++ 
b/ql/src/test/org/apache/hadoop/hive/ql/security/authorization/plugin/TestAuthorizationMetaStoreFilterHook.java
@@ -0,0 +1,329 @@
+/*
+ * 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.security.authorization.plugin;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hive.metastore.api.PrincipalType;
+import org.apache.hadoop.hive.metastore.api.Table;
+import org.apache.hadoop.hive.ql.session.SessionState;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link AuthorizationMetaStoreFilterHook#filterTables}.
+ *
+ * Validates correctness of the O(n) HashMap-based fix replacing the original
+ * O(n^2) nested-loop implementation, including catName null-handling logic.
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class TestAuthorizationMetaStoreFilterHook {
+
+  private AuthorizationMetaStoreFilterHook hook;
+  private MockedStatic<SessionState> mockedSessionState;
+
+  @Mock private SessionState sessionState;
+  @Mock private HiveAuthorizer authorizer;
+
+  @Before
+  public void setUp() {
+    hook = new AuthorizationMetaStoreFilterHook(new Configuration());
+    when(sessionState.getUserIpAddress()).thenReturn("127.0.0.1");
+    
when(sessionState.getForwardedAddresses()).thenReturn(Collections.emptyList());
+    when(sessionState.getAuthorizerV2()).thenReturn(authorizer);
+    mockedSessionState = mockStatic(SessionState.class);
+    mockedSessionState.when(SessionState::get).thenReturn(sessionState);
+  }
+
+  @After
+  public void tearDown() {
+    mockedSessionState.close();
+  }
+
+
+  /** Create a Table with catName. */
+  private Table createTable(String catName, String dbName, String tableName) {
+    Table table = new Table();
+    table.setCatName(catName);
+    table.setDbName(dbName);
+    table.setTableName(tableName);
+    table.setOwner("testowner");
+    table.setOwnerType(PrincipalType.USER);
+    return table;
+  }
+
+  /** Create a Table without catName (catName = null). */
+  private Table createTable(String dbName, String tableName) {
+    return createTable(null, dbName, tableName);
+  }
+
+  /**
+   * Stub authorizer to return HivePrivilegeObjects with the given catName
+   * for tables matching dbName + any of the allowedTableNames.
+   */
+  private void allowTablesWithCatName(String catName, String dbName, String... 
allowedTableNames)
+       throws HiveAuthzPluginException, HiveAccessControlException {
+    when(authorizer.filterListCmdObjects(any(), any())).thenAnswer(invocation 
-> {
+      List<HivePrivilegeObject> input = invocation.getArgument(0);
+      List<HivePrivilegeObject> result = new ArrayList<>();
+      Set<String> seen = new HashSet<>();
+      for (HivePrivilegeObject obj : input) {
+        for (String allowed : allowedTableNames) {
+          if (dbName.equals(obj.getDbname()) && 
allowed.equals(obj.getObjectName())) {
+            String key = (catName != null ? catName : "") + "\0" + dbName + 
"\0" + allowed;
+            if (seen.add(key)) {
+              result.add(new HivePrivilegeObject(
+                    HivePrivilegeObject.HivePrivilegeObjectType.TABLE_OR_VIEW, 
catName, dbName, allowed));
+            }
+            break;
+          }
+        }
+      }
+      return result;
+    });
+  }
+
+  /** Stub authorizer to return all objects unchanged (allow all, catName 
stays null). */
+  private void allowAll() throws HiveAuthzPluginException, 
HiveAccessControlException {
+    when(authorizer.filterListCmdObjects(any(), any())).thenAnswer(invocation 
-> invocation.getArgument(0));
+  }
+
+  /** Stub authorizer to deny everything. */
+  private void denyAll() throws HiveAuthzPluginException, 
HiveAccessControlException {
+    when(authorizer.filterListCmdObjects(any(), 
any())).thenReturn(Collections.emptyList());
+  }
+
+  @Test
+  public void testFilterTablesEmptyInput() throws Exception {
+    denyAll();
+    List<Table> result = hook.filterTables(Collections.emptyList());
+    assertTrue(result.isEmpty());
+  }
+
+  @Test
+  public void testFilterTablesAllAllowed() throws Exception {
+    List<Table> input = List.of(
+        createTable("db1", "tbl1"),
+        createTable("db1", "tbl2"),
+        createTable("db1", "tbl3"));
+    allowAll();
+    List<Table> result = hook.filterTables(input);
+    assertEquals(3, result.size());
+  }
+
+  @Test
+  public void testFilterTablesAllFiltered() throws Exception {
+    List<Table> input = List.of(
+        createTable("db1", "tbl1"),
+        createTable("db1", "tbl2"));
+    denyAll();
+    List<Table> result = hook.filterTables(input);
+    assertTrue(result.isEmpty());
+  }
+
+  @Test
+  public void testFilterTablesSomeFiltered() throws Exception {
+    List<Table> input = List.of(
+        createTable("db1", "tbl1"),
+        createTable("db1", "tbl2"),
+        createTable("db1", "tbl3"));
+      // authorize returns tbl1 and tbl3 with null catName
+    when(authorizer.filterListCmdObjects(any(), any())).thenAnswer(invocation 
-> {
+      List<HivePrivilegeObject> objs = invocation.getArgument(0);
+      return objs.stream()
+                    .filter(o -> "tbl1".equals(o.getObjectName()) || 
"tbl3".equals(o.getObjectName()))
+                    .toList();
+    });
+    List<Table> result = hook.filterTables(input);
+    assertEquals(2, result.size());
+    List<String> names = result.stream().map(Table::getTableName).toList();
+    assertTrue(names.contains("tbl1"));
+    assertTrue(names.contains("tbl3"));
+  }
+
+  /**
+   * Case 2: obj.catName == null → must match a table that also has null 
catName.
+   * Note: HivePrivilegeObject normalizes null catName to the default catalog 
("hive"),
+   * so the null-catName-obj path in production code is unreachable in 
practice.
+   * The live equivalent is tested via 
testFilterTables_tableCatNameNull_matchesObjWithNonNullCatName.
+   */
+  @Test
+  public void testFilterTablesObjCatNameNullMatchesTableWithNullCatName() 
throws Exception {
+    List<Table> input = List.of(createTable(null, "db1", "tbl1"));
+    allowTablesWithCatName(null, "db1", "tbl1");
+    List<Table> result = hook.filterTables(input);
+    assertEquals("obj with null catName should match table with null catName", 
1, result.size());
+  }
+
+  /**
+   * Table with null catName: tablesToPrivilegeObjs normalizes null → 
default catalog ("hive"),
+   * so the authorizer receives and returns a "hive"-catName HPO. The lookup 
normalizes the
+   * table's null catName to "hive" as well, producing an exact match.
+   */
+  @Test
+  public void testFilterTablesTableCatNameNullMatchesObjWithNonNullCatName() 
throws Exception {
+    List<Table> input = List.of(createTable(null, "db1", "tbl1")); // table 
has null catName
+    // authorizer echoes back the default-catalog HPO it received
+    allowTablesWithCatName(null, "db1", "tbl1");
+    List<Table> result = hook.filterTables(input);
+    assertEquals("table with null catName should match when authorizer returns 
default-catalog HPO", 1, result.size());
+  }
+
+  /**
+   * Case 3: obj.catName != null, table.catName != null, and they match → 
matches.
+   */
+  @Test
+  public void testFilterTablesSameCatNameMatches() throws Exception {
+    List<Table> input = List.of(createTable("cat1", "db1", "tbl1"));
+    allowTablesWithCatName("cat1", "db1", "tbl1");
+    List<Table> result = hook.filterTables(input);
+    assertEquals("same catName on obj and table should match", 1, 
result.size());
+    assertEquals("cat1", result.get(0).getCatName());
+  }
+
+  /**
+   * Both obj.catName and table.catName are non-null but DIFFERENT → no 
match.
+   * Original logic: condition is true → continue (skip).
+   */
+  @Test
+  public void testFilterTablesDifferentCatNameNoMatch() throws Exception {
+    List<Table> input = List.of(createTable("cat2", "db1", "tbl1")); // table 
has cat2
+    // authorizer says tbl1 is allowed but with catName=cat1
+    allowTablesWithCatName("cat1", "db1", "tbl1");
+    List<Table> result = hook.filterTables(input);
+    assertTrue("obj.catName=cat1 vs table.catName=cat2 should not match", 
result.isEmpty());
+  }
+
+  /**
+   * Multiple catalogs, same db+table name.
+   * obj.catName=cat1 should return only the cat1 table, not cat2.
+   */
+  @Test
+  public void testFilterTablesMultipleCatalogsSameDbTableExactCatNameMatch() 
throws Exception {
+    Table cat1Table = createTable("cat1", "db1", "tbl1");
+    Table cat2Table = createTable("cat2", "db1", "tbl1");
+    List<Table> input = List.of(cat1Table, cat2Table);
+
+    allowTablesWithCatName("cat1", "db1", "tbl1");
+
+    List<Table> result = hook.filterTables(input);
+
+    assertEquals("Only cat1 table should be returned", 1, result.size());
+    assertEquals("cat1", result.get(0).getCatName());
+  }
+
+  /**
+   * Table with null catName is normalized to the default catalog for lookup.
+   * When the authorizer allows it (returning the default-catalog HPO it 
received),
+   * the null-catName table is included; a table with a different catName 
("cat2")
+   * that was not authorized is excluded.
+   */
+  @Test
+  public void 
testFilterTablesNullCatTableIncludedWhenAuthorizedOtherCatExcluded() throws 
Exception {
+    Table nullCatTable = createTable(null, "db1", "tbl1");   // normalized to 
"hive" for lookup
+    Table cat2Table    = createTable("cat2", "db1", "tbl1"); // not authorized
+    List<Table> input  = List.of(nullCatTable, cat2Table);
+
+    // authorizer allows only the default-catalog ("hive") entry, not cat2
+    allowTablesWithCatName(null, "db1", "tbl1");
+
+    List<Table> result = hook.filterTables(input);
+
+    assertEquals("only the null-catName table (authorized via default catalog) 
should be returned", 1, result.size());
+    assertEquals(null, result.get(0).getCatName());
+  }
+
+  /**
+   * Mixed: some tables have catName, some don't; authorizer excludes one 
table entirely.
+   * - tbl1 (catName=cat1): authorized → exact match on "cat1" → included.
+   * - tbl2 (catName=null): normalized to "hive" → authorizer echoes "hive" 
HPO → included.
+   * - tbl3 (catName=cat2): not authorized → excluded.
+   */
+  @Test
+  public void testFilterTablesMixedCatNameCombinations() throws Exception {
+    List<Table> input = List.of(
+        createTable("cat1", "db1", "tbl1"),  // authorized, exact catName match
+        createTable(null,   "db1", "tbl2"),  // authorized, null catName 
normalized to "hive"
+        createTable("cat2", "db1", "tbl3")); // not authorized → excluded
+
+    // authorizer passes through tbl1 and tbl2 unchanged, drops tbl3
+    when(authorizer.filterListCmdObjects(any(), any())).thenAnswer(invocation 
-> {
+      List<HivePrivilegeObject> objs = invocation.getArgument(0);
+      return objs.stream()
+          .filter(o -> !"tbl3".equals(o.getObjectName()))
+          .toList();
+    });
+
+    List<Table> result = hook.filterTables(input);
+
+    List<String> names = result.stream().map(Table::getTableName).toList();
+    assertEquals("Should return tbl1 (cat1 exact match) and tbl2 (null catName 
normalized to hive)", 2, result.size());
+    assertTrue(names.contains("tbl1"));
+    assertTrue(names.contains("tbl2"));
+    assertTrue(!names.contains("tbl3"));
+  }
+
+  /**
+   * 1 lakh tables must complete within 5 seconds.
+   * The original O(n^2) implementation would take 200-300 seconds.
+   * Note: uses inline time assertion instead of @Test(timeout) to avoid JUnit 
4's
+   * separate-thread execution, which breaks Mockito's ThreadLocal-based 
MockedStatic.
+   */
+  @Test
+  public void testFilterTablesPerformance100kTables() throws Exception {
+    int total = 100_000;
+    List<Table> input = IntStream.range(0, total)
+        .mapToObj(i -> createTable("cat1", "db1", "tbl_" + i))
+        .toList();
+
+    when(authorizer.filterListCmdObjects(any(), any())).thenAnswer(invocation 
-> {
+      List<HivePrivilegeObject> objs = invocation.getArgument(0);
+      return objs.stream()
+          .filter(o -> Integer.parseInt(o.getObjectName().split("_")[1]) % 2 
== 0)
+          .map(o -> new HivePrivilegeObject(
+             HivePrivilegeObject.HivePrivilegeObjectType.TABLE_OR_VIEW, 
"cat1", o.getDbname(), o.getObjectName()))
+          .toList();
+    });
+
+    long start = System.currentTimeMillis();
+    List<Table> result = hook.filterTables(input);
+    long elapsed = System.currentTimeMillis() - start;
+
+    assertEquals(total / 2, result.size());
+    assertTrue(String.format("filterTables with %d tables took %d ms, expected 
< 5000 ms", total, elapsed),
+        elapsed < 5000);
+  }
+}

Reply via email to