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

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


The following commit(s) were added to refs/heads/master by this push:
     new dcc27718d9a [improvement](catalog) avoid loading every table for SHOW 
TABLES on external catalogs (#66080)
dcc27718d9a is described below

commit dcc27718d9a2db68536ccf3fee458708815256c2
Author: York Cao <[email protected]>
AuthorDate: Tue Aug 11 19:48:31 2026 +0800

    [improvement](catalog) avoid loading every table for SHOW TABLES on 
external catalogs (#66080)
    
    ### What problem does this PR solve?
    
    Issue Number: close #66079
    
    Problem Summary:
    
    `SHOW TABLES` on an external catalog iterated `dbIf.getTables()`, which
    eagerly initializes every table via the meta cache (one
    `getTableNullable()`
    -> remote metadata load per table). For catalogs with many tables this
    turned
    a cheap name listing into N remote loads.
    
    This adds a fast path for the common case only -- a non-verbose `SHOW
    TABLES`
    on an external catalog -- that lists names via
    `dbIf.getTableNamesOrEmptyWithLock()` without initializing any table.
    The
    per-table `SHOW` privilege filter is preserved (it is name-based and
    needs no
    table load), so no table name is leaked to a user who lacks `SHOW` on
    it.
    
    Everything else keeps the original `getTables()` loop unchanged:
    - Internal-catalog `SHOW TABLES` / `SHOW FULL TABLES`.
    - External-catalog `SHOW FULL TABLES` (verbose): it still needs
    `getMysqlType()`
      and the storage-format columns, so it takes the original path and its
      4-column output (`Table_type`, `Storage_format`,
      `Inverted_index_storage_format`) is unchanged.
    - `SHOW VIEWS` (needs `getEngine()` to filter views) and `SHOW STREAMS`.
    
    ### Release note
    
    None (performance optimization; `SHOW TABLES` output for accessible
    tables is
    unchanged, `SHOW FULL TABLES` / `SHOW VIEWS` are unaffected).
    
    ### Check List (For Author)
    
    - Test:
    - Manual test on a live cluster with an HMS catalog: `SHOW TABLES` and
    `SHOW TABLES LIKE` list names via the fast path; `SHOW FULL TABLES`
    returns the correct 4 columns (name, `Table_type`, `Storage_format`,
          `Inverted_index_storage_format`) with no column mismatch.
    - Ran existing `ShowTableCommandTest` (2 passed); FE build (`build.sh
    --fe`)
          + checkstyle green.
        - No new unit test: exercising the external-catalog branch needs an
    ExternalCatalog with a working `getTableNamesOrEmptyWithLock()`, which
    is
    not available in the FE unit-test harness; a portable regression `.out`
          needs the docker HMS test environment.
    - Behavior changed: No material change (a non-loadable external table's
    name is
    now listed by `SHOW TABLES` where the old path silently omitted it on
    load
      failure, which is arguably more correct).
    - Does this need documentation: No
    
    ---------
    
    Co-authored-by: lbs <[email protected]>
---
 .../trees/plans/commands/ShowTableCommand.java     |  21 +++
 .../trees/plans/commands/ShowTableCommandTest.java | 171 +++++++++++++++++++++
 2 files changed, 192 insertions(+)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowTableCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowTableCommand.java
index 0345e1ed30c..73368e914d9 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowTableCommand.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowTableCommand.java
@@ -31,6 +31,7 @@ import org.apache.doris.common.ErrorCode;
 import org.apache.doris.common.ErrorReport;
 import org.apache.doris.common.PatternMatcher;
 import org.apache.doris.common.PatternMatcherWrapper;
+import org.apache.doris.datasource.InternalCatalog;
 import org.apache.doris.mysql.privilege.PrivPredicate;
 import org.apache.doris.nereids.trees.plans.PlanType;
 import org.apache.doris.nereids.trees.plans.commands.info.AliasInfo;
@@ -174,6 +175,26 @@ public class ShowTableCommand extends ShowCommand {
                 
Preconditions.checkArgument(table.get().getType().equals(TableIf.TableType.STREAM));
                 rows.add(Lists.newArrayList(table.get().getName()));
             }
+        } else if (!(dbIf.getCatalog() instanceof InternalCatalog)
+                && !isVerbose && type.equals(PlanType.SHOW_TABLES)) {
+            // Non-verbose SHOW TABLES on an external catalog: list names 
directly
+            // instead of dbIf.getTables(), which loads every table via the 
meta
+            // cache (one remote metadata load per table). The per-table SHOW 
priv
+            // filter below must be kept (name-based, needs no table load).
+            // NOTE: must use getTableNamesWithLock(), NOT 
getTableNamesOrEmptyWithLock():
+            // the latter swallows the case-insensitive name-conflict / 
meta_names_mapping
+            // exception and returns an empty set, silently hiding conflicting 
table names.
+            for (String tableName : dbIf.getTableNamesWithLock()) {
+                if (matcher != null && !matcher.match(tableName)) {
+                    continue;
+                }
+                if (!Env.getCurrentEnv().getAccessManager()
+                        .checkTblPriv(ConnectContext.get(), catalog, 
dbIf.getFullName(), tableName,
+                                PrivPredicate.SHOW)) {
+                    continue;
+                }
+                rows.add(Lists.newArrayList(tableName));
+            }
         } else {
             for (TableIf tbl : dbIf.getTables()) {
                 if (type.equals(PlanType.SHOW_VIEWS) && (tbl.getEngine() == 
null
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ShowTableCommandTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ShowTableCommandTest.java
index 6f273d3df4d..f1715d9cbfc 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ShowTableCommandTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ShowTableCommandTest.java
@@ -18,18 +18,35 @@
 package org.apache.doris.nereids.trees.plans.commands;
 
 import org.apache.doris.backup.CatalogMocker;
+import org.apache.doris.catalog.DatabaseIf;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.TableIf;
 import org.apache.doris.common.AnalysisException;
+import org.apache.doris.datasource.CatalogIf;
+import org.apache.doris.datasource.CatalogMgr;
 import org.apache.doris.datasource.InternalCatalog;
+import org.apache.doris.mysql.privilege.AccessControllerManager;
+import org.apache.doris.mysql.privilege.PrivPredicate;
 import org.apache.doris.nereids.trees.plans.PlanType;
 import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.ShowResultSet;
+import org.apache.doris.qe.StmtExecutor;
 import org.apache.doris.utframe.TestWithFeService;
 
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
 
 import java.io.IOException;
+import java.util.List;
 
 public class ShowTableCommandTest extends TestWithFeService {
+    private static final String CATALOG_NAME = "hive_catalog";
+    private static final String DB_NAME = "hive_db";
+
     private ConnectContext ctx;
 
     private void runBefore() throws IOException {
@@ -63,4 +80,158 @@ public class ShowTableCommandTest extends TestWithFeService 
{
                 "", false, PlanType.SHOW_TABLES);
         Assertions.assertThrows(AnalysisException.class, () -> 
command2.validate(ctx));
     }
+
+    /**
+     * Bundle of mocks needed to drive {@link ShowTableCommand#doRun}: a 
mocked {@link ConnectContext}
+     * whose {@code 
getEnv().getCatalogMgr().getCatalogOrAnalysisException(...).getDbOrAnalysisException(...)}
+     * chain resolves to {@code dbIf}, wired to the given catalog (which 
decides whether
+     * {@code dbIf.getCatalog() instanceof InternalCatalog} holds).
+     */
+    private static final class ShowTableCommandMocks {
+        private final ConnectContext ctx = Mockito.mock(ConnectContext.class);
+        private final StmtExecutor executor = Mockito.mock(StmtExecutor.class);
+        private final Env env = Mockito.mock(Env.class);
+        private final AccessControllerManager accessControllerManager = 
Mockito.mock(AccessControllerManager.class);
+        @SuppressWarnings("unchecked")
+        private final DatabaseIf<TableIf> dbIf = 
Mockito.mock(DatabaseIf.class);
+
+        @SuppressWarnings("unchecked")
+        ShowTableCommandMocks(CatalogIf<?> catalogIf) throws AnalysisException 
{
+            CatalogMgr catalogMgr = Mockito.mock(CatalogMgr.class);
+            Mockito.when(ctx.getEnv()).thenReturn(env);
+            Mockito.when(env.getCatalogMgr()).thenReturn(catalogMgr);
+            
Mockito.when(env.getAccessManager()).thenReturn(accessControllerManager);
+            
Mockito.when(catalogMgr.getCatalogOrAnalysisException(Mockito.anyString())).thenReturn(catalogIf);
+            
Mockito.when(catalogIf.getDbOrAnalysisException(Mockito.anyString())).thenReturn(dbIf);
+            Mockito.when(dbIf.getCatalog()).thenReturn(catalogIf);
+            Mockito.when(dbIf.getFullName()).thenReturn(DB_NAME);
+        }
+    }
+
+    private static ShowResultSet runDoRun(ShowTableCommandMocks mocks, 
ShowTableCommand command) throws Exception {
+        try (MockedStatic<Env> mockedEnv = Mockito.mockStatic(Env.class);
+                MockedStatic<ConnectContext> mockedConnectContext = 
Mockito.mockStatic(ConnectContext.class)) {
+            mockedEnv.when(Env::getCurrentEnv).thenReturn(mocks.env);
+            // Lower-case-table-names off, so LIKE patterns are matched 
case-sensitively.
+            mockedEnv.when(() -> 
Env.getLowerCaseTableNames(Mockito.anyString())).thenReturn(0);
+            
mockedConnectContext.when(ConnectContext::get).thenReturn(mocks.ctx);
+            return command.doRun(mocks.ctx, mocks.executor);
+        }
+    }
+
+    @Test
+    public void testExternalCatalogNonVerboseShowTablesUsesFastPath() throws 
Exception {
+        CatalogIf<?> catalogIf = Mockito.mock(CatalogIf.class);
+        ShowTableCommandMocks mocks = new ShowTableCommandMocks(catalogIf);
+        Mockito.when(mocks.dbIf.getTableNamesWithLock())
+                .thenReturn(ImmutableSet.of("t2", "t1", "t_filtered_out"));
+        // Every table is visible except "t_filtered_out", which SHOW 
privilege denies.
+        Mockito.when(mocks.accessControllerManager.checkTblPriv(
+                Mockito.eq(mocks.ctx), Mockito.eq(CATALOG_NAME), 
Mockito.eq(DB_NAME),
+                Mockito.anyString(), Mockito.eq(PrivPredicate.SHOW)))
+                .thenAnswer(invocation -> 
!"t_filtered_out".equals(invocation.getArgument(3)));
+
+        ShowTableCommand command = new ShowTableCommand(DB_NAME, CATALOG_NAME, 
false, PlanType.SHOW_TABLES);
+        ShowResultSet result = runDoRun(mocks, command);
+
+        // Behavior: names come back sorted, and the privilege-denied table is 
excluded.
+        List<List<String>> rows = result.getResultRows();
+        Assertions.assertEquals(2, rows.size());
+        Assertions.assertEquals(Lists.newArrayList("t1"), rows.get(0));
+        Assertions.assertEquals(Lists.newArrayList("t2"), rows.get(1));
+
+        // Call counts: the fast path must list names, and must never load 
every table.
+        // It uses getTableNamesWithLock() (not the exception-swallowing 
...OrEmpty... variant)
+        // so a name-conflict exception raised while listing is still 
propagated.
+        Mockito.verify(mocks.dbIf, Mockito.times(1)).getTableNamesWithLock();
+        Mockito.verify(mocks.dbIf, Mockito.times(0)).getTables();
+    }
+
+    @Test
+    public void testExternalCatalogShowTablesLikePatternUsesFastPath() throws 
Exception {
+        CatalogIf<?> catalogIf = Mockito.mock(CatalogIf.class);
+        ShowTableCommandMocks mocks = new ShowTableCommandMocks(catalogIf);
+        Mockito.when(mocks.dbIf.getTableNamesWithLock())
+                .thenReturn(ImmutableSet.of("tbl1", "tbl2", "other_tbl"));
+        Mockito.when(mocks.accessControllerManager.checkTblPriv(
+                Mockito.eq(mocks.ctx), Mockito.eq(CATALOG_NAME), 
Mockito.eq(DB_NAME),
+                Mockito.anyString(), Mockito.eq(PrivPredicate.SHOW)))
+                .thenReturn(true);
+
+        // The mysql LIKE pattern is applied on the names-only fast path: 
"tbl_"
+        // matches tbl1/tbl2, but not other_tbl.
+        ShowTableCommand command = new ShowTableCommand(DB_NAME, CATALOG_NAME, 
false, "tbl_", null,
+                PlanType.SHOW_TABLES);
+        ShowResultSet result = runDoRun(mocks, command);
+
+        List<List<String>> rows = result.getResultRows();
+        Assertions.assertEquals(2, rows.size());
+        Assertions.assertEquals(Lists.newArrayList("tbl1"), rows.get(0));
+        Assertions.assertEquals(Lists.newArrayList("tbl2"), rows.get(1));
+        Mockito.verify(mocks.dbIf, Mockito.times(1)).getTableNamesWithLock();
+        Mockito.verify(mocks.dbIf, Mockito.times(0)).getTables();
+    }
+
+    @Test
+    public void testExternalCatalogShowTablesPropagatesNameListingFailure() 
throws Exception {
+        CatalogIf<?> catalogIf = Mockito.mock(CatalogIf.class);
+        ShowTableCommandMocks mocks = new ShowTableCommandMocks(catalogIf);
+        // Guards the getTableNamesWithLock() contract: a case-insensitive 
name-conflict
+        // failure must surface as an error, not be swallowed into an empty 
result.
+        Mockito.when(mocks.dbIf.getTableNamesWithLock()).thenThrow(
+                new RuntimeException("Found conflicting table names under 
case-insensitive conditions"));
+
+        ShowTableCommand command = new ShowTableCommand(DB_NAME, CATALOG_NAME, 
false, PlanType.SHOW_TABLES);
+        Assertions.assertThrows(RuntimeException.class, () -> runDoRun(mocks, 
command));
+        Mockito.verify(mocks.dbIf, Mockito.times(0)).getTables();
+    }
+
+    @Test
+    public void testExternalCatalogVerboseShowTablesUsesSlowPath() throws 
Exception {
+        CatalogIf<?> catalogIf = Mockito.mock(CatalogIf.class);
+        ShowTableCommandMocks mocks = new ShowTableCommandMocks(catalogIf);
+        Mockito.when(mocks.dbIf.getTables()).thenReturn(Lists.newArrayList());
+
+        // Verbose SHOW TABLES needs per-table metadata (storage format, 
etc.), so even on an
+        // external catalog it must fall back to the slow path that loads 
every table.
+        ShowTableCommand command = new ShowTableCommand(DB_NAME, CATALOG_NAME, 
true, PlanType.SHOW_TABLES);
+        ShowResultSet result = runDoRun(mocks, command);
+
+        Assertions.assertTrue(result.getResultRows().isEmpty());
+        Mockito.verify(mocks.dbIf, Mockito.times(1)).getTables();
+        Mockito.verify(mocks.dbIf, Mockito.times(0)).getTableNamesWithLock();
+    }
+
+    @Test
+    public void testExternalCatalogShowViewsUsesSlowPath() throws Exception {
+        CatalogIf<?> catalogIf = Mockito.mock(CatalogIf.class);
+        ShowTableCommandMocks mocks = new ShowTableCommandMocks(catalogIf);
+        Mockito.when(mocks.dbIf.getTables()).thenReturn(Lists.newArrayList());
+
+        // SHOW VIEWS needs the engine type of every table to filter views, so 
the name-only
+        // fast path (guarded by PlanType.SHOW_TABLES) must not be taken here.
+        ShowTableCommand command = new ShowTableCommand(DB_NAME, CATALOG_NAME, 
false, PlanType.SHOW_VIEWS);
+        ShowResultSet result = runDoRun(mocks, command);
+
+        Assertions.assertTrue(result.getResultRows().isEmpty());
+        Mockito.verify(mocks.dbIf, Mockito.times(1)).getTables();
+        Mockito.verify(mocks.dbIf, Mockito.times(0)).getTableNamesWithLock();
+    }
+
+    @Test
+    public void testInternalCatalogNonVerboseShowTablesUsesSlowPath() throws 
Exception {
+        InternalCatalog internalCatalog = Mockito.mock(InternalCatalog.class);
+        ShowTableCommandMocks mocks = new 
ShowTableCommandMocks(internalCatalog);
+        Mockito.when(mocks.dbIf.getTables()).thenReturn(Lists.newArrayList());
+
+        // The fast path is only for external catalogs; internal catalogs 
always take the
+        // slow path regardless of verbosity.
+        ShowTableCommand command = new ShowTableCommand(DB_NAME, 
InternalCatalog.INTERNAL_CATALOG_NAME, false,
+                PlanType.SHOW_TABLES);
+        ShowResultSet result = runDoRun(mocks, command);
+
+        Assertions.assertTrue(result.getResultRows().isEmpty());
+        Mockito.verify(mocks.dbIf, Mockito.times(1)).getTables();
+        Mockito.verify(mocks.dbIf, Mockito.times(0)).getTableNamesWithLock();
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to