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 e902912fdad1c2..bf4b0198b76874 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 @@ -744,48 +744,17 @@ private ShowResultSet showCreateInternalCatalogTable(ShowCreateTableStmt showStm MetaUtils.checkDbNullAndReport(db, showStmt.getDb()); List> rows = Lists.newArrayList(); TableName tableName = showStmt.getTbl(); - Table table = MetaUtils.getSessionAwareTable(connectContext, db, tableName); - if (table == null) { + 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 { - Locker dbLocker = new Locker(); - dbLocker.lockDatabase(db.getId(), LockType.READ); - try { - // 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.getIndexNameById(mvMeta.getIndexId()).equals(showStmt.getTable())) { - if (mvMeta.getOriginStmt() == null) { - String mvName = olapTable.getIndexNameById(mvMeta.getIndexId()); - 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", ScalarType.createVarchar(20))) - .addColumn(new Column("Create Materialized View", - ScalarType.createVarchar(30))) - .build(); - return new ShowResultSet(showResultSetMetaData, rows); - } - } - } - } - } finally { - dbLocker.unLockDatabase(db.getId(), LockType.READ); - } - 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); @@ -829,12 +798,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", ScalarType.createVarchar(20))) - .addColumn(new Column("Create Materialized View", ScalarType.createVarchar(30))) - .build(); - return new ShowResultSet(showResultSetMetaData, rows); + return buildShowCreateMaterializedViewResult(table.getName(), createTableStmt.get(0)); } } else { if (showStmt.getType() != ShowCreateTableStmt.CreateTableType.TABLE) { @@ -849,6 +813,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", ScalarType.createVarchar(20))) + .addColumn(new Column("Create Materialized View", ScalarType.createVarchar(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 indexId = olapTable.getIndexIdByName(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 (indexId == null || indexId == olapTable.getBaseIndexId()) { + continue; + } + // Skip shadow / mid-schema-change indexes. + boolean visible = olapTable.getVisibleIndexMetas().stream() + .anyMatch(m -> m.getIndexId() == indexId); + if (!visible) { + continue; + } + MaterializedIndexMeta mvMeta = olapTable.getIndexMetaByIndexId(indexId); + 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, TableName tbl, String catalogName) { String dbName = tbl.getDb(); 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 75d7164413e8b2..6d6524e504d559 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 @@ -629,6 +629,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.analysis.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};