From 523dba967fc4644171c80cdd07fc9a6196f665f0 Mon Sep 17 00:00:00 2001 From: Zhigao TONG Date: Sun, 31 May 2026 20:50:41 +0800 Subject: [PATCH 1/3] refactor: update materialized view log privilege checks and error messages Signed-off-by: Zhigao TONG --- pkg/executor/grant.go | 6 +- pkg/executor/materialized_view.go | 62 ++++----------- pkg/executor/show.go | 6 +- .../test/ddl/materialized_view_ddl_test.go | 11 ++- pkg/executor/test/ddl/mview_log_ddl_test.go | 77 +++++++++++++++---- pkg/planner/core/planbuilder.go | 59 ++++++++++---- .../r/executor/mview_privilege.result | 38 ++++----- .../t/executor/mview_privilege.test | 19 +++-- 8 files changed, 165 insertions(+), 113 deletions(-) diff --git a/pkg/executor/grant.go b/pkg/executor/grant.go index aa6b95083d323..b4a403d4994d9 100644 --- a/pkg/executor/grant.go +++ b/pkg/executor/grant.go @@ -614,10 +614,10 @@ func isSafeMViewTableGrantPriv(priv mysql.PrivilegeType) bool { func isSafeMLogTableGrantPriv(priv mysql.PrivilegeType) bool { switch priv { - case mysql.AllPriv, mysql.UsagePriv, mysql.GrantPriv, mysql.SelectPriv: + case mysql.AllPriv, mysql.UsagePriv, mysql.GrantPriv: return true default: - return false + return materializedViewTablePrivs.Has(priv) } } @@ -736,7 +736,7 @@ func tablePrivsForGrantTarget(ctx sessionctx.Context, db string, tbl string) (my case meta.MaterializedView != nil: return materializedViewTablePrivs, nil case meta.MaterializedViewLog != nil: - return mysql.Privileges{mysql.SelectPriv}, nil + return materializedViewTablePrivs, nil case meta.MaterializedViewShadow != nil: return nil, nil default: diff --git a/pkg/executor/materialized_view.go b/pkg/executor/materialized_view.go index a39d42976cd0d..f140d3cf3f583 100644 --- a/pkg/executor/materialized_view.go +++ b/pkg/executor/materialized_view.go @@ -559,57 +559,30 @@ func checkCancelMaterializedViewJobPrivilege( if !found { return cancelMaterializedViewJobNotRunningUserError(stmt) } - baseTable, err := is.TableByName(kctx, pmodel.NewCIStr(dbName), pmodel.NewCIStr(tableName)) - if err != nil { - return errors.Trace(err) - } - if err := checkOperateViewOnAnyDependentMView(ctx, is, baseTable.Meta()); err != nil { - return cancelMaterializedViewJobPrecheckUserError(stmt, err) + if pm.RequestVerification(ctx.GetSessionVars().ActiveRoles, dbName, tableName, "", mysql.OperateViewPriv) { + return nil } - return nil + return cancelMaterializedViewJobPrecheckUserError(stmt, + plannererrors.ErrTableaccessDenied.GenWithStackByArgs("OPERATE VIEW", user.AuthUsername, user.AuthHostname, tableName)) default: return errors.Errorf("invalid materialized view job cancel type: %d", stmt.Tp) } } -func checkOperateViewOnAnyDependentMView( +func checkOperateViewOnMLog( ctx sessionctx.Context, - is infoschema.InfoSchema, - baseTableMeta *model.TableInfo, + schemaName pmodel.CIStr, + mlogName pmodel.CIStr, ) error { pm := privilege.GetPrivilegeManager(ctx) user := ctx.GetSessionVars().User if pm == nil || user == nil { return nil } - - var lastTableName string - if baseTableMeta != nil { - lastTableName = baseTableMeta.Name.L - } - if baseTableMeta == nil || baseTableMeta.MaterializedViewBase == nil || len(baseTableMeta.MaterializedViewBase.MViewIDs) == 0 { - return errors.NewNoStackErrorf( - "no dependent materialized view found for materialized view log on table %s", - lastTableName, - ) - } - - for _, id := range baseTableMeta.MaterializedViewBase.MViewIDs { - mvTable, ok := is.TableByID(context.Background(), id) - if !ok || mvTable.Meta().MaterializedView == nil { - continue - } - dbInfo, ok := infoschema.SchemaByTable(is, mvTable.Meta()) - if !ok { - continue - } - mvName := mvTable.Meta().Name.L - lastTableName = mvName - if pm.RequestVerification(ctx.GetSessionVars().ActiveRoles, dbInfo.Name.L, mvName, "", mysql.OperateViewPriv) { - return nil - } + if pm.RequestVerification(ctx.GetSessionVars().ActiveRoles, schemaName.L, mlogName.L, "", mysql.OperateViewPriv) { + return nil } - return plannererrors.ErrTableaccessDenied.GenWithStackByArgs("OPERATE VIEW", user.AuthUsername, user.AuthHostname, lastTableName) + return plannererrors.ErrTableaccessDenied.GenWithStackByArgs("OPERATE VIEW", user.AuthUsername, user.AuthHostname, mlogName.L) } func checkRefreshMaterializedViewBaseTableSelect( @@ -717,19 +690,16 @@ WHERE PURGE_JOB_ID = %? if !ok { return "", "", false, errors.Errorf("cannot resolve materialized view log %d for cancel job %d", mlogID, purgeJobID) } - mlogInfo := mlogTable.Meta().MaterializedViewLog + mlogMeta := mlogTable.Meta() + mlogInfo := mlogMeta.MaterializedViewLog if mlogInfo == nil { return "", "", false, errors.Errorf("table %d is not a materialized view log", mlogID) } - baseTable, ok := is.TableByID(context.Background(), mlogInfo.BaseTableID) - if !ok { - return "", "", false, errors.Errorf("cannot resolve base table %d for materialized view log %d", mlogInfo.BaseTableID, mlogID) - } - dbInfo, ok := infoschema.SchemaByTable(is, baseTable.Meta()) + dbInfo, ok := infoschema.SchemaByTable(is, mlogMeta) if !ok { - return "", "", false, errors.Errorf("cannot resolve schema for base table %d", mlogInfo.BaseTableID) + return "", "", false, errors.Errorf("cannot resolve schema for materialized view log %d", mlogID) } - return dbInfo.Name.L, baseTable.Meta().Name.L, true, nil + return dbInfo.Name.L, mlogMeta.Name.L, true, nil } // MVCompleteDeltaApplyExec applies COMPLETE DELTA APPLY diff rows to the target MV table. @@ -1787,7 +1757,7 @@ func (e *PurgeMaterializedViewLogExec) executePurgeMaterializedViewLog( if err != nil { return err } - if err := checkOperateViewOnAnyDependentMView(e.Ctx(), domain.GetDomain(e.Ctx()).InfoSchema(), baseTableMeta); err != nil { + if err := checkOperateViewOnMLog(e.Ctx(), schemaName, mlogName); err != nil { return err } releaseCtx := kctx diff --git a/pkg/executor/show.go b/pkg/executor/show.go index 21a9039fb76c9..2da2089699e89 100644 --- a/pkg/executor/show.go +++ b/pkg/executor/show.go @@ -723,13 +723,9 @@ func (e *ShowExec) fetchShowMaterializedViewLogs(ctx context.Context) error { } baseID := tbl.MaterializedViewLog.BaseTableID baseName := "" - baseDB := e.DBName.O if baseID != 0 { if baseTbl, ok := e.is.TableByID(ctx, baseID); ok { baseName = baseTbl.Meta().Name.O - if dbInfo, ok := infoschema.SchemaByTable(e.is, baseTbl.Meta()); ok { - baseDB = dbInfo.Name.O - } } } if baseName == "" { @@ -742,7 +738,7 @@ func (e *ShowExec) fetchShowMaterializedViewLogs(ctx context.Context) error { if fieldPatternsLike != nil && !fieldPatternsLike.DoMatch(lowerMLogName) { continue } - if checker != nil && !checker.RequestVerification(activeRoles, baseDB, baseName, "", mysql.SelectPriv) { + if checker != nil && !hasAnyMaterializedViewVisiblePriv(checker, activeRoles, e.DBName.O, tbl.Name.O) { continue } rows = append(rows, mlogRow{ diff --git a/pkg/executor/test/ddl/materialized_view_ddl_test.go b/pkg/executor/test/ddl/materialized_view_ddl_test.go index edc473eb6fc98..673f11aea1056 100644 --- a/pkg/executor/test/ddl/materialized_view_ddl_test.go +++ b/pkg/executor/test/ddl/materialized_view_ddl_test.go @@ -564,6 +564,15 @@ func TestShowMaterializedViewLogs(t *testing.T) { Check(testkit.Rows(otherExpected)) tk.MustQuery("show materialized view logs in test_show_mlog_other like 'test_show_mlog_other.$mlog$%'"). Check(testkit.Rows()) + + tk.MustExec("create user 'show_mlog_u'@'%' identified by ''") + defer tk.MustExec("drop user 'show_mlog_u'@'%'") + tk.MustExec("grant select on test.t to 'show_mlog_u'@'%'") + tkUser := testkit.NewTestKit(t, store) + require.NoError(t, tkUser.Session().Auth(&auth.UserIdentity{Username: "show_mlog_u", Hostname: "%"}, nil, nil, nil)) + tkUser.MustQuery("show materialized view logs from test").Check(testkit.Rows()) + tk.MustExec("grant alter on test.`$mlog$t` to 'show_mlog_u'@'%'") + tkUser.MustQuery("show materialized view logs from test").Check(testkit.Rows(expected)) } func TestShowMaterializedViewStatusPrivilege(t *testing.T) { @@ -624,7 +633,7 @@ func TestShowMaterializedViewStatusPrivilege(t *testing.T) { err = tkUser.ExecToErr("show materialized view log on test.t_show_mv_status wait_purge") require.ErrorContains(t, err, "SHOW VIEW command denied") - tk.MustExec("grant show view on test.t_show_mv_status to 'show_mv_status_u'@'%'") + tk.MustExec("grant show view on test.`$mlog$t_show_mv_status` to 'show_mv_status_u'@'%'") tkUser.MustQuery("show materialized view log on test.t_show_mv_status wait_purge").Check(testkit.Rows(mlogExpected(2))) } diff --git a/pkg/executor/test/ddl/mview_log_ddl_test.go b/pkg/executor/test/ddl/mview_log_ddl_test.go index 86371d577b735..2ed746e64081f 100644 --- a/pkg/executor/test/ddl/mview_log_ddl_test.go +++ b/pkg/executor/test/ddl/mview_log_ddl_test.go @@ -143,26 +143,26 @@ func TestCreateMaterializedViewLogPrivilege(t *testing.T) { tkNoCreate := testkit.NewTestKit(t, store) require.NoError(t, tkNoCreate.Session().Auth(&auth.UserIdentity{Username: "u_create_mlog_no_create", Hostname: "%"}, nil, nil, nil)) err := tkNoCreate.ExecToErr("create materialized view log on test.t_create_mlog_priv (a)") - require.ErrorContains(t, err, "CREATE command denied") + require.ErrorContains(t, err, "CREATE VIEW command denied") - tk.MustExec("grant create on test.* to 'u_create_mlog_no_select'@'%'") + tk.MustExec("grant create view on test.* to 'u_create_mlog_no_select'@'%'") tkNoSelect := testkit.NewTestKit(t, store) require.NoError(t, tkNoSelect.Session().Auth(&auth.UserIdentity{Username: "u_create_mlog_no_select", Hostname: "%"}, nil, nil, nil)) err = tkNoSelect.ExecToErr("create materialized view log on test.t_create_mlog_priv (a)") require.ErrorContains(t, err, "SELECT command denied") - tk.MustExec("grant create on test.* to 'u_create_mlog_ok'@'%'") + tk.MustExec("grant create view on test.* to 'u_create_mlog_ok'@'%'") tk.MustExec("grant select on test.t_create_mlog_priv to 'u_create_mlog_ok'@'%'") tkOK := testkit.NewTestKit(t, store) require.NoError(t, tkOK.Session().Auth(&auth.UserIdentity{Username: "u_create_mlog_ok", Hostname: "%"}, nil, nil, nil)) tkOK.MustExec("create materialized view log on test.t_create_mlog_priv (a)") - tk.MustExec("grant create on test.t_create_mlog_priv to 'u_create_mlog_table_create'@'%'") + tk.MustExec("grant create view on test.t_create_mlog_priv to 'u_create_mlog_table_create'@'%'") tk.MustExec("grant select on test.t_create_mlog_priv to 'u_create_mlog_table_create'@'%'") tkTableCreate := testkit.NewTestKit(t, store) require.NoError(t, tkTableCreate.Session().Auth(&auth.UserIdentity{Username: "u_create_mlog_table_create", Hostname: "%"}, nil, nil, nil)) err = tkTableCreate.ExecToErr("create materialized view log on test.t_create_mlog_priv (a)") - require.ErrorContains(t, err, "CREATE command denied") + require.ErrorContains(t, err, "CREATE VIEW command denied") } func TestGrantMaterializedViewObjectPrivileges(t *testing.T) { @@ -199,12 +199,13 @@ func TestGrantMaterializedViewObjectPrivileges(t *testing.T) { require.Len(t, rows, 1) mlogTablePrivs := fmt.Sprint(rows[0][0]) require.Contains(t, mlogTablePrivs, "Select") + require.Contains(t, mlogTablePrivs, "Show View") + require.Contains(t, mlogTablePrivs, "Alter") + require.Contains(t, mlogTablePrivs, "Drop") + require.Contains(t, mlogTablePrivs, "Operate View") require.NotContains(t, mlogTablePrivs, "Insert") require.NotContains(t, mlogTablePrivs, "Update") require.NotContains(t, mlogTablePrivs, "Delete") - require.NotContains(t, mlogTablePrivs, "Alter") - require.NotContains(t, mlogTablePrivs, "Drop") - require.NotContains(t, mlogTablePrivs, "Operate View") require.Equal(t, "", fmt.Sprint(rows[0][1])) err = tk.ExecToErr("grant update on test.`$mlog$t_grant_mv_priv` to 'u_grant_mv_priv'@'%'") @@ -227,8 +228,23 @@ func TestShowCreateMaterializedViewLog(t *testing.T) { require.Contains(t, showCreate, "SHARD_ROW_ID_BITS = 2 PRE_SPLIT_REGIONS = 2") require.Contains(t, showCreate, "PURGE START WITH CAST('2026-01-02 03:04:05' AS DATETIME) NEXT DATE_ADD(NOW(), INTERVAL 1 HOUR)") + tk.MustExec("create user 'u_show_create_mlog'@'%'") + defer tk.MustExec("drop user 'u_show_create_mlog'@'%'") + tkShow := testkit.NewTestKit(t, store) + require.NoError(t, tkShow.Session().Auth(&auth.UserIdentity{Username: "u_show_create_mlog", Hostname: "%"}, nil, nil, nil)) + err := tkShow.ExecToErr("show create materialized view log on test.t_show_mlog") + require.ErrorContains(t, err, "SHOW VIEW command denied") + tk.MustExec("grant show view on test.`$mlog$t_show_mlog` to 'u_show_create_mlog'@'%'") + err = tkShow.ExecToErr("show create materialized view log on test.t_show_mlog") + require.ErrorContains(t, err, "SELECT command denied") + tk.MustExec("grant select on test.`$mlog$t_show_mlog` to 'u_show_create_mlog'@'%'") + userRows := tkShow.MustQuery("show create materialized view log on test.t_show_mlog").Rows() + require.Len(t, userRows, 1) + require.Equal(t, "t_show_mlog", userRows[0][0]) + require.Equal(t, showCreate, userRows[0][1]) + tk.MustExec("create table t_no_mlog (a int)") - err := tk.QueryToErr("show create materialized view log on t_no_mlog") + err = tk.QueryToErr("show create materialized view log on t_no_mlog") require.ErrorContains(t, err, "materialized view log does not exist for base table test.t_no_mlog") } @@ -453,15 +469,18 @@ func TestAlterMaterializedViewLogPurgeUpdatesMetaAndNextTime(t *testing.T) { tk.MustExec("drop materialized view log on t") } -func TestAlterMaterializedViewLogPurgeUpdatesNextTimeWithSelectPrivilege(t *testing.T) { +func TestAlterMaterializedViewLogPurgeUpdatesNextTimeWithMLogAlterPrivilege(t *testing.T) { store, dom := testkit.CreateMockStoreAndDomain(t) tk := testkit.NewTestKit(t, store) tk.MustExec("use test") tk.MustExec("create table t (a int, b int)") tk.MustExec("create materialized view log on t (a, b) purge next date_add(now(), interval 2 hour)") tk.MustExec("create user 'mv_alter_purge_u'@'%' identified by ''") + tk.MustExec("create user 'mv_alter_purge_select_u'@'%' identified by ''") defer tk.MustExec("drop user 'mv_alter_purge_u'@'%'") - tk.MustExec("grant select on test.t to 'mv_alter_purge_u'@'%'") + defer tk.MustExec("drop user 'mv_alter_purge_select_u'@'%'") + tk.MustExec("grant alter on test.`$mlog$t` to 'mv_alter_purge_u'@'%'") + tk.MustExec("grant select on test.t to 'mv_alter_purge_select_u'@'%'") getMLogMeta := func() (int64, string, string, string) { is := dom.InfoSchema() @@ -478,6 +497,11 @@ func TestAlterMaterializedViewLogPurgeUpdatesNextTimeWithSelectPrivilege(t *test require.NoError(t, tkUser.Session().Auth(&auth.UserIdentity{Username: "mv_alter_purge_u", Hostname: "%"}, nil, nil, nil)) tkUser.MustExec("alter materialized view log on test.t purge next date_add(now(), interval 25 minute)") + tkSelectUser := testkit.NewTestKit(t, store) + require.NoError(t, tkSelectUser.Session().Auth(&auth.UserIdentity{Username: "mv_alter_purge_select_u", Hostname: "%"}, nil, nil, nil)) + err := tkSelectUser.ExecToErr("alter materialized view log on test.t purge next date_add(now(), interval 30 minute)") + require.ErrorContains(t, err, "ALTER command denied") + mlogID, purgeMethod, purgeStartWith, purgeNext := getMLogMeta() require.Equal(t, "DEFERRED", purgeMethod) require.Equal(t, "", purgeStartWith) @@ -488,6 +512,29 @@ func TestAlterMaterializedViewLogPurgeUpdatesNextTimeWithSelectPrivilege(t *test )).Check(testkit.Rows("1 1 1")) } +func TestDropMaterializedViewLogPrivilege(t *testing.T) { + store := testkit.CreateMockStore(t) + tk := testkit.NewTestKit(t, store) + tk.MustExec("use test") + tk.MustExec("create table t_drop_mlog_priv (a int)") + tk.MustExec("create materialized view log on t_drop_mlog_priv (a)") + tk.MustExec("create user 'u_drop_mlog_select'@'%'") + tk.MustExec("create user 'u_drop_mlog_ok'@'%'") + defer tk.MustExec("drop user 'u_drop_mlog_select'@'%'") + defer tk.MustExec("drop user 'u_drop_mlog_ok'@'%'") + tk.MustExec("grant select on test.t_drop_mlog_priv to 'u_drop_mlog_select'@'%'") + tk.MustExec("grant drop on test.`$mlog$t_drop_mlog_priv` to 'u_drop_mlog_ok'@'%'") + + tkSelect := testkit.NewTestKit(t, store) + require.NoError(t, tkSelect.Session().Auth(&auth.UserIdentity{Username: "u_drop_mlog_select", Hostname: "%"}, nil, nil, nil)) + err := tkSelect.ExecToErr("drop materialized view log on test.t_drop_mlog_priv") + require.ErrorContains(t, err, "DROP command denied") + + tkDrop := testkit.NewTestKit(t, store) + require.NoError(t, tkDrop.Session().Auth(&auth.UserIdentity{Username: "u_drop_mlog_ok", Hostname: "%"}, nil, nil, nil)) + tkDrop.MustExec("drop materialized view log on test.t_drop_mlog_priv") +} + func TestAlterMaterializedViewLogPurgeBestEffortInfoUpdateWarning(t *testing.T) { store, dom := testkit.CreateMockStoreAndDomain(t) tk := testkit.NewTestKit(t, store) @@ -884,7 +931,7 @@ func TestPurgeMaterializedViewLogPrivilege(t *testing.T) { defer tk.MustExec("drop user 'u2'@'%'") defer tk.MustExec("drop user 'u3'@'%'") tk.MustExec("grant select on test.t_purge_priv to 'u1'@'%'") - tk.MustExec("grant operate view on test.mv_purge_priv to 'u2'@'%'") + tk.MustExec("grant operate view on test.`$mlog$t_purge_priv` to 'u2'@'%'") tk.MustExec("grant operate view on *.* to 'u3'@'%'") tkUser := testkit.NewTestKit(t, store) @@ -899,13 +946,11 @@ func TestPurgeMaterializedViewLogPrivilege(t *testing.T) { tkUser.MustExec("use test") tkUser.MustExec("purge materialized view log on t_purge_priv") - // Even global OPERATE VIEW is not sufficient when the mlog no longer has dependent MVs. tk.MustExec("drop materialized view mv_purge_priv") tkUser = testkit.NewTestKit(t, store) require.NoError(t, tkUser.Session().Auth(&auth.UserIdentity{Username: "u3", Hostname: "%"}, nil, nil, nil)) tkUser.MustExec("use test") - err = tkUser.ExecToErr("purge materialized view log on t_purge_priv") - require.ErrorContains(t, err, "no dependent materialized view found") + tkUser.MustExec("purge materialized view log on t_purge_priv") } func TestPurgeMaterializedViewLogCancelWatcherUsesHistRequest(t *testing.T) { @@ -1049,7 +1094,7 @@ func TestCancelMaterializedViewLogPurgeJob(t *testing.T) { require.NoError(t, tkCancel.Session().Auth(&auth.UserIdentity{Username: "mv_purge_cancel_u", Hostname: "%"}, nil, nil, nil)) err = tkCancel.ExecToErr(fmt.Sprintf("cancel materialized view log purge job %s", jobID)) require.ErrorContains(t, err, "cannot cancel materialized view log purge job") - tk.MustExec("grant operate view on test.mv_purge_cancel_job to 'mv_purge_cancel_u'@'%'") + tk.MustExec("grant operate view on test.`$mlog$t_purge_cancel_job` to 'mv_purge_cancel_u'@'%'") tkCancel.MustExec(fmt.Sprintf("cancel materialized view log purge job %s", jobID)) time.Sleep(300 * time.Millisecond) diff --git a/pkg/planner/core/planbuilder.go b/pkg/planner/core/planbuilder.go index c6315e4cbdda6..7077d9931afbf 100644 --- a/pkg/planner/core/planbuilder.go +++ b/pkg/planner/core/planbuilder.go @@ -3594,10 +3594,19 @@ func (b *PlanBuilder) buildShow(ctx context.Context, show *ast.ShowStmt) (base.P } user := b.ctx.GetSessionVars().User if show.Tp == ast.ShowCreateMaterializedViewLog { + dbName := show.Table.Schema.L + if dbName == "" { + dbName = b.ctx.GetSessionVars().CurrentDB + } + mlogName := b.materializedViewLogNameForBaseTable(ctx, dbName, show.Table.Name) + if user != nil { + err = plannererrors.ErrTableaccessDenied.GenWithStackByArgs("SHOW VIEW", user.AuthUsername, user.AuthHostname, mlogName.L) + } + b.visitInfo = appendVisitInfo(b.visitInfo, mysql.ShowViewPriv, dbName, mlogName.L, "", err) if user != nil { - err = plannererrors.ErrTableaccessDenied.GenWithStackByArgs("SELECT", user.AuthUsername, user.AuthHostname, show.Table.Name.L) + err = plannererrors.ErrTableaccessDenied.GenWithStackByArgs("SELECT", user.AuthUsername, user.AuthHostname, mlogName.L) } - b.visitInfo = appendVisitInfo(b.visitInfo, mysql.SelectPriv, show.Table.Schema.L, show.Table.Name.L, "", err) + b.visitInfo = appendVisitInfo(b.visitInfo, mysql.SelectPriv, dbName, mlogName.L, "", err) } else if isView { if user != nil { err = plannererrors.ErrTableaccessDenied.GenWithStackByArgs("SHOW VIEW", user.AuthUsername, user.AuthHostname, show.Table.Name.L) @@ -3634,12 +3643,23 @@ func (b *PlanBuilder) buildShow(ctx context.Context, show *ast.ShowStmt) (base.P err = plannererrors.ErrTableaccessDenied.GenWithStackByArgs("SHOW VIEW", user.AuthUsername, user.AuthHostname, show.Table.Name.L) } b.visitInfo = appendVisitInfo(b.visitInfo, mysql.ShowViewPriv, show.Table.Schema.L, show.Table.Name.L, "", err) - case ast.ShowMaterializedViewRemainLogs, ast.ShowMaterializedViewLogWaitPurge: + case ast.ShowMaterializedViewRemainLogs: var err error if user := b.ctx.GetSessionVars().User; user != nil { err = plannererrors.ErrTableaccessDenied.GenWithStackByArgs("SHOW VIEW", user.AuthUsername, user.AuthHostname, show.Table.Name.L) } b.visitInfo = appendVisitInfo(b.visitInfo, mysql.ShowViewPriv, show.Table.Schema.L, show.Table.Name.L, "", err) + case ast.ShowMaterializedViewLogWaitPurge: + var err error + dbName := show.Table.Schema.L + if dbName == "" { + dbName = b.ctx.GetSessionVars().CurrentDB + } + mlogName := b.materializedViewLogNameForBaseTable(ctx, dbName, show.Table.Name) + if user := b.ctx.GetSessionVars().User; user != nil { + err = plannererrors.ErrTableaccessDenied.GenWithStackByArgs("SHOW VIEW", user.AuthUsername, user.AuthHostname, mlogName.L) + } + b.visitInfo = appendVisitInfo(b.visitInfo, mysql.ShowViewPriv, dbName, mlogName.L, "", err) case ast.ShowBackups: err := plannererrors.ErrSpecificAccessDenied.GenWithStackByArgs("SUPER or BACKUP_ADMIN") b.visitInfo = appendDynamicVisitInfo(b.visitInfo, []string{"BACKUP_ADMIN"}, false, err) @@ -6255,14 +6275,15 @@ func (b *PlanBuilder) buildDDL(ctx context.Context, node ast.DDLNode) (base.Plan if dbName == "" { return nil, plannererrors.ErrNoDB } + mlogName := b.materializedViewLogNameForBaseTable(ctx, dbName, v.Table.Name) var createAuthErr, selectAuthErr error if user := b.ctx.GetSessionVars().User; user != nil { - createAuthErr = plannererrors.ErrTableaccessDenied.GenWithStackByArgs("CREATE", user.AuthUsername, user.AuthHostname, v.Table.Name.L) + createAuthErr = plannererrors.ErrTableaccessDenied.GenWithStackByArgs("CREATE VIEW", user.AuthUsername, user.AuthHostname, mlogName.L) selectAuthErr = plannererrors.ErrTableaccessDenied.GenWithStackByArgs("SELECT", user.AuthUsername, user.AuthHostname, v.Table.Name.L) } - // Creating a materialized view log requires CREATE TABLE privilege on the schema and - // SELECT privilege on the base table. - b.visitInfo = appendVisitInfo(b.visitInfo, mysql.CreatePriv, dbName, "", "", createAuthErr) + // Creating a materialized view log creates a view-like maintenance object and + // reads base table metadata/columns. + b.visitInfo = appendVisitInfo(b.visitInfo, mysql.CreateViewPriv, dbName, mlogName.L, "", createAuthErr) b.visitInfo = appendVisitInfo(b.visitInfo, mysql.SelectPriv, dbName, v.Table.Name.L, "", selectAuthErr) case *ast.DropMaterializedViewLogStmt: dbName := v.Table.Schema.L @@ -6272,11 +6293,12 @@ func (b *PlanBuilder) buildDDL(ctx context.Context, node ast.DDLNode) (base.Plan if dbName == "" { return nil, plannererrors.ErrNoDB } + mlogName := b.materializedViewLogNameForBaseTable(ctx, dbName, v.Table.Name) if b.ctx.GetSessionVars().User != nil { - authErr = plannererrors.ErrTableaccessDenied.GenWithStackByArgs("SELECT", b.ctx.GetSessionVars().User.AuthUsername, - b.ctx.GetSessionVars().User.AuthHostname, v.Table.Name.L) + authErr = plannererrors.ErrTableaccessDenied.GenWithStackByArgs("DROP", b.ctx.GetSessionVars().User.AuthUsername, + b.ctx.GetSessionVars().User.AuthHostname, mlogName.L) } - b.visitInfo = appendVisitInfo(b.visitInfo, mysql.SelectPriv, dbName, v.Table.Name.L, "", authErr) + b.visitInfo = appendVisitInfo(b.visitInfo, mysql.DropPriv, dbName, mlogName.L, "", authErr) case *ast.AlterMaterializedViewLogStmt: dbName := v.Table.Schema.L if dbName == "" { @@ -6285,11 +6307,12 @@ func (b *PlanBuilder) buildDDL(ctx context.Context, node ast.DDLNode) (base.Plan if dbName == "" { return nil, plannererrors.ErrNoDB } + mlogName := b.materializedViewLogNameForBaseTable(ctx, dbName, v.Table.Name) if b.ctx.GetSessionVars().User != nil { - authErr = plannererrors.ErrTableaccessDenied.GenWithStackByArgs("SELECT", b.ctx.GetSessionVars().User.AuthUsername, - b.ctx.GetSessionVars().User.AuthHostname, v.Table.Name.L) + authErr = plannererrors.ErrTableaccessDenied.GenWithStackByArgs("ALTER", b.ctx.GetSessionVars().User.AuthUsername, + b.ctx.GetSessionVars().User.AuthHostname, mlogName.L) } - b.visitInfo = appendVisitInfo(b.visitInfo, mysql.SelectPriv, dbName, v.Table.Name.L, "", authErr) + b.visitInfo = appendVisitInfo(b.visitInfo, mysql.AlterPriv, dbName, mlogName.L, "", authErr) case *ast.OptimizeTableStmt: return nil, dbterror.ErrGeneralUnsupportedDDL.GenWithStack("OPTIMIZE TABLE is not supported") } @@ -6297,6 +6320,16 @@ func (b *PlanBuilder) buildDDL(ctx context.Context, node ast.DDLNode) (base.Plan return p, nil } +func (b *PlanBuilder) materializedViewLogNameForBaseTable(ctx context.Context, dbName string, baseName pmodel.CIStr) pmodel.CIStr { + if dbName == "" { + dbName = b.ctx.GetSessionVars().CurrentDB + } + if baseTable, err := b.is.TableByName(ctx, pmodel.NewCIStr(dbName), baseName); err == nil { + return pmodel.NewCIStr("$mlog$" + baseTable.Meta().Name.O) + } + return pmodel.NewCIStr("$mlog$" + baseName.O) +} + const ( // TraceFormatRow indicates row tracing format. TraceFormatRow = "row" diff --git a/tests/integrationtest/r/executor/mview_privilege.result b/tests/integrationtest/r/executor/mview_privilege.result index e794de0066f9f..365a8f692650a 100644 --- a/tests/integrationtest/r/executor/mview_privilege.result +++ b/tests/integrationtest/r/executor/mview_privilege.result @@ -76,8 +76,7 @@ create user 'u_show_log'@'%'; create user 'u_refresh_select'@'%'; grant show view on mview_privilege_test.mv to 'u_show_mv'@'%'; grant show view, select on mview_privilege_test.mv to 'u_show_create'@'%'; -grant select on mview_privilege_test.t to 'u_show_log'@'%'; -grant show view on mview_privilege_test.t to 'u_show_log'@'%'; +grant show view, select on mview_privilege_test.`$mlog$t` to 'u_show_log'@'%'; grant operate view on mview_privilege_test.mv to 'u_refresh_select'@'%'; grant select on mview_privilege_test.t to 'u_refresh_select'@'%'; create user 'u_db_refresh'@'%'; @@ -107,7 +106,7 @@ create user 'u_purge_sel'@'%'; create user 'u_purge_op'@'%'; create user 'u_purge_global'@'%'; grant select on mview_privilege_test.t to 'u_purge_sel'@'%'; -grant operate view on mview_privilege_test.mv to 'u_purge_op'@'%'; +grant operate view on mview_privilege_test.`$mlog$t` to 'u_purge_op'@'%'; grant operate view on *.* to 'u_purge_global'@'%'; create user 'u_dml'@'%'; grant select, insert, update, delete on mview_privilege_test.* to 'u_dml'@'%'; @@ -157,21 +156,22 @@ show create materialized view log on t; Materialized View Log Create Materialized View Log t CREATE MATERIALIZED VIEW LOG ON `t` (`a`, `b`) PURGE NEXT DATE_ADD(NOW(), INTERVAL 1 HOUR) show materialized view log on t_hidden wait_purge; -Error 1142 (42000): SHOW VIEW command denied to user 'u_show_log'@'%' for table 't_hidden' -revoke select on mview_privilege_test.t from 'u_show_log'@'%'; +Error 1142 (42000): SHOW VIEW command denied to user 'u_show_log'@'%' for table '$mlog$t_hidden' +revoke select on mview_privilege_test.`$mlog$t` from 'u_show_log'@'%'; show grants for 'u_show_log'@'%'; Grants for u_show_log@% GRANT USAGE ON *.* TO 'u_show_log'@'%' -GRANT SHOW VIEW ON `mview_privilege_test`.`t` TO 'u_show_log'@'%' +GRANT SHOW VIEW ON `mview_privilege_test`.`$mlog$t` TO 'u_show_log'@'%' show materialized views from mview_privilege_test; mview_id mview_name show materialized view logs from mview_privilege_test; mlog_id mlog_name base_table_id base_table_name + $mlog$t t show materialized view log on mview_privilege_test.t wait_purge; mlog_id mlog_name base_table_id base_table_name wait_purge $mlog$t t 0 show create materialized view log on mview_privilege_test.t; -Error 1142 (42000): SELECT command denied to user 'u_show_log'@'%' for table 't' +Error 1142 (42000): SELECT command denied to user 'u_show_log'@'%' for table '$mlog$t' refresh materialized view mv complete delta apply; revoke select on mview_privilege_test.t from 'u_refresh_select'@'%'; show grants for 'u_refresh_select'@'%'; @@ -236,24 +236,24 @@ Error 1288 (HY000): The target table $mlog$t of the UPDATE is not updatable delete from `$mlog$t`; Error 1288 (HY000): The target table $mlog$t of the DELETE is not updatable purge materialized view log on t; -Error 1142 (42000): OPERATE VIEW command denied to user 'u_purge_sel'@'%' for table 'mv' +Error 1142 (42000): OPERATE VIEW command denied to user 'u_purge_sel'@'%' for table '$mlog$t' purge materialized view log on t; -revoke operate view on mview_privilege_test.mv from 'u_purge_op'@'%'; +revoke operate view on mview_privilege_test.`$mlog$t` from 'u_purge_op'@'%'; show grants for 'u_purge_op'@'%'; Grants for u_purge_op@% GRANT USAGE ON *.* TO 'u_purge_op'@'%' purge materialized view log on mview_privilege_test.t; -Error 1142 (42000): OPERATE VIEW command denied to user 'u_purge_op'@'%' for table 'mv' +Error 1142 (42000): OPERATE VIEW command denied to user 'u_purge_op'@'%' for table '$mlog$t' purge materialized view log on t; revoke operate view on *.* from 'u_purge_global'@'%'; show grants for 'u_purge_global'@'%'; Grants for u_purge_global@% GRANT USAGE ON *.* TO 'u_purge_global'@'%' purge materialized view log on mview_privilege_test.t; -Error 1142 (42000): OPERATE VIEW command denied to user 'u_purge_global'@'%' for table 'mv' +Error 1142 (42000): OPERATE VIEW command denied to user 'u_purge_global'@'%' for table '$mlog$t' drop materialized view mv; purge materialized view log on mview_privilege_test.t; -Error 1105 (HY000): no dependent materialized view found for materialized view log on table t +Error 1142 (42000): OPERATE VIEW command denied to user 'u_purge_global'@'%' for table '$mlog$t' create table t_revoke_ddl (id int not null, v int not null); insert into t_revoke_ddl values (1, 10), (2, 20); create materialized view log on t_revoke_ddl (id, v) purge next date_add(now(), interval 1 hour); @@ -272,15 +272,15 @@ GRANT SELECT ON `mview_privilege_test`.`t_revoke_ddl` TO 'u_create_mv'@'%' create materialized view mv_revoke_create (id, s, cnt) as select id, sum(v), count(1) from t_revoke_ddl group by id; Error 1142 (42000): CREATE VIEW command denied to user 'u_create_mv'@'%' for table 'mv_revoke_create' create user 'u_create_mlog'@'%'; -grant create on mview_privilege_test.* to 'u_create_mlog'@'%'; +grant create view on mview_privilege_test.* to 'u_create_mlog'@'%'; grant select on mview_privilege_test.t_revoke_create_mlog to 'u_create_mlog'@'%'; -revoke create on mview_privilege_test.* from 'u_create_mlog'@'%'; +revoke create view on mview_privilege_test.* from 'u_create_mlog'@'%'; show grants for 'u_create_mlog'@'%'; Grants for u_create_mlog@% GRANT USAGE ON *.* TO 'u_create_mlog'@'%' GRANT SELECT ON `mview_privilege_test`.`t_revoke_create_mlog` TO 'u_create_mlog'@'%' create materialized view log on t_revoke_create_mlog (id, v); -Error 1142 (42000): CREATE command denied to user 'u_create_mlog'@'%' for table 't_revoke_create_mlog' +Error 1142 (42000): CREATE VIEW command denied to user 'u_create_mlog'@'%' for table '$mlog$t_revoke_create_mlog' create user 'u_alter_mv'@'%'; grant alter on mview_privilege_test.mv_revoke_ddl to 'u_alter_mv'@'%'; revoke alter on mview_privilege_test.mv_revoke_ddl from 'u_alter_mv'@'%'; @@ -298,15 +298,15 @@ GRANT USAGE ON *.* TO 'u_drop_mv'@'%' drop materialized view mview_privilege_test.mv_revoke_ddl; Error 1142 (42000): DROP command denied to user 'u_drop_mv'@'%' for table 'mv_revoke_ddl' create user 'u_mlog_select'@'%'; -grant select on mview_privilege_test.t_revoke_ddl to 'u_mlog_select'@'%'; -revoke select on mview_privilege_test.t_revoke_ddl from 'u_mlog_select'@'%'; +grant alter, drop on mview_privilege_test.`$mlog$t_revoke_ddl` to 'u_mlog_select'@'%'; +revoke alter, drop on mview_privilege_test.`$mlog$t_revoke_ddl` from 'u_mlog_select'@'%'; show grants for 'u_mlog_select'@'%'; Grants for u_mlog_select@% GRANT USAGE ON *.* TO 'u_mlog_select'@'%' alter materialized view log on mview_privilege_test.t_revoke_ddl purge next date_add(now(), interval 1 hour); -Error 1142 (42000): SELECT command denied to user 'u_mlog_select'@'%' for table 't_revoke_ddl' +Error 1142 (42000): ALTER command denied to user 'u_mlog_select'@'%' for table '$mlog$t_revoke_ddl' drop materialized view log on mview_privilege_test.t_revoke_ddl; -Error 1142 (42000): SELECT command denied to user 'u_mlog_select'@'%' for table 't_revoke_ddl' +Error 1142 (42000): DROP command denied to user 'u_mlog_select'@'%' for table '$mlog$t_revoke_ddl' drop user if exists 'u_role'@'%'; drop role if exists r_operate; drop user if exists 'u_show_mv'@'%'; diff --git a/tests/integrationtest/t/executor/mview_privilege.test b/tests/integrationtest/t/executor/mview_privilege.test index 7b7feaa8980d7..0db41866d7bd5 100644 --- a/tests/integrationtest/t/executor/mview_privilege.test +++ b/tests/integrationtest/t/executor/mview_privilege.test @@ -31,8 +31,7 @@ create user 'u_show_log'@'%'; create user 'u_refresh_select'@'%'; grant show view on mview_privilege_test.mv to 'u_show_mv'@'%'; grant show view, select on mview_privilege_test.mv to 'u_show_create'@'%'; -grant select on mview_privilege_test.t to 'u_show_log'@'%'; -grant show view on mview_privilege_test.t to 'u_show_log'@'%'; +grant show view, select on mview_privilege_test.`$mlog$t` to 'u_show_log'@'%'; grant operate view on mview_privilege_test.mv to 'u_refresh_select'@'%'; grant select on mview_privilege_test.t to 'u_refresh_select'@'%'; @@ -61,7 +60,7 @@ create user 'u_purge_sel'@'%'; create user 'u_purge_op'@'%'; create user 'u_purge_global'@'%'; grant select on mview_privilege_test.t to 'u_purge_sel'@'%'; -grant operate view on mview_privilege_test.mv to 'u_purge_op'@'%'; +grant operate view on mview_privilege_test.`$mlog$t` to 'u_purge_op'@'%'; grant operate view on *.* to 'u_purge_global'@'%'; create user 'u_dml'@'%'; @@ -121,7 +120,7 @@ show materialized view log on t_hidden wait_purge; disconnect u_show_log; connection default; -revoke select on mview_privilege_test.t from 'u_show_log'@'%'; +revoke select on mview_privilege_test.`$mlog$t` from 'u_show_log'@'%'; show grants for 'u_show_log'@'%'; connect (u_show_log, localhost, u_show_log,,); connection u_show_log; @@ -234,7 +233,7 @@ purge materialized view log on t; disconnect u_purge_op; connection default; -revoke operate view on mview_privilege_test.mv from 'u_purge_op'@'%'; +revoke operate view on mview_privilege_test.`$mlog$t` from 'u_purge_op'@'%'; show grants for 'u_purge_op'@'%'; connect (u_purge_op, localhost, u_purge_op,,); connection u_purge_op; @@ -262,7 +261,7 @@ drop materialized view mv; connect (u_purge_global, localhost, u_purge_global,,); connection u_purge_global; --- error 1105 +-- error 1142 purge materialized view log on mview_privilege_test.t; disconnect u_purge_global; @@ -289,9 +288,9 @@ disconnect u_create_mv; connection default; create user 'u_create_mlog'@'%'; -grant create on mview_privilege_test.* to 'u_create_mlog'@'%'; +grant create view on mview_privilege_test.* to 'u_create_mlog'@'%'; grant select on mview_privilege_test.t_revoke_create_mlog to 'u_create_mlog'@'%'; -revoke create on mview_privilege_test.* from 'u_create_mlog'@'%'; +revoke create view on mview_privilege_test.* from 'u_create_mlog'@'%'; show grants for 'u_create_mlog'@'%'; connect (u_create_mlog, localhost, u_create_mlog,,mview_privilege_test); connection u_create_mlog; @@ -323,8 +322,8 @@ disconnect u_drop_mv; connection default; create user 'u_mlog_select'@'%'; -grant select on mview_privilege_test.t_revoke_ddl to 'u_mlog_select'@'%'; -revoke select on mview_privilege_test.t_revoke_ddl from 'u_mlog_select'@'%'; +grant alter, drop on mview_privilege_test.`$mlog$t_revoke_ddl` to 'u_mlog_select'@'%'; +revoke alter, drop on mview_privilege_test.`$mlog$t_revoke_ddl` from 'u_mlog_select'@'%'; show grants for 'u_mlog_select'@'%'; connect (u_mlog_select, localhost, u_mlog_select,,); connection u_mlog_select; From 04905c7e4077a5d2a499b136fd6c9974322e4393 Mon Sep 17 00:00:00 2001 From: Zhigao TONG Date: Sun, 31 May 2026 21:26:21 +0800 Subject: [PATCH 2/3] refactor: replace manual materialized view log name construction with dedicated function Signed-off-by: Zhigao TONG --- pkg/ddl/executor.go | 3 +-- pkg/ddl/materialized_view.go | 6 +++--- pkg/ddl/schematracker/checker.go | 2 +- pkg/ddl/schematracker/dm_tracker.go | 3 +-- pkg/executor/materialized_view.go | 2 +- pkg/meta/model/table.go | 8 ++++++++ pkg/planner/core/planbuilder.go | 4 ++-- 7 files changed, 17 insertions(+), 11 deletions(-) diff --git a/pkg/ddl/executor.go b/pkg/ddl/executor.go index 18c248882e7b4..de5ba6aa910ad 100644 --- a/pkg/ddl/executor.go +++ b/pkg/ddl/executor.go @@ -1086,8 +1086,7 @@ func (e *executor) CreateMaterializedViewLog(ctx sessionctx.Context, s *ast.Crea return dbterror.ErrWrongObject.GenWithStackByArgs(schemaName, s.Table.Name, "BASE TABLE") } - mlogName := "$mlog$" + baseTable.Meta().Name.O - mlogNameCIStr := pmodel.NewCIStr(mlogName) + mlogNameCIStr := model.MaterializedViewLogTableName(baseTable.Meta().Name) if err := checkTooLongTable(mlogNameCIStr); err != nil { return err } diff --git a/pkg/ddl/materialized_view.go b/pkg/ddl/materialized_view.go index 35d12bd13404f..e6532b32d772a 100644 --- a/pkg/ddl/materialized_view.go +++ b/pkg/ddl/materialized_view.go @@ -235,7 +235,7 @@ func (e *executor) CreateMaterializedView(ctx sessionctx.Context, s *ast.CreateM } baseTableID := baseTable.Meta().ID - mlogName := pmodel.NewCIStr("$mlog$" + baseTable.Meta().Name.O) + mlogName := model.MaterializedViewLogTableName(baseTable.Meta().Name) mlogTable, err := is.TableByName(e.ctx, baseTableName.Schema, mlogName) if err != nil { if infoschema.ErrTableNotExists.Equal(err) { @@ -435,7 +435,7 @@ func (e *executor) DropMaterializedViewLog(ctx sessionctx.Context, s *ast.DropMa } baseTableID := baseTable.Meta().ID - mlogName := pmodel.NewCIStr("$mlog$" + baseTable.Meta().Name.O) + mlogName := model.MaterializedViewLogTableName(baseTable.Meta().Name) mlogTable, err := is.TableByName(e.ctx, schemaName, mlogName) if err != nil { return err @@ -563,7 +563,7 @@ func (e *executor) AlterMaterializedViewLog(ctx sessionctx.Context, s *ast.Alter } baseTableID := baseTable.Meta().ID - mlogName := pmodel.NewCIStr("$mlog$" + baseTable.Meta().Name.O) + mlogName := model.MaterializedViewLogTableName(baseTable.Meta().Name) mlogTable, err := is.TableByName(e.ctx, schemaName, mlogName) if err != nil { return err diff --git a/pkg/ddl/schematracker/checker.go b/pkg/ddl/schematracker/checker.go index 8cfdec696415e..d960bff12d599 100644 --- a/pkg/ddl/schematracker/checker.go +++ b/pkg/ddl/schematracker/checker.go @@ -292,7 +292,7 @@ func (d *Checker) CreateMaterializedViewLog(ctx sessionctx.Context, stmt *ast.Cr if schemaName.O == "" { schemaName = pmodel.NewCIStr(ctx.GetSessionVars().CurrentDB) } - d.checkTableInfo(ctx, schemaName, pmodel.NewCIStr("$mlog$"+stmt.Table.Name.O)) + d.checkTableInfo(ctx, schemaName, model.MaterializedViewLogTableName(stmt.Table.Name)) d.checkTableInfo(ctx, schemaName, stmt.Table.Name) return nil } diff --git a/pkg/ddl/schematracker/dm_tracker.go b/pkg/ddl/schematracker/dm_tracker.go index 259100c6404ad..8853b635982ee 100644 --- a/pkg/ddl/schematracker/dm_tracker.go +++ b/pkg/ddl/schematracker/dm_tracker.go @@ -253,8 +253,7 @@ func (d *SchemaTracker) CreateMaterializedViewLog(ctx sessionctx.Context, s *ast return dbterror.ErrWrongObject.GenWithStackByArgs(schemaName, s.Table.Name, "BASE TABLE") } - mlogName := "$mlog$" + baseTable.Name.O - mlogNameCIStr := pmodel.NewCIStr(mlogName) + mlogNameCIStr := model.MaterializedViewLogTableName(baseTable.Name) if utf8.RuneCountInString(mlogNameCIStr.L) > mysql.MaxTableNameLength { return dbterror.ErrTooLongIdent.GenWithStackByArgs(mlogNameCIStr) } diff --git a/pkg/executor/materialized_view.go b/pkg/executor/materialized_view.go index f140d3cf3f583..b7808cbeb9826 100644 --- a/pkg/executor/materialized_view.go +++ b/pkg/executor/materialized_view.go @@ -2267,7 +2267,7 @@ func (e *PurgeMaterializedViewLogExec) resolvePurgeMaterializedViewLogMeta( baseTableMeta = baseTable.Meta() baseTableID := baseTableMeta.ID - mlogName = pmodel.NewCIStr("$mlog$" + baseTableMeta.Name.O) + mlogName = model.MaterializedViewLogTableName(baseTableMeta.Name) mlogTable, err := is.TableByName(context.Background(), schemaName, mlogName) if err != nil { if infoschema.ErrTableNotExists.Equal(err) { diff --git a/pkg/meta/model/table.go b/pkg/meta/model/table.go index dd9666d7a3a1b..d6944b2daac84 100644 --- a/pkg/meta/model/table.go +++ b/pkg/meta/model/table.go @@ -868,6 +868,9 @@ type MaterializedViewLogInfo struct { } const ( + // MaterializedViewLogTableNamePrefix is the prefix of the physical table name for a materialized view log. + MaterializedViewLogTableNamePrefix = "$mlog$" + // MaterializedViewLogDMLTypeColumnName is the auto-added internal column on a materialized view log table, // recording row operation type (I/U/D). MaterializedViewLogDMLTypeColumnName = "_MLOG$_DML_TYPE" @@ -877,6 +880,11 @@ const ( MaterializedViewLogOldNewColumnName = "_MLOG$_OLD_NEW" ) +// MaterializedViewLogTableName returns the physical materialized view log table name for a base table. +func MaterializedViewLogTableName(baseTableName model.CIStr) model.CIStr { + return model.NewCIStr(MaterializedViewLogTableNamePrefix + baseTableName.O) +} + // Clone clones MaterializedViewLogInfo. func (i *MaterializedViewLogInfo) Clone() *MaterializedViewLogInfo { if i == nil { diff --git a/pkg/planner/core/planbuilder.go b/pkg/planner/core/planbuilder.go index 7077d9931afbf..97625edd9f7b7 100644 --- a/pkg/planner/core/planbuilder.go +++ b/pkg/planner/core/planbuilder.go @@ -6325,9 +6325,9 @@ func (b *PlanBuilder) materializedViewLogNameForBaseTable(ctx context.Context, d dbName = b.ctx.GetSessionVars().CurrentDB } if baseTable, err := b.is.TableByName(ctx, pmodel.NewCIStr(dbName), baseName); err == nil { - return pmodel.NewCIStr("$mlog$" + baseTable.Meta().Name.O) + return model.MaterializedViewLogTableName(baseTable.Meta().Name) } - return pmodel.NewCIStr("$mlog$" + baseName.O) + return model.MaterializedViewLogTableName(baseName) } const ( From 735cf14a1a2d22f98c57d2581bd39a6cadc81e25 Mon Sep 17 00:00:00 2001 From: Zhigao TONG Date: Sun, 31 May 2026 22:15:01 +0800 Subject: [PATCH 3/3] refactor: add TODOs for future support of column-level ALTER MATERIALIZED VIEW LOG actions and action-specific privilege checks Signed-off-by: Zhigao TONG --- pkg/ddl/materialized_view.go | 3 +++ pkg/parser/ast/ddl.go | 1 + pkg/planner/core/planbuilder.go | 2 ++ 3 files changed, 6 insertions(+) diff --git a/pkg/ddl/materialized_view.go b/pkg/ddl/materialized_view.go index e6532b32d772a..14e1820033d0d 100644 --- a/pkg/ddl/materialized_view.go +++ b/pkg/ddl/materialized_view.go @@ -572,6 +572,9 @@ func (e *executor) AlterMaterializedViewLog(ctx sessionctx.Context, s *ast.Alter return dbterror.ErrWrongObject.GenWithStackByArgs(schemaName.O, mlogName, "MATERIALIZED VIEW LOG") } + // TODO: split ALTER MATERIALIZED VIEW LOG into per-action handlers and + // dedicated DDL job args when schema-changing actions (for example column + // add/drop) are supported. for _, action := range s.Actions { switch action.Tp { case ast.AlterMaterializedViewLogActionPurge: diff --git a/pkg/parser/ast/ddl.go b/pkg/parser/ast/ddl.go index 64a40160c094a..6fde62007e9f4 100644 --- a/pkg/parser/ast/ddl.go +++ b/pkg/parser/ast/ddl.go @@ -2037,6 +2037,7 @@ type AlterMaterializedViewLogAction struct { type AlterMaterializedViewLogActionType int const ( + // TODO: support column-level ALTER MATERIALIZED VIEW LOG actions. AlterMaterializedViewLogActionPurge AlterMaterializedViewLogActionType = iota ) diff --git a/pkg/planner/core/planbuilder.go b/pkg/planner/core/planbuilder.go index 97625edd9f7b7..3f2f3b3156899 100644 --- a/pkg/planner/core/planbuilder.go +++ b/pkg/planner/core/planbuilder.go @@ -6308,6 +6308,8 @@ func (b *PlanBuilder) buildDDL(ctx context.Context, node ast.DDLNode) (base.Plan return nil, plannererrors.ErrNoDB } mlogName := b.materializedViewLogNameForBaseTable(ctx, dbName, v.Table.Name) + // TODO: add action-specific privilege checks for ALTER MATERIALIZED VIEW LOG + // (for example base-table SELECT for future column-level actions). if b.ctx.GetSessionVars().User != nil { authErr = plannererrors.ErrTableaccessDenied.GenWithStackByArgs("ALTER", b.ctx.GetSessionVars().User.AuthUsername, b.ctx.GetSessionVars().User.AuthHostname, mlogName.L)