Files
tidb/pkg/ddl/schematracker/checker.go
2025-05-15 13:59:08 +00:00

607 lines
18 KiB
Go

// Copyright 2022 PingCAP, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package schematracker
import (
"bytes"
"context"
"fmt"
"regexp"
"strings"
"sync/atomic"
"github.com/ngaut/pools"
"github.com/pingcap/tidb/pkg/ddl"
"github.com/pingcap/tidb/pkg/ddl/schemaver"
"github.com/pingcap/tidb/pkg/ddl/serverstate"
"github.com/pingcap/tidb/pkg/ddl/systable"
"github.com/pingcap/tidb/pkg/infoschema"
"github.com/pingcap/tidb/pkg/kv"
"github.com/pingcap/tidb/pkg/meta/autoid"
"github.com/pingcap/tidb/pkg/meta/model"
"github.com/pingcap/tidb/pkg/owner"
"github.com/pingcap/tidb/pkg/parser/ast"
"github.com/pingcap/tidb/pkg/parser/mysql"
"github.com/pingcap/tidb/pkg/sessionctx"
"github.com/pingcap/tidb/pkg/sessionctx/vardef"
"github.com/pingcap/tidb/pkg/sessionctx/variable"
"github.com/pingcap/tidb/pkg/statistics/handle"
"github.com/pingcap/tidb/pkg/store/helper"
"github.com/pingcap/tidb/pkg/store/mockstore"
)
var (
// ConstructResultOfShowCreateDatabase should be assigned to executor.ConstructResultOfShowCreateDatabase.
// It is used to break cycle import.
ConstructResultOfShowCreateDatabase func(sessionctx.Context, *model.DBInfo, bool, *bytes.Buffer) error
// ConstructResultOfShowCreateTable should be assigned to executor.ConstructResultOfShowCreateTable.
// It is used to break cycle import.
ConstructResultOfShowCreateTable func(sessionctx.Context, *model.TableInfo, autoid.Allocators, *bytes.Buffer) error
)
func init() {
mockstore.DDLCheckerInjector = NewStorageDDLInjector
}
// Checker is used to check the result of SchemaTracker is same as real DDL.
type Checker struct {
realDDL ddl.DDL
tracker SchemaTracker
closed atomic.Bool
realExecutor ddl.Executor
infoCache *infoschema.InfoCache
}
// NewChecker creates a Checker.
func NewChecker(realDDL ddl.DDL, realExecutor ddl.Executor, infoCache *infoschema.InfoCache) *Checker {
return &Checker{
realDDL: realDDL,
realExecutor: realExecutor,
infoCache: infoCache,
tracker: NewSchemaTracker(2),
}
}
// Disable turns off check.
func (d *Checker) Disable() {
d.closed.Store(true)
}
// Enable turns on check.
func (d *Checker) Enable() {
d.closed.Store(false)
}
// CreateTestDB creates a `test` database like the default behaviour of TiDB.
func (d *Checker) CreateTestDB(ctx sessionctx.Context) {
d.tracker.CreateTestDB(ctx)
}
func (d *Checker) checkDBInfo(ctx sessionctx.Context, dbName ast.CIStr) {
if d.closed.Load() {
return
}
dbInfo, _ := d.infoCache.GetLatest().SchemaByName(dbName)
dbInfo2 := d.tracker.SchemaByName(dbName)
if dbInfo == nil || dbInfo2 == nil {
if dbInfo == nil && dbInfo2 == nil {
return
}
errStr := fmt.Sprintf("inconsistent dbInfo, dbName: %s, real ddl: %p, schematracker:%p", dbName, dbInfo, dbInfo2)
panic(errStr)
}
result := bytes.NewBuffer(make([]byte, 0, 512))
err := ConstructResultOfShowCreateDatabase(ctx, dbInfo, false, result)
if err != nil {
panic(err)
}
result2 := bytes.NewBuffer(make([]byte, 0, 512))
err = ConstructResultOfShowCreateDatabase(ctx, dbInfo2, false, result2)
if err != nil {
panic(err)
}
s1 := result.String()
s2 := result2.String()
if s1 != s2 {
errStr := fmt.Sprintf("%s != %s", s1, s2)
panic(errStr)
}
}
func (d *Checker) checkTableInfo(ctx sessionctx.Context, dbName, tableName ast.CIStr) {
if d.closed.Load() {
return
}
if dbName.L == mysql.SystemDB {
// no need to check system tables.
return
}
tableInfo, _ := d.infoCache.GetLatest().TableByName(context.Background(), dbName, tableName)
tableInfo2, _ := d.tracker.TableByName(context.Background(), dbName, tableName)
if tableInfo == nil || tableInfo2 == nil {
if tableInfo == nil && tableInfo2 == nil {
return
}
errStr := fmt.Sprintf("inconsistent tableInfo, dbName: %s, tableName: %s, real ddl: %p, schematracker:%p",
dbName, tableName, tableInfo, tableInfo2)
panic(errStr)
}
result := bytes.NewBuffer(make([]byte, 0, 512))
err := ConstructResultOfShowCreateTable(ctx, tableInfo.Meta(), autoid.Allocators{}, result)
if err != nil {
panic(err)
}
result2 := bytes.NewBuffer(make([]byte, 0, 512))
err = ConstructResultOfShowCreateTable(ctx, tableInfo2, autoid.Allocators{}, result2)
if err != nil {
panic(err)
}
// SchemaTracker will always use NONCLUSTERED so it can support more types of DDL.
removeClusteredIndexComment := func(s string) string {
ret := strings.ReplaceAll(s, " /*T![clustered_index] NONCLUSTERED */", "")
ret = strings.ReplaceAll(ret, " /*T![clustered_index] CLUSTERED */", "")
return ret
}
s1 := removeClusteredIndexComment(result.String())
s2 := removeClusteredIndexComment(result2.String())
// Remove shard_row_id_bits and pre_split_regions comments.
if ctx.GetSessionVars().ShardRowIDBits != 0 || ctx.GetSessionVars().PreSplitRegions != 0 {
removeShardPreSplitComment := func(s string) string {
pattern := ` \/\*T! SHARD_ROW_ID_BITS=.*?\*\/`
re := regexp.MustCompile(pattern)
ret := re.ReplaceAllString(s, "")
pattern = ` \/\*T! PRE_SPLIT_REGIONS=.*?\*\/`
re = regexp.MustCompile(pattern)
ret = re.ReplaceAllString(ret, "")
return ret
}
s1 = removeShardPreSplitComment(s1)
s2 = removeShardPreSplitComment(s2)
}
if s1 != s2 {
errStr := fmt.Sprintf("%s\n!=\n%s", s1, s2)
panic(errStr)
}
}
// CreateSchema implements the DDL interface.
func (d *Checker) CreateSchema(ctx sessionctx.Context, stmt *ast.CreateDatabaseStmt) error {
err := d.realExecutor.CreateSchema(ctx, stmt)
if err != nil {
return err
}
err = d.tracker.CreateSchema(ctx, stmt)
if err != nil {
panic(err)
}
d.checkDBInfo(ctx, stmt.Name)
return nil
}
// AlterSchema implements the DDL interface.
func (d *Checker) AlterSchema(sctx sessionctx.Context, stmt *ast.AlterDatabaseStmt) error {
err := d.realExecutor.AlterSchema(sctx, stmt)
if err != nil {
return err
}
err = d.tracker.AlterSchema(sctx, stmt)
if err != nil {
panic(err)
}
d.checkDBInfo(sctx, stmt.Name)
return nil
}
// DropSchema implements the DDL interface.
func (d *Checker) DropSchema(ctx sessionctx.Context, stmt *ast.DropDatabaseStmt) error {
err := d.realExecutor.DropSchema(ctx, stmt)
if err != nil {
return err
}
err = d.tracker.DropSchema(ctx, stmt)
if err != nil {
panic(err)
}
d.checkDBInfo(ctx, stmt.Name)
return nil
}
// RecoverSchema implements the DDL interface.
func (*Checker) RecoverSchema(_ sessionctx.Context, _ *model.RecoverSchemaInfo) (err error) {
return nil
}
// CreateTable implements the DDL interface.
func (d *Checker) CreateTable(ctx sessionctx.Context, stmt *ast.CreateTableStmt) error {
err := d.realExecutor.CreateTable(ctx, stmt)
if err != nil || d.closed.Load() {
return err
}
// some unit test will also check warnings, we reset the warnings after SchemaTracker use session context again.
count := ctx.GetSessionVars().StmtCtx.WarningCount()
// backup old session variables because CreateTable will change them.
enableClusteredIndex := ctx.GetSessionVars().EnableClusteredIndex
err = d.tracker.CreateTable(ctx, stmt)
if err != nil {
panic(err)
}
ctx.GetSessionVars().EnableClusteredIndex = enableClusteredIndex
ctx.GetSessionVars().StmtCtx.TruncateWarnings(int(count))
d.checkTableInfo(ctx, stmt.Table.Schema, stmt.Table.Name)
return nil
}
// CreateView implements the DDL interface.
func (d *Checker) CreateView(ctx sessionctx.Context, stmt *ast.CreateViewStmt) error {
err := d.realExecutor.CreateView(ctx, stmt)
if err != nil {
return err
}
err = d.tracker.CreateView(ctx, stmt)
if err != nil {
panic(err)
}
d.checkTableInfo(ctx, stmt.ViewName.Schema, stmt.ViewName.Name)
return nil
}
// DropTable implements the DDL interface.
func (d *Checker) DropTable(ctx sessionctx.Context, stmt *ast.DropTableStmt) (err error) {
err = d.realExecutor.DropTable(ctx, stmt)
_ = d.tracker.DropTable(ctx, stmt)
for _, tableName := range stmt.Tables {
d.checkTableInfo(ctx, tableName.Schema, tableName.Name)
}
return err
}
// RecoverTable implements the DDL interface.
func (*Checker) RecoverTable(_ sessionctx.Context, _ *model.RecoverTableInfo) (err error) {
//TODO implement me
panic("implement me")
}
// FlashbackCluster implements the DDL interface.
func (*Checker) FlashbackCluster(_ sessionctx.Context, _ uint64) (err error) {
//TODO implement me
panic("implement me")
}
// DropView implements the DDL interface.
func (d *Checker) DropView(ctx sessionctx.Context, stmt *ast.DropTableStmt) (err error) {
err = d.realExecutor.DropView(ctx, stmt)
if err != nil {
return err
}
err = d.tracker.DropView(ctx, stmt)
if err != nil {
panic(err)
}
for _, tableName := range stmt.Tables {
d.checkTableInfo(ctx, tableName.Schema, tableName.Name)
}
return nil
}
// CreateIndex implements the DDL interface.
func (d *Checker) CreateIndex(ctx sessionctx.Context, stmt *ast.CreateIndexStmt) error {
err := d.realExecutor.CreateIndex(ctx, stmt)
if err != nil {
return err
}
err = d.tracker.CreateIndex(ctx, stmt)
if err != nil {
panic(err)
}
d.checkTableInfo(ctx, stmt.Table.Schema, stmt.Table.Name)
return nil
}
// DropIndex implements the DDL interface.
func (d *Checker) DropIndex(ctx sessionctx.Context, stmt *ast.DropIndexStmt) error {
err := d.realExecutor.DropIndex(ctx, stmt)
if err != nil {
return err
}
err = d.tracker.DropIndex(ctx, stmt)
if err != nil {
panic(err)
}
d.checkTableInfo(ctx, stmt.Table.Schema, stmt.Table.Name)
return nil
}
// AlterTable implements the DDL interface.
func (d *Checker) AlterTable(ctx context.Context, sctx sessionctx.Context, stmt *ast.AlterTableStmt) error {
err := d.realExecutor.AlterTable(ctx, sctx, stmt)
if err != nil || d.closed.Load() {
return err
}
// some unit test will also check warnings, we reset the warnings after SchemaTracker use session context again.
count := sctx.GetSessionVars().StmtCtx.WarningCount()
err = d.tracker.AlterTable(ctx, sctx, stmt)
if err != nil {
panic(err)
}
sctx.GetSessionVars().StmtCtx.TruncateWarnings(int(count))
d.checkTableInfo(sctx, stmt.Table.Schema, stmt.Table.Name)
return nil
}
// TruncateTable implements the DDL interface.
func (*Checker) TruncateTable(_ sessionctx.Context, _ ast.Ident) error {
//TODO implement me
panic("implement me")
}
// RenameTable implements the DDL interface.
func (d *Checker) RenameTable(ctx sessionctx.Context, stmt *ast.RenameTableStmt) error {
err := d.realExecutor.RenameTable(ctx, stmt)
if err != nil {
return err
}
err = d.tracker.RenameTable(ctx, stmt)
if err != nil {
panic(err)
}
for _, tableName := range stmt.TableToTables {
d.checkTableInfo(ctx, tableName.OldTable.Schema, tableName.OldTable.Name)
d.checkTableInfo(ctx, tableName.NewTable.Schema, tableName.NewTable.Name)
}
return nil
}
// LockTables implements the DDL interface.
func (d *Checker) LockTables(ctx sessionctx.Context, stmt *ast.LockTablesStmt) error {
return d.realExecutor.LockTables(ctx, stmt)
}
// UnlockTables implements the DDL interface.
func (d *Checker) UnlockTables(ctx sessionctx.Context, lockedTables []model.TableLockTpInfo) error {
return d.realExecutor.UnlockTables(ctx, lockedTables)
}
// AlterTableMode implements the DDL interface.
func (d *Checker) AlterTableMode(ctx sessionctx.Context, args *model.AlterTableModeArgs) error {
return d.realExecutor.AlterTableMode(ctx, args)
}
// RefreshMeta implements the DDL interface.
func (d *Checker) RefreshMeta(ctx sessionctx.Context, args *model.RefreshMetaArgs) error {
return d.realExecutor.RefreshMeta(ctx, args)
}
// CleanupTableLock implements the DDL interface.
func (d *Checker) CleanupTableLock(ctx sessionctx.Context, tables []*ast.TableName) error {
return d.realExecutor.CleanupTableLock(ctx, tables)
}
// UpdateTableReplicaInfo implements the DDL interface.
func (*Checker) UpdateTableReplicaInfo(_ sessionctx.Context, _ int64, _ bool) error {
//TODO implement me
panic("implement me")
}
// RepairTable implements the DDL interface.
func (*Checker) RepairTable(_ sessionctx.Context, _ *ast.CreateTableStmt) error {
//TODO implement me
panic("implement me")
}
// CreateSequence implements the DDL interface.
func (*Checker) CreateSequence(_ sessionctx.Context, _ *ast.CreateSequenceStmt) error {
//TODO implement me
panic("implement me")
}
// DropSequence implements the DDL interface.
func (*Checker) DropSequence(_ sessionctx.Context, _ *ast.DropSequenceStmt) (err error) {
//TODO implement me
panic("implement me")
}
// AlterSequence implements the DDL interface.
func (*Checker) AlterSequence(_ sessionctx.Context, _ *ast.AlterSequenceStmt) error {
//TODO implement me
panic("implement me")
}
// CreatePlacementPolicy implements the DDL interface.
func (*Checker) CreatePlacementPolicy(_ sessionctx.Context, _ *ast.CreatePlacementPolicyStmt) error {
//TODO implement me
panic("implement me")
}
// DropPlacementPolicy implements the DDL interface.
func (*Checker) DropPlacementPolicy(_ sessionctx.Context, _ *ast.DropPlacementPolicyStmt) error {
//TODO implement me
panic("implement me")
}
// AlterPlacementPolicy implements the DDL interface.
func (*Checker) AlterPlacementPolicy(_ sessionctx.Context, _ *ast.AlterPlacementPolicyStmt) error {
//TODO implement me
panic("implement me")
}
// AddResourceGroup implements the DDL interface.
// ResourceGroup do not affect the transaction.
func (*Checker) AddResourceGroup(_ sessionctx.Context, _ *ast.CreateResourceGroupStmt) error {
return nil
}
// DropResourceGroup implements the DDL interface.
func (*Checker) DropResourceGroup(_ sessionctx.Context, _ *ast.DropResourceGroupStmt) error {
return nil
}
// AlterResourceGroup implements the DDL interface.
func (*Checker) AlterResourceGroup(_ sessionctx.Context, _ *ast.AlterResourceGroupStmt) error {
return nil
}
// CreateSchemaWithInfo implements the DDL interface.
func (d *Checker) CreateSchemaWithInfo(ctx sessionctx.Context, info *model.DBInfo, onExist ddl.OnExist) error {
err := d.realExecutor.CreateSchemaWithInfo(ctx, info, onExist)
if err != nil {
return err
}
err = d.tracker.CreateSchemaWithInfo(ctx, info, onExist)
if err != nil {
panic(err)
}
d.checkDBInfo(ctx, info.Name)
return nil
}
// CreateTableWithInfo implements the DDL interface.
func (*Checker) CreateTableWithInfo(_ sessionctx.Context, _ ast.CIStr, _ *model.TableInfo, _ []model.InvolvingSchemaInfo, _ ...ddl.CreateTableOption) error {
//TODO implement me
panic("implement me")
}
// BatchCreateTableWithInfo implements the DDL interface.
func (*Checker) BatchCreateTableWithInfo(_ sessionctx.Context, _ ast.CIStr, _ []*model.TableInfo, _ ...ddl.CreateTableOption) error {
//TODO implement me
panic("implement me")
}
// CreatePlacementPolicyWithInfo implements the DDL interface.
func (*Checker) CreatePlacementPolicyWithInfo(_ sessionctx.Context, _ *model.PolicyInfo, _ ddl.OnExist) error {
//TODO implement me
panic("implement me")
}
// Start implements the DDL interface.
func (d *Checker) Start(startMode ddl.StartMode, ctxPool *pools.ResourcePool) error {
return d.realDDL.Start(startMode, ctxPool)
}
// Stats implements the DDL interface.
func (d *Checker) Stats(vars *variable.SessionVars) (map[string]any, error) {
return d.realDDL.Stats(vars)
}
// GetScope implements the DDL interface.
func (d *Checker) GetScope(status string) vardef.ScopeFlag {
return d.realDDL.GetScope(status)
}
// Stop implements the DDL interface.
func (d *Checker) Stop() error {
return d.realDDL.Stop()
}
// RegisterStatsHandle implements the DDL interface.
func (d *Checker) RegisterStatsHandle(h *handle.Handle) {
d.realDDL.RegisterStatsHandle(h)
}
// SchemaSyncer implements the DDL interface.
func (d *Checker) SchemaSyncer() schemaver.Syncer {
return d.realDDL.SchemaSyncer()
}
// StateSyncer implements the DDL interface.
func (d *Checker) StateSyncer() serverstate.Syncer {
return d.realDDL.StateSyncer()
}
// OwnerManager implements the DDL interface.
func (d *Checker) OwnerManager() owner.Manager {
return d.realDDL.OwnerManager()
}
// GetID implements the DDL interface.
func (d *Checker) GetID() string {
return d.realDDL.GetID()
}
// DoDDLJob implements the DDL interface.
func (d *Checker) DoDDLJob(ctx sessionctx.Context, job *model.Job) error {
de := d.realExecutor.(ddl.ExecutorForTest)
return de.DoDDLJob(ctx, job)
}
// GetMinJobIDRefresher implements the DDL interface.
func (d *Checker) GetMinJobIDRefresher() *systable.MinJobIDRefresher {
return d.realDDL.GetMinJobIDRefresher()
}
// DoDDLJobWrapper implements the DDL interface.
func (d *Checker) DoDDLJobWrapper(ctx sessionctx.Context, jobW *ddl.JobWrapper) error {
de := d.realExecutor.(ddl.ExecutorForTest)
return de.DoDDLJobWrapper(ctx, jobW)
}
type storageAndMore interface {
kv.Storage
kv.StorageWithPD
kv.EtcdBackend
helper.Storage
}
// StorageDDLInjector wraps kv.Storage to inject checker to domain's DDL in bootstrap time.
type StorageDDLInjector struct {
storageAndMore
Injector func(ddl.DDL, ddl.Executor, *infoschema.InfoCache) *Checker
}
// NewStorageDDLInjector creates a new StorageDDLInjector to inject Checker.
func NewStorageDDLInjector(s kv.Storage) kv.Storage {
raw := s.(storageAndMore)
ret := StorageDDLInjector{
storageAndMore: raw,
Injector: NewChecker,
}
return ret
}
// UnwrapStorage unwraps StorageDDLInjector for one level.
func UnwrapStorage(s kv.Storage) kv.Storage {
injector, ok := s.(StorageDDLInjector)
if !ok {
return s
}
return injector.storageAndMore
}