From fe6b0016ba07dd1138701b6fac6ce13988f6026e Mon Sep 17 00:00:00 2001 From: Kevin Cai Date: Sat, 2 May 2026 20:02:49 +0800 Subject: [PATCH] [BugFix] Restore SHOW CREATE MATERIALIZED VIEW for sync MVs Sync MVs are mv indexes inside an OLAP table and are not registered as separate Tables in Database.idToTable / nameToTable. Since #43162 ("temporary table part-1") replaced the lookup in showCreateInternalCatalogTable with MetaUtils.getSessionAwareTable, which throws SemanticException on miss, the existing fallback that scanned OLAP tables for a matching mv index has been unreachable - SHOW CREATE MATERIALIZED VIEW has been failing with "Table ... is not found". Catch SemanticException from the lookup and route MV-typed queries to a new findSyncMaterializedViewCreateStmt helper that performs the scan under DB READ (the indexNameToMetaId / indexMetaIdToMeta maps are HashMap-backed and mutated by ALTER, so the scan needs DB-wide exclusion). Non-MV queries rethrow the original "table not found" error unchanged. Signed-off-by: Kevin Cai --- .../java/com/starrocks/qe/ShowExecutor.java | 140 +++++++++++------- .../catalog/MaterializedViewTest.java | 77 ++++++++++ .../R/test_show_create_sync_materialized_view | 39 +++++ .../T/test_show_create_sync_materialized_view | 34 +++++ 4 files changed, 236 insertions(+), 54 deletions(-) create mode 100644 test/sql/test_materialized_view/R/test_show_create_sync_materialized_view create mode 100644 test/sql/test_materialized_view/T/test_show_create_sync_materialized_view diff --git a/fe/fe-core/src/main/java/com/starrocks/qe/ShowExecutor.java b/fe/fe-core/src/main/java/com/starrocks/qe/ShowExecutor.java index bf6d29933560c6..a6faba84bde5c0 100644 --- a/fe/fe-core/src/main/java/com/starrocks/qe/ShowExecutor.java +++ b/fe/fe-core/src/main/java/com/starrocks/qe/ShowExecutor.java @@ -850,56 +850,18 @@ private ShowResultSet showCreateInternalCatalogTable(ShowCreateTableStmt showStm List> rows = Lists.newArrayList(); TableName tableName = new TableName(showStmt.getCatalogName(), showStmt.getDb(), showStmt.getTable()); // Lookup is ConcurrentHashMap-backed (Database.nameToTable) for internal catalog and - // throws SemanticException if the table is missing, so it is safe outside the lock. - Table table = MetaUtils.getSessionAwareTable(connectContext, db, tableName); - // TODO(sync-mv): the (table == null) branch below is currently unreachable - - // MetaUtils.getSessionAwareTable throws SemanticException on miss rather than - // returning null (since #43162, "temporary table part-1"), so the sync-MV - // fallback never runs. As a side effect, SHOW CREATE MATERIALIZED VIEW - // has been broken since that change (sync MVs live as MaterializedIndexMeta - // inside an OLAP table, not as separately-registered Tables, so they are missed - // by the throwing lookup above). The fallback is kept here as a marker until a - // follow-up commit reworks the lookup to make this path reachable. When that - // happens, the iteration must run under DB READ (or per-table READ) - it reads - // HashMap-backed OlapTable index metadata which is mutated by concurrent - // ALTER/rollup. Today the unlocked iteration is harmless because it never - // executes. - if (table == null) { + // throws SemanticException if the name does not match a registered Table. + Table table; + try { + table = MetaUtils.getSessionAwareTable(connectContext, db, tableName); + } catch (SemanticException e) { + // Sync MVs are mv indexes inside an OLAP table and are not registered as Tables, + // so the lookup misses. Fall through to a DB-wide scan for the MV-typed query; + // for any other type, the miss is a real "table not found". if (showStmt.getType() != ShowCreateTableStmt.CreateTableType.MATERIALIZED_VIEW) { - ErrorReport.reportSemanticException(ErrorCode.ERR_BAD_TABLE_ERROR, showStmt.getTable()); - } else { - // For Sync Materialized View, it is a mv index inside OLAP table, - // so we can not get it from database. - for (Table tbl : GlobalStateMgr.getCurrentState().getLocalMetastore().getTables(db.getId())) { - if (tbl.getType() == Table.TableType.OLAP) { - OlapTable olapTable = (OlapTable) tbl; - List visibleMaterializedViews = - olapTable.getVisibleIndexMetas(); - for (MaterializedIndexMeta mvMeta : visibleMaterializedViews) { - if (olapTable.getIndexNameByMetaId(mvMeta.getIndexMetaId()).equals(showStmt.getTable())) { - if (mvMeta.getOriginStmt() == null) { - String mvName = olapTable.getIndexNameByMetaId(mvMeta.getIndexMetaId()); - rows.add(Lists.newArrayList(showStmt.getTable(), - ShowMaterializedViewStatus.buildCreateMVSql(olapTable, - mvName, mvMeta), "utf8", "utf8_general_ci")); - } else { - rows.add(Lists.newArrayList(showStmt.getTable(), mvMeta.getOriginStmt(), - "utf8", "utf8_general_ci")); - } - - ShowResultSetMetaData showResultSetMetaData = ShowResultSetMetaData.builder() - .addColumn(new Column("Materialized View", - TypeFactory.createVarcharType(20))) - .addColumn(new Column("Create Materialized View", - TypeFactory.createVarcharType(30))) - .build(); - return new ShowResultSet(showResultSetMetaData, rows); - } - } - } - } - ErrorReport.reportSemanticException(ErrorCode.ERR_BAD_TABLE_ERROR, showStmt.getTable()); + throw e; } + return findSyncMaterializedViewCreateStmt(connectContext, db, showStmt); } Locker locker = new Locker(); locker.lockTableWithIntensiveDbLock(db.getId(), table.getId(), LockType.READ); @@ -948,12 +910,7 @@ private ShowResultSet showCreateInternalCatalogTable(ShowCreateTableStmt showStm .build(); return new ShowResultSet(showViewResultSetMeta, rows); } else { - rows.add(Lists.newArrayList(table.getName(), createTableStmt.get(0))); - ShowResultSetMetaData showResultSetMetaData = ShowResultSetMetaData.builder() - .addColumn(new Column("Materialized View", TypeFactory.createVarcharType(20))) - .addColumn(new Column("Create Materialized View", TypeFactory.createVarcharType(30))) - .build(); - return new ShowResultSet(showResultSetMetaData, rows); + return buildShowCreateMaterializedViewResult(table.getName(), createTableStmt.get(0)); } } else { if (showStmt.getType() != ShowCreateTableStmt.CreateTableType.TABLE) { @@ -968,6 +925,81 @@ private ShowResultSet showCreateInternalCatalogTable(ShowCreateTableStmt showStm } } + // Shared SHOW CREATE MATERIALIZED VIEW schema for the async and sync MV paths. + private static ShowResultSet buildShowCreateMaterializedViewResult(String mvName, String createSql) { + List> rows = Lists.newArrayList(); + rows.add(Lists.newArrayList(mvName, createSql)); + ShowResultSetMetaData meta = ShowResultSetMetaData.builder() + .addColumn(new Column("Materialized View", TypeFactory.createVarcharType(20))) + .addColumn(new Column("Create Materialized View", TypeFactory.createVarcharType(30))) + .build(); + return new ShowResultSet(meta, rows); + } + + // Sync MVs live as MaterializedIndexMetas inside an OLAP table, not as + // separate Tables, so a name lookup misses and we scan. The visitor's + // pre-execution auth check resolves the sync MV name to a null BasicTable + // and silently no-ops, so we re-check privileges on the owning OLAP table + // once a match is found; the deny error names the sync MV (not the base + // table) to avoid revealing which table hosts the index. + private ShowResultSet findSyncMaterializedViewCreateStmt(ConnectContext connectContext, + Database db, + ShowCreateTableStmt showStmt) { + // ConcurrentHashMap-backed snapshot: weakly consistent, not a point-in-time + // atomic snapshot. Acceptable here - this is a best-effort fallback. + List tablesSnapshot = GlobalStateMgr.getCurrentState().getLocalMetastore().getTables(db.getId()); + Locker locker = new Locker(); + for (Table tbl : tablesSnapshot) { + // Include CLOUD_NATIVE (shared-data) base tables - sync MVs exist + // there too, see LakeSyncMaterializedViewTest. + if (!tbl.isOlapOrCloudNativeTable()) { + continue; + } + OlapTable olapTable = (OlapTable) tbl; + locker.lockTableWithIntensiveDbLock(db.getId(), olapTable.getId(), LockType.READ); + try { + Long metaId = olapTable.getIndexMetaIdByName(showStmt.getTable()); + // Skip the base index (its name equals the table's current name) - only + // reachable here if a concurrent RENAME slipped between the failed name + // lookup that routed us into this fallback and the snapshot below. + if (metaId == null || metaId == olapTable.getBaseIndexMetaId()) { + continue; + } + // Skip shadow / mid-schema-change indexes. + boolean visible = olapTable.getVisibleIndexMetas().stream() + .anyMatch(m -> m.getIndexMetaId() == metaId); + if (!visible) { + continue; + } + MaterializedIndexMeta mvMeta = olapTable.getIndexMetaByMetaId(metaId); + if (mvMeta == null) { + continue; + } + TableName baseTableName = new TableName(InternalCatalog.DEFAULT_INTERNAL_CATALOG_NAME, + db.getFullName(), olapTable.getName()); + try { + Authorizer.checkAnyActionOnTable(connectContext, baseTableName); + } catch (AccessDeniedException denied) { + AccessDeniedException.reportAccessDenied( + InternalCatalog.DEFAULT_INTERNAL_CATALOG_NAME, + connectContext.getCurrentUserIdentity(), + connectContext.getCurrentRoleIds(), + PrivilegeType.ANY.name(), + ObjectType.TABLE.name(), + showStmt.getTable()); + } + String createSql = mvMeta.getOriginStmt() == null + ? ShowMaterializedViewStatus.buildCreateMVSql(olapTable, showStmt.getTable(), mvMeta) + : mvMeta.getOriginStmt(); + return buildShowCreateMaterializedViewResult(showStmt.getTable(), createSql); + } finally { + locker.unLockTableWithIntensiveDbLock(db.getId(), olapTable.getId(), LockType.READ); + } + } + ErrorReport.reportSemanticException(ErrorCode.ERR_BAD_TABLE_ERROR, showStmt.getTable()); + return null; // unreachable; reportSemanticException always throws + } + private ShowResultSet showCreateExternalCatalogTable(ConnectContext context, ShowCreateTableStmt showStmt, String catalogName) { TableRef tableRef = showStmt.getTableRef(); diff --git a/fe/fe-core/src/test/java/com/starrocks/catalog/MaterializedViewTest.java b/fe/fe-core/src/test/java/com/starrocks/catalog/MaterializedViewTest.java index 3026c80aad8d5a..fba3ae72098a96 100644 --- a/fe/fe-core/src/test/java/com/starrocks/catalog/MaterializedViewTest.java +++ b/fe/fe-core/src/test/java/com/starrocks/catalog/MaterializedViewTest.java @@ -625,6 +625,83 @@ public void testShowSyncMV() throws Exception { Assertions.assertEquals(connectContext.getState().getStateType(), QueryState.MysqlStateType.EOF); } + // Regression test: sync MVs are mv indexes inside an OLAP table, not registered as Tables. + // SHOW CREATE MATERIALIZED VIEW must locate the index by name and return its + // CREATE statement instead of failing with "Table not found". + @Test + public void testShowCreateRollupSyncMV() throws Exception { + starRocksAssert.withDatabase("test").useDatabase("test") + .withTable("CREATE TABLE test.tbl_rollup_sync_mv\n" + + "(\n" + + " k1 date,\n" + + " k2 int,\n" + + " v1 int sum\n" + + ")\n" + + "DISTRIBUTED BY HASH(k2) BUCKETS 3\n" + + "PROPERTIES('replication_num' = '1');") + .withMaterializedView( + "create materialized view rollup_sync_mv_to_check as " + + "select k2, sum(v1) as total from tbl_rollup_sync_mv group by k2;"); + String showSql = "show create materialized view rollup_sync_mv_to_check;"; + StatementBase statement = SqlParser.parseSingleStatement(showSql, connectContext.getSessionVariable().getSqlMode()); + StmtExecutor stmtExecutor = new StmtExecutor(connectContext, statement); + stmtExecutor.execute(); + Assertions.assertEquals(QueryState.MysqlStateType.EOF, connectContext.getState().getStateType()); + } + + // Fallback scan exhaustion: a sync-MV-shaped lookup with a name that matches + // nothing in any base table runs through the full snapshot and falls through + // to ErrorReport.reportSemanticException(ERR_BAD_TABLE_ERROR, ...). + @Test + public void testShowCreateSyncMVNotFound() throws Exception { + starRocksAssert.withDatabase("test").useDatabase("test") + .withTable("CREATE TABLE test.tbl_sync_mv_not_found\n" + + "(\n" + + " k1 date,\n" + + " k2 int,\n" + + " v1 int sum\n" + + ")\n" + + "DISTRIBUTED BY HASH(k2) BUCKETS 3\n" + + "PROPERTIES('replication_num' = '1');"); + String showSql = "show create materialized view does_not_exist_anywhere;"; + StatementBase statement = SqlParser.parseSingleStatement(showSql, connectContext.getSessionVariable().getSqlMode()); + StmtExecutor stmtExecutor = new StmtExecutor(connectContext, statement); + stmtExecutor.execute(); + Assertions.assertEquals(QueryState.MysqlStateType.ERR, connectContext.getState().getStateType()); + } + + // Authorization deny path: once the fallback locates the owning base table and + // calls Authorizer.checkAnyActionOnTable, mock the check to throw and verify + // the deny error fires (reportAccessDenied -> ERR state). + @Test + public void testShowCreateSyncMVAccessDenied() throws Exception { + starRocksAssert.withDatabase("test").useDatabase("test") + .withTable("CREATE TABLE test.tbl_sync_mv_auth\n" + + "(\n" + + " k1 date,\n" + + " k2 int,\n" + + " v1 int sum\n" + + ")\n" + + "DISTRIBUTED BY HASH(k2) BUCKETS 3\n" + + "PROPERTIES('replication_num' = '1');") + .withMaterializedView( + "create materialized view sync_mv_auth_deny as " + + "select k2, sum(v1) as total from tbl_sync_mv_auth group by k2;"); + new mockit.MockUp() { + @mockit.Mock + public void checkAnyActionOnTable(ConnectContext context, + com.starrocks.catalog.TableName tableName) + throws com.starrocks.authorization.AccessDeniedException { + throw new com.starrocks.authorization.AccessDeniedException(); + } + }; + String showSql = "show create materialized view sync_mv_auth_deny;"; + StatementBase statement = SqlParser.parseSingleStatement(showSql, connectContext.getSessionVariable().getSqlMode()); + StmtExecutor stmtExecutor = new StmtExecutor(connectContext, statement); + stmtExecutor.execute(); + Assertions.assertEquals(QueryState.MysqlStateType.ERR, connectContext.getState().getStateType()); + } + @Test public void testAlterMVWithIndex() throws Exception { starRocksAssert.withDatabase("test").useDatabase("test") diff --git a/test/sql/test_materialized_view/R/test_show_create_sync_materialized_view b/test/sql/test_materialized_view/R/test_show_create_sync_materialized_view new file mode 100644 index 00000000000000..92373b3965e4bf --- /dev/null +++ b/test/sql/test_materialized_view/R/test_show_create_sync_materialized_view @@ -0,0 +1,39 @@ +-- name: test_show_create_sync_materialized_view @native +create database test_show_create_sync_mv_${uuid0}; +-- result: +-- !result +use test_show_create_sync_mv_${uuid0}; +-- result: +-- !result +create table sync_mv_base +( + k1 date, + k2 int, + v1 int sum +) +distributed by hash(k2) buckets 3 +properties('replication_num' = '1'); +-- result: +-- !result +create materialized view sync_mv_rollup as select k2, sum(v1) from sync_mv_base group by k2; +-- result: +-- !result +function: wait_materialized_view_finish() +-- result: +None +-- !result +show create materialized view sync_mv_rollup; +-- result: +[REGEX]sync_mv_rollup\tcreate materialized view sync_mv_rollup as select.*from sync_mv_base group by.* +-- !result +show create table sync_mv_rollup; +-- result: +[REGEX].*Table.*sync_mv_rollup.*is not found.* +-- !result +show create view sync_mv_rollup; +-- result: +[REGEX].*Table.*sync_mv_rollup.*is not found.* +-- !result +drop database test_show_create_sync_mv_${uuid0}; +-- result: +-- !result diff --git a/test/sql/test_materialized_view/T/test_show_create_sync_materialized_view b/test/sql/test_materialized_view/T/test_show_create_sync_materialized_view new file mode 100644 index 00000000000000..7ca896ba9a437d --- /dev/null +++ b/test/sql/test_materialized_view/T/test_show_create_sync_materialized_view @@ -0,0 +1,34 @@ +-- name: test_show_create_sync_materialized_view @native + +create database test_show_create_sync_mv_${uuid0}; +use test_show_create_sync_mv_${uuid0}; + +create table sync_mv_base +( + k1 date, + k2 int, + v1 int sum +) +distributed by hash(k2) buckets 3 +properties('replication_num' = '1'); + +-- Sync materialized view (legacy rollup style: no DISTRIBUTED BY, no REFRESH +-- clause). Stored as a MaterializedIndexMeta inside the base OLAP table, not +-- as a separately-registered Table. +create materialized view sync_mv_rollup as select k2, sum(v1) from sync_mv_base group by k2; +function: wait_materialized_view_finish() + +-- Regression: SHOW CREATE MATERIALIZED VIEW on a sync MV must locate the +-- MaterializedIndexMeta via the dedicated scan path inside +-- showCreateInternalCatalogTable. Without the fix this returned +-- "Table sync_mv_rollup is not found" because the regular name lookup misses +-- sync MV index names. +show create materialized view sync_mv_rollup; + +-- Negative cases: SHOW CREATE TABLE / VIEW on a sync MV name must still fail +-- with "Table not found" (the fallback scan is only entered for MV-typed +-- queries; see ShowExecutor.showCreateInternalCatalogTable). +show create table sync_mv_rollup; +show create view sync_mv_rollup; + +drop database test_show_create_sync_mv_${uuid0};