Files
tidb/pkg/infoschema/builder.go
2024-03-29 03:45:49 +00:00

980 lines
32 KiB
Go

// Copyright 2016 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 infoschema
import (
"cmp"
"context"
"fmt"
"slices"
"strings"
"github.com/ngaut/pools"
"github.com/pingcap/errors"
"github.com/pingcap/failpoint"
"github.com/pingcap/tidb/pkg/config"
"github.com/pingcap/tidb/pkg/kv"
"github.com/pingcap/tidb/pkg/meta"
"github.com/pingcap/tidb/pkg/meta/autoid"
"github.com/pingcap/tidb/pkg/parser/charset"
"github.com/pingcap/tidb/pkg/parser/model"
"github.com/pingcap/tidb/pkg/sessionctx"
"github.com/pingcap/tidb/pkg/sessionctx/variable"
"github.com/pingcap/tidb/pkg/table"
"github.com/pingcap/tidb/pkg/table/tables"
"github.com/pingcap/tidb/pkg/util/domainutil"
"github.com/pingcap/tidb/pkg/util/intest"
)
// Builder builds a new InfoSchema.
type Builder struct {
enableV2 bool
infoschemaV2
// dbInfos do not need to be copied everytime applying a diff, instead,
// they can be copied only once over the whole lifespan of Builder.
// This map will indicate which DB has been copied, so that they
// don't need to be copied again.
dirtyDB map[string]bool
// Used by autoid allocators
autoid.Requirement
factory func() (pools.Resource, error)
bundleInfoBuilder
infoData *Data
}
// ApplyDiff applies SchemaDiff to the new InfoSchema.
// Return the detail updated table IDs that are produced from SchemaDiff and an error.
func (b *Builder) ApplyDiff(m *meta.Meta, diff *model.SchemaDiff) ([]int64, error) {
b.schemaMetaVersion = diff.Version
switch diff.Type {
case model.ActionCreateSchema:
return nil, applyCreateSchema(b, m, diff)
case model.ActionDropSchema:
return applyDropSchema(b, diff), nil
case model.ActionRecoverSchema:
return applyRecoverSchema(b, m, diff)
case model.ActionModifySchemaCharsetAndCollate:
return nil, applyModifySchemaCharsetAndCollate(b, m, diff)
case model.ActionModifySchemaDefaultPlacement:
return nil, applyModifySchemaDefaultPlacement(b, m, diff)
case model.ActionCreatePlacementPolicy:
return nil, applyCreatePolicy(b, m, diff)
case model.ActionDropPlacementPolicy:
return applyDropPolicy(b, diff.SchemaID), nil
case model.ActionAlterPlacementPolicy:
return applyAlterPolicy(b, m, diff)
case model.ActionCreateResourceGroup:
return nil, applyCreateOrAlterResourceGroup(b, m, diff)
case model.ActionAlterResourceGroup:
return nil, applyCreateOrAlterResourceGroup(b, m, diff)
case model.ActionDropResourceGroup:
return applyDropResourceGroup(b, m, diff), nil
case model.ActionTruncateTablePartition, model.ActionTruncateTable:
return applyTruncateTableOrPartition(b, m, diff)
case model.ActionDropTable, model.ActionDropTablePartition:
return applyDropTableOrPartition(b, m, diff)
case model.ActionRecoverTable:
return applyRecoverTable(b, m, diff)
case model.ActionCreateTables:
return applyCreateTables(b, m, diff)
case model.ActionReorganizePartition, model.ActionRemovePartitioning,
model.ActionAlterTablePartitioning:
return applyReorganizePartition(b, m, diff)
case model.ActionExchangeTablePartition:
return applyExchangeTablePartition(b, m, diff)
case model.ActionFlashbackCluster:
return []int64{-1}, nil
default:
return applyDefaultAction(b, m, diff)
}
}
func (b *Builder) applyCreateTables(m *meta.Meta, diff *model.SchemaDiff) ([]int64, error) {
return b.applyAffectedOpts(m, make([]int64, 0, len(diff.AffectedOpts)), diff, model.ActionCreateTable)
}
func applyTruncateTableOrPartition(b *Builder, m *meta.Meta, diff *model.SchemaDiff) ([]int64, error) {
tblIDs, err := applyTableUpdate(b, m, diff)
if err != nil {
return nil, errors.Trace(err)
}
// bundle ops
if diff.Type == model.ActionTruncateTable {
b.deleteBundle(b.infoSchema, diff.OldTableID)
b.markTableBundleShouldUpdate(diff.TableID)
}
for _, opt := range diff.AffectedOpts {
if diff.Type == model.ActionTruncateTablePartition {
// Reduce the impact on DML when executing partition DDL. eg.
// While session 1 performs the DML operation associated with partition 1,
// the TRUNCATE operation of session 2 on partition 2 does not cause the operation of session 1 to fail.
tblIDs = append(tblIDs, opt.OldTableID)
b.markPartitionBundleShouldUpdate(opt.TableID)
}
b.deleteBundle(b.infoSchema, opt.OldTableID)
}
return tblIDs, nil
}
func applyDropTableOrPartition(b *Builder, m *meta.Meta, diff *model.SchemaDiff) ([]int64, error) {
tblIDs, err := applyTableUpdate(b, m, diff)
if err != nil {
return nil, errors.Trace(err)
}
// bundle ops
b.markTableBundleShouldUpdate(diff.TableID)
for _, opt := range diff.AffectedOpts {
b.deleteBundle(b.infoSchema, opt.OldTableID)
}
return tblIDs, nil
}
func applyReorganizePartition(b *Builder, m *meta.Meta, diff *model.SchemaDiff) ([]int64, error) {
tblIDs, err := applyTableUpdate(b, m, diff)
if err != nil {
return nil, errors.Trace(err)
}
// bundle ops
for _, opt := range diff.AffectedOpts {
if opt.OldTableID != 0 {
b.deleteBundle(b.infoSchema, opt.OldTableID)
}
if opt.TableID != 0 {
b.markTableBundleShouldUpdate(opt.TableID)
}
// TODO: Should we also check markPartitionBundleShouldUpdate?!?
}
return tblIDs, nil
}
func applyExchangeTablePartition(b *Builder, m *meta.Meta, diff *model.SchemaDiff) ([]int64, error) {
// It is not in StatePublic.
if diff.OldTableID == diff.TableID && diff.OldSchemaID == diff.SchemaID {
ntIDs, err := applyTableUpdate(b, m, diff)
if err != nil {
return nil, errors.Trace(err)
}
if diff.AffectedOpts == nil || diff.AffectedOpts[0].OldSchemaID == 0 {
return ntIDs, err
}
// Reload parition tabe.
ptSchemaID := diff.AffectedOpts[0].OldSchemaID
ptID := diff.AffectedOpts[0].TableID
ptDiff := &model.SchemaDiff{
Type: diff.Type,
Version: diff.Version,
TableID: ptID,
SchemaID: ptSchemaID,
OldTableID: ptID,
OldSchemaID: ptSchemaID,
}
ptIDs, err := applyTableUpdate(b, m, ptDiff)
if err != nil {
return nil, errors.Trace(err)
}
return append(ptIDs, ntIDs...), nil
}
ntSchemaID := diff.OldSchemaID
ntID := diff.OldTableID
ptSchemaID := diff.SchemaID
ptID := diff.TableID
partID := diff.TableID
if len(diff.AffectedOpts) > 0 {
ptID = diff.AffectedOpts[0].TableID
if diff.AffectedOpts[0].SchemaID != 0 {
ptSchemaID = diff.AffectedOpts[0].SchemaID
}
}
// The normal table needs to be updated first:
// Just update the tables separately
currDiff := &model.SchemaDiff{
// This is only for the case since https://github.com/pingcap/tidb/pull/45877
// Fixed now, by adding back the AffectedOpts
// to carry the partitioned Table ID.
Type: diff.Type,
Version: diff.Version,
TableID: ntID,
SchemaID: ntSchemaID,
}
if ptID != partID {
currDiff.TableID = partID
currDiff.OldTableID = ntID
currDiff.OldSchemaID = ntSchemaID
}
ntIDs, err := applyTableUpdate(b, m, currDiff)
if err != nil {
return nil, errors.Trace(err)
}
// partID is the new id for the non-partitioned table!
b.markTableBundleShouldUpdate(partID)
// Then the partitioned table, will re-read the whole table, including all partitions!
currDiff.TableID = ptID
currDiff.SchemaID = ptSchemaID
currDiff.OldTableID = ptID
currDiff.OldSchemaID = ptSchemaID
ptIDs, err := applyTableUpdate(b, m, currDiff)
if err != nil {
return nil, errors.Trace(err)
}
// ntID is the new id for the partition!
b.markPartitionBundleShouldUpdate(ntID)
err = updateAutoIDForExchangePartition(b.Requirement.Store(), ptSchemaID, ptID, ntSchemaID, ntID)
if err != nil {
return nil, errors.Trace(err)
}
return append(ptIDs, ntIDs...), nil
}
func applyRecoverTable(b *Builder, m *meta.Meta, diff *model.SchemaDiff) ([]int64, error) {
tblIDs, err := applyTableUpdate(b, m, diff)
if err != nil {
return nil, errors.Trace(err)
}
// bundle ops
for _, opt := range diff.AffectedOpts {
b.markTableBundleShouldUpdate(opt.TableID)
}
return tblIDs, nil
}
func updateAutoIDForExchangePartition(store kv.Storage, ptSchemaID, ptID, ntSchemaID, ntID int64) error {
err := kv.RunInNewTxn(kv.WithInternalSourceType(context.Background(), kv.InternalTxnDDL), store, true, func(ctx context.Context, txn kv.Transaction) error {
t := meta.NewMeta(txn)
ptAutoIDs, err := t.GetAutoIDAccessors(ptSchemaID, ptID).Get()
if err != nil {
return err
}
// non-partition table auto IDs.
ntAutoIDs, err := t.GetAutoIDAccessors(ntSchemaID, ntID).Get()
if err != nil {
return err
}
// Set both tables to the maximum auto IDs between normal table and partitioned table.
newAutoIDs := meta.AutoIDGroup{
RowID: max(ptAutoIDs.RowID, ntAutoIDs.RowID),
IncrementID: max(ptAutoIDs.IncrementID, ntAutoIDs.IncrementID),
RandomID: max(ptAutoIDs.RandomID, ntAutoIDs.RandomID),
}
err = t.GetAutoIDAccessors(ptSchemaID, ptID).Put(newAutoIDs)
if err != nil {
return err
}
err = t.GetAutoIDAccessors(ntSchemaID, ntID).Put(newAutoIDs)
if err != nil {
return err
}
return nil
})
return err
}
func (b *Builder) applyAffectedOpts(m *meta.Meta, tblIDs []int64, diff *model.SchemaDiff, tp model.ActionType) ([]int64, error) {
if diff.AffectedOpts != nil {
for _, opt := range diff.AffectedOpts {
affectedDiff := &model.SchemaDiff{
Version: diff.Version,
Type: tp,
SchemaID: opt.SchemaID,
TableID: opt.TableID,
OldSchemaID: opt.OldSchemaID,
OldTableID: opt.OldTableID,
}
affectedIDs, err := b.ApplyDiff(m, affectedDiff)
if err != nil {
return nil, errors.Trace(err)
}
tblIDs = append(tblIDs, affectedIDs...)
}
}
return tblIDs, nil
}
func applyDefaultAction(b *Builder, m *meta.Meta, diff *model.SchemaDiff) ([]int64, error) {
tblIDs, err := applyTableUpdate(b, m, diff)
if err != nil {
return nil, errors.Trace(err)
}
return b.applyAffectedOpts(m, tblIDs, diff, diff.Type)
}
func (b *Builder) getTableIDs(diff *model.SchemaDiff) (oldTableID, newTableID int64) {
switch diff.Type {
case model.ActionCreateSequence, model.ActionRecoverTable:
newTableID = diff.TableID
case model.ActionCreateTable:
// WARN: when support create table with foreign key in https://github.com/pingcap/tidb/pull/37148,
// create table with foreign key requires a multi-step state change(none -> write-only -> public),
// when the table's state changes from write-only to public, infoSchema need to drop the old table
// which state is write-only, otherwise, infoSchema.sortedTablesBuckets will contain 2 table both
// have the same ID, but one state is write-only, another table's state is public, it's unexpected.
//
// WARN: this change will break the compatibility if execute create table with foreign key DDL when upgrading TiDB,
// since old-version TiDB doesn't know to delete the old table.
// Since the cluster-index feature also has similar problem, we chose to prevent DDL execution during the upgrade process to avoid this issue.
oldTableID = diff.OldTableID
newTableID = diff.TableID
case model.ActionDropTable, model.ActionDropView, model.ActionDropSequence:
oldTableID = diff.TableID
case model.ActionTruncateTable, model.ActionCreateView,
model.ActionExchangeTablePartition, model.ActionAlterTablePartitioning,
model.ActionRemovePartitioning:
oldTableID = diff.OldTableID
newTableID = diff.TableID
default:
oldTableID = diff.TableID
newTableID = diff.TableID
}
return
}
func (b *Builder) updateBundleForTableUpdate(diff *model.SchemaDiff, newTableID, oldTableID int64) {
// handle placement rule cache
switch diff.Type {
case model.ActionCreateTable:
b.markTableBundleShouldUpdate(newTableID)
case model.ActionDropTable:
b.deleteBundle(b.infoSchema, oldTableID)
case model.ActionTruncateTable:
b.deleteBundle(b.infoSchema, oldTableID)
b.markTableBundleShouldUpdate(newTableID)
case model.ActionRecoverTable:
b.markTableBundleShouldUpdate(newTableID)
case model.ActionAlterTablePlacement:
b.markTableBundleShouldUpdate(newTableID)
}
}
func dropTableForUpdate(b *Builder, newTableID, oldTableID int64, dbInfo *model.DBInfo, diff *model.SchemaDiff) ([]int64, autoid.Allocators, error) {
tblIDs := make([]int64, 0, 2)
var newAllocs autoid.Allocators
// We try to reuse the old allocator, so the cached auto ID can be reused.
if tableIDIsValid(oldTableID) {
if oldTableID == newTableID &&
// For rename table, keep the old alloc.
// For repairing table in TiDB cluster, given 2 normal node and 1 repair node.
// For normal node's information schema, repaired table is existed.
// For repair node's information schema, repaired table is filtered (couldn't find it in `is`).
// So here skip to reserve the allocators when repairing table.
diff.Type != model.ActionRepairTable &&
// Alter sequence will change the sequence info in the allocator, so the old allocator is not valid any more.
diff.Type != model.ActionAlterSequence {
// TODO: Check how this would work with ADD/REMOVE Partitioning,
// which may have AutoID not connected to tableID
// TODO: can there be _tidb_rowid AutoID per partition?
oldAllocs, _ := allocByID(b, oldTableID)
newAllocs = filterAllocators(diff, oldAllocs)
}
tmpIDs := tblIDs
if (diff.Type == model.ActionRenameTable || diff.Type == model.ActionRenameTables) && diff.OldSchemaID != diff.SchemaID {
oldDBInfo, ok := oldSchemaInfo(b, diff)
if !ok {
return nil, newAllocs, ErrDatabaseNotExists.GenWithStackByArgs(
fmt.Sprintf("(Schema ID %d)", diff.OldSchemaID),
)
}
tmpIDs = applyDropTable(b, diff, oldDBInfo, oldTableID, tmpIDs)
} else {
tmpIDs = applyDropTable(b, diff, dbInfo, oldTableID, tmpIDs)
}
if oldTableID != newTableID {
// Update tblIDs only when oldTableID != newTableID because applyCreateTable() also updates tblIDs.
tblIDs = tmpIDs
}
}
return tblIDs, newAllocs, nil
}
func (b *Builder) applyTableUpdate(m *meta.Meta, diff *model.SchemaDiff) ([]int64, error) {
roDBInfo, ok := b.infoSchema.SchemaByID(diff.SchemaID)
if !ok {
return nil, ErrDatabaseNotExists.GenWithStackByArgs(
fmt.Sprintf("(Schema ID %d)", diff.SchemaID),
)
}
dbInfo := b.getSchemaAndCopyIfNecessary(roDBInfo.Name.L)
oldTableID, newTableID := b.getTableIDs(diff)
b.updateBundleForTableUpdate(diff, newTableID, oldTableID)
b.copySortedTables(oldTableID, newTableID)
tblIDs, allocs, err := dropTableForUpdate(b, newTableID, oldTableID, dbInfo, diff)
if err != nil {
return nil, err
}
if tableIDIsValid(newTableID) {
// All types except DropTableOrView.
var err error
tblIDs, err = applyCreateTable(b, m, dbInfo, newTableID, allocs, diff.Type, tblIDs, diff.Version)
if err != nil {
return nil, errors.Trace(err)
}
}
return tblIDs, nil
}
func filterAllocators(diff *model.SchemaDiff, oldAllocs autoid.Allocators) autoid.Allocators {
var newAllocs autoid.Allocators
switch diff.Type {
case model.ActionRebaseAutoID, model.ActionModifyTableAutoIdCache:
// Only drop auto-increment allocator.
newAllocs = oldAllocs.Filter(func(a autoid.Allocator) bool {
tp := a.GetType()
return tp != autoid.RowIDAllocType && tp != autoid.AutoIncrementType
})
case model.ActionRebaseAutoRandomBase:
// Only drop auto-random allocator.
newAllocs = oldAllocs.Filter(func(a autoid.Allocator) bool {
tp := a.GetType()
return tp != autoid.AutoRandomType
})
default:
// Keep all allocators.
newAllocs = oldAllocs
}
return newAllocs
}
func appendAffectedIDs(affected []int64, tblInfo *model.TableInfo) []int64 {
affected = append(affected, tblInfo.ID)
if pi := tblInfo.GetPartitionInfo(); pi != nil {
for _, def := range pi.Definitions {
affected = append(affected, def.ID)
}
}
return affected
}
func (b *Builder) applyCreateSchema(m *meta.Meta, diff *model.SchemaDiff) error {
di, err := m.GetDatabase(diff.SchemaID)
if err != nil {
return errors.Trace(err)
}
if di == nil {
// When we apply an old schema diff, the database may has been dropped already, so we need to fall back to
// full load.
return ErrDatabaseNotExists.GenWithStackByArgs(
fmt.Sprintf("(Schema ID %d)", diff.SchemaID),
)
}
b.addDB(diff.Version, di, &schemaTables{dbInfo: di, tables: make(map[string]table.Table)})
return nil
}
func (b *Builder) applyModifySchemaCharsetAndCollate(m *meta.Meta, diff *model.SchemaDiff) error {
di, err := m.GetDatabase(diff.SchemaID)
if err != nil {
return errors.Trace(err)
}
if di == nil {
// This should never happen.
return ErrDatabaseNotExists.GenWithStackByArgs(
fmt.Sprintf("(Schema ID %d)", diff.SchemaID),
)
}
newDbInfo := b.getSchemaAndCopyIfNecessary(di.Name.L)
newDbInfo.Charset = di.Charset
newDbInfo.Collate = di.Collate
return nil
}
func (b *Builder) applyModifySchemaDefaultPlacement(m *meta.Meta, diff *model.SchemaDiff) error {
di, err := m.GetDatabase(diff.SchemaID)
if err != nil {
return errors.Trace(err)
}
if di == nil {
// This should never happen.
return ErrDatabaseNotExists.GenWithStackByArgs(
fmt.Sprintf("(Schema ID %d)", diff.SchemaID),
)
}
newDbInfo := b.getSchemaAndCopyIfNecessary(di.Name.L)
newDbInfo.PlacementPolicyRef = di.PlacementPolicyRef
return nil
}
func (b *Builder) applyDropSchema(diff *model.SchemaDiff) []int64 {
di, ok := b.infoSchema.SchemaByID(diff.SchemaID)
if !ok {
return nil
}
delete(b.infoSchema.schemaMap, di.Name.L)
// Copy the sortedTables that contain the table we are going to drop.
tableIDs := make([]int64, 0, len(di.Tables))
bucketIdxMap := make(map[int]struct{}, len(di.Tables))
for _, tbl := range di.Tables {
bucketIdxMap[tableBucketIdx(tbl.ID)] = struct{}{}
// TODO: If the table ID doesn't exist.
tableIDs = appendAffectedIDs(tableIDs, tbl)
}
for bucketIdx := range bucketIdxMap {
b.copySortedTablesBucket(bucketIdx)
}
di = di.Clone()
for _, id := range tableIDs {
b.deleteBundle(b.infoSchema, id)
b.applyDropTable(diff, di, id, nil)
}
return tableIDs
}
func (b *Builder) applyRecoverSchema(m *meta.Meta, diff *model.SchemaDiff) ([]int64, error) {
if di, ok := b.infoSchema.SchemaByID(diff.SchemaID); ok {
return nil, ErrDatabaseExists.GenWithStackByArgs(
fmt.Sprintf("(Schema ID %d)", di.ID),
)
}
di, err := m.GetDatabase(diff.SchemaID)
if err != nil {
return nil, errors.Trace(err)
}
b.infoSchema.schemaMap[di.Name.L] = &schemaTables{
dbInfo: di,
tables: make(map[string]table.Table, len(diff.AffectedOpts)),
}
return applyCreateTables(b, m, diff)
}
// copySortedTables copies sortedTables for old table and new table for later modification.
func (b *Builder) copySortedTables(oldTableID, newTableID int64) {
if tableIDIsValid(oldTableID) {
b.copySortedTablesBucket(tableBucketIdx(oldTableID))
}
if tableIDIsValid(newTableID) && newTableID != oldTableID {
b.copySortedTablesBucket(tableBucketIdx(newTableID))
}
}
func (b *Builder) copySortedTablesBucket(bucketIdx int) {
oldSortedTables := b.infoSchema.sortedTablesBuckets[bucketIdx]
newSortedTables := make(sortedTables, len(oldSortedTables))
copy(newSortedTables, oldSortedTables)
b.infoSchema.sortedTablesBuckets[bucketIdx] = newSortedTables
}
func (b *Builder) updateBundleForCreateTable(tblInfo *model.TableInfo, tp model.ActionType) {
switch tp {
case model.ActionDropTablePartition:
case model.ActionTruncateTablePartition:
// ReorganizePartition handle the bundles in applyReorganizePartition
case model.ActionReorganizePartition, model.ActionRemovePartitioning,
model.ActionAlterTablePartitioning:
default:
pi := tblInfo.GetPartitionInfo()
if pi != nil {
for _, partition := range pi.Definitions {
b.markPartitionBundleShouldUpdate(partition.ID)
}
}
}
}
func (b *Builder) buildAllocsForCreateTable(tp model.ActionType, dbInfo *model.DBInfo, tblInfo *model.TableInfo, allocs autoid.Allocators) autoid.Allocators {
if len(allocs.Allocs) != 0 {
tblVer := autoid.AllocOptionTableInfoVersion(tblInfo.Version)
switch tp {
case model.ActionRebaseAutoID, model.ActionModifyTableAutoIdCache:
idCacheOpt := autoid.CustomAutoIncCacheOption(tblInfo.AutoIdCache)
// If the allocator type might be AutoIncrementType, create both AutoIncrementType
// and RowIDAllocType allocator for it. Because auto id and row id could share the same allocator.
// Allocate auto id may route to allocate row id, if row id allocator is nil, the program panic!
for _, tp := range [2]autoid.AllocatorType{autoid.AutoIncrementType, autoid.RowIDAllocType} {
newAlloc := autoid.NewAllocator(b.Requirement, dbInfo.ID, tblInfo.ID, tblInfo.IsAutoIncColUnsigned(), tp, tblVer, idCacheOpt)
allocs = allocs.Append(newAlloc)
}
case model.ActionRebaseAutoRandomBase:
newAlloc := autoid.NewAllocator(b.Requirement, dbInfo.ID, tblInfo.ID, tblInfo.IsAutoRandomBitColUnsigned(), autoid.AutoRandomType, tblVer)
allocs = allocs.Append(newAlloc)
case model.ActionModifyColumn:
// Change column attribute from auto_increment to auto_random.
if tblInfo.ContainsAutoRandomBits() && allocs.Get(autoid.AutoRandomType) == nil {
// Remove auto_increment allocator.
allocs = allocs.Filter(func(a autoid.Allocator) bool {
return a.GetType() != autoid.AutoIncrementType && a.GetType() != autoid.RowIDAllocType
})
newAlloc := autoid.NewAllocator(b.Requirement, dbInfo.ID, tblInfo.ID, tblInfo.IsAutoRandomBitColUnsigned(), autoid.AutoRandomType, tblVer)
allocs = allocs.Append(newAlloc)
}
}
return allocs
}
return autoid.NewAllocatorsFromTblInfo(b.Requirement, dbInfo.ID, tblInfo)
}
func applyCreateTable(b *Builder, m *meta.Meta, dbInfo *model.DBInfo, tableID int64, allocs autoid.Allocators, tp model.ActionType, affected []int64, schemaVersion int64) ([]int64, error) {
tblInfo, err := m.GetTable(dbInfo.ID, tableID)
if err != nil {
return nil, errors.Trace(err)
}
if tblInfo == nil {
// When we apply an old schema diff, the table may has been dropped already, so we need to fall back to
// full load.
return nil, ErrTableNotExists.FastGenByArgs(
fmt.Sprintf("(Schema ID %d)", dbInfo.ID),
fmt.Sprintf("(Table ID %d)", tableID),
)
}
b.updateBundleForCreateTable(tblInfo, tp)
if tp != model.ActionTruncateTablePartition {
affected = appendAffectedIDs(affected, tblInfo)
}
// Failpoint check whether tableInfo should be added to repairInfo.
// Typically used in repair table test to load mock `bad` tableInfo into repairInfo.
failpoint.Inject("repairFetchCreateTable", func(val failpoint.Value) {
if val.(bool) {
if domainutil.RepairInfo.InRepairMode() && tp != model.ActionRepairTable && domainutil.RepairInfo.CheckAndFetchRepairedTable(dbInfo, tblInfo) {
failpoint.Return(nil, nil)
}
}
})
ConvertCharsetCollateToLowerCaseIfNeed(tblInfo)
ConvertOldVersionUTF8ToUTF8MB4IfNeed(tblInfo)
allocs = b.buildAllocsForCreateTable(tp, dbInfo, tblInfo, allocs)
tbl, err := b.tableFromMeta(allocs, tblInfo)
if err != nil {
return nil, errors.Trace(err)
}
b.infoSchema.addReferredForeignKeys(dbInfo.Name, tblInfo)
if !b.enableV2 {
tableNames := b.infoSchema.schemaMap[dbInfo.Name.L]
tableNames.tables[tblInfo.Name.L] = tbl
}
b.addTable(schemaVersion, dbInfo, tblInfo, tbl)
bucketIdx := tableBucketIdx(tableID)
slices.SortFunc(b.infoSchema.sortedTablesBuckets[bucketIdx], func(i, j table.Table) int {
return cmp.Compare(i.Meta().ID, j.Meta().ID)
})
if tblInfo.TempTableType != model.TempTableNone {
b.addTemporaryTable(tableID)
}
newTbl, ok := b.infoSchema.TableByID(tableID)
if ok {
dbInfo.Tables = append(dbInfo.Tables, newTbl.Meta())
}
return affected, nil
}
// ConvertCharsetCollateToLowerCaseIfNeed convert the charset / collation of table and its columns to lower case,
// if the table's version is prior to TableInfoVersion3.
func ConvertCharsetCollateToLowerCaseIfNeed(tbInfo *model.TableInfo) {
if tbInfo.Version >= model.TableInfoVersion3 {
return
}
tbInfo.Charset = strings.ToLower(tbInfo.Charset)
tbInfo.Collate = strings.ToLower(tbInfo.Collate)
for _, col := range tbInfo.Columns {
col.SetCharset(strings.ToLower(col.GetCharset()))
col.SetCollate(strings.ToLower(col.GetCollate()))
}
}
// ConvertOldVersionUTF8ToUTF8MB4IfNeed convert old version UTF8 to UTF8MB4 if config.TreatOldVersionUTF8AsUTF8MB4 is enable.
func ConvertOldVersionUTF8ToUTF8MB4IfNeed(tbInfo *model.TableInfo) {
if tbInfo.Version >= model.TableInfoVersion2 || !config.GetGlobalConfig().TreatOldVersionUTF8AsUTF8MB4 {
return
}
if tbInfo.Charset == charset.CharsetUTF8 {
tbInfo.Charset = charset.CharsetUTF8MB4
tbInfo.Collate = charset.CollationUTF8MB4
}
for _, col := range tbInfo.Columns {
if col.Version < model.ColumnInfoVersion2 && col.GetCharset() == charset.CharsetUTF8 {
col.SetCharset(charset.CharsetUTF8MB4)
col.SetCollate(charset.CollationUTF8MB4)
}
}
}
func (b *Builder) applyDropTable(diff *model.SchemaDiff, dbInfo *model.DBInfo, tableID int64, affected []int64) []int64 {
bucketIdx := tableBucketIdx(tableID)
sortedTbls := b.infoSchema.sortedTablesBuckets[bucketIdx]
idx := sortedTbls.searchTable(tableID)
if idx == -1 {
return affected
}
if tableNames, ok := b.infoSchema.schemaMap[dbInfo.Name.L]; ok {
tblInfo := sortedTbls[idx].Meta()
delete(tableNames.tables, tblInfo.Name.L)
affected = appendAffectedIDs(affected, tblInfo)
}
// Remove the table in sorted table slice.
b.infoSchema.sortedTablesBuckets[bucketIdx] = append(sortedTbls[0:idx], sortedTbls[idx+1:]...)
// Remove the table in temporaryTables
if b.infoSchema.temporaryTableIDs != nil {
delete(b.infoSchema.temporaryTableIDs, tableID)
}
// The old DBInfo still holds a reference to old table info, we need to remove it.
b.deleteReferredForeignKeys(dbInfo, tableID)
return affected
}
func (b *Builder) deleteReferredForeignKeys(dbInfo *model.DBInfo, tableID int64) {
for i, tblInfo := range dbInfo.Tables {
if tblInfo.ID == tableID {
if i == len(dbInfo.Tables)-1 {
dbInfo.Tables = dbInfo.Tables[:i]
} else {
dbInfo.Tables = append(dbInfo.Tables[:i], dbInfo.Tables[i+1:]...)
}
b.infoSchema.deleteReferredForeignKeys(dbInfo.Name, tblInfo)
break
}
}
}
// Build builds and returns the built infoschema.
func (b *Builder) Build(schemaTS uint64) InfoSchema {
if b.enableV2 {
b.infoschemaV2.ts = schemaTS
updateInfoSchemaBundles(b)
return &b.infoschemaV2
}
updateInfoSchemaBundles(b)
return b.infoSchema
}
// InitWithOldInfoSchema initializes an empty new InfoSchema by copies all the data from old InfoSchema.
func (b *Builder) InitWithOldInfoSchema(oldSchema InfoSchema) (*Builder, error) {
// Do not mix infoschema v1 and infoschema v2 building, this can simplify the logic.
// If we want to build infoschema v2, but the old infoschema is v1, just return error to trigger a full load.
if b.enableV2 != IsV2(oldSchema) {
return nil, errors.New("builder's infoschema mismatch, return error to trigger full reload")
}
if schemaV2, ok := oldSchema.(*infoschemaV2); ok {
b.infoschemaV2.ts = schemaV2.ts
}
oldIS := oldSchema.base()
b.initBundleInfoBuilder()
b.infoSchema.schemaMetaVersion = oldIS.schemaMetaVersion
b.copySchemasMap(oldIS)
b.copyBundlesMap(oldIS)
b.copyPoliciesMap(oldIS)
b.copyResourceGroupMap(oldIS)
b.copyTemporaryTableIDsMap(oldIS)
b.copyReferredForeignKeyMap(oldIS)
copy(b.infoSchema.sortedTablesBuckets, oldIS.sortedTablesBuckets)
return b, nil
}
func (b *Builder) copySchemasMap(oldIS *infoSchema) {
for k, v := range oldIS.schemaMap {
b.infoSchema.schemaMap[k] = v
}
}
// getSchemaAndCopyIfNecessary creates a new schemaTables instance when a table in the database has changed.
// It also does modifications on the new one because old schemaTables must be read-only.
// And it will only copy the changed database once in the lifespan of the Builder.
// NOTE: please make sure the dbName is in lowercase.
func (b *Builder) getSchemaAndCopyIfNecessary(dbName string) *model.DBInfo {
if !b.dirtyDB[dbName] {
b.dirtyDB[dbName] = true
oldSchemaTables := b.infoSchema.schemaMap[dbName]
newSchemaTables := &schemaTables{
dbInfo: oldSchemaTables.dbInfo.Copy(),
tables: make(map[string]table.Table, len(oldSchemaTables.tables)),
}
for k, v := range oldSchemaTables.tables {
newSchemaTables.tables[k] = v
}
b.infoSchema.schemaMap[dbName] = newSchemaTables
return newSchemaTables.dbInfo
}
return b.infoSchema.schemaMap[dbName].dbInfo
}
func (b *Builder) initVirtualTables(schemaVersion int64) error {
// Initialize virtual tables.
for _, driver := range drivers {
err := b.createSchemaTablesForDB(driver.DBInfo, driver.TableFromMeta, schemaVersion)
if err != nil {
return errors.Trace(err)
}
}
return nil
}
func (b *Builder) sortAllTablesByID() {
// Sort all tables by `ID`
for _, v := range b.infoSchema.sortedTablesBuckets {
slices.SortFunc(v, func(a, b table.Table) int {
return cmp.Compare(a.Meta().ID, b.Meta().ID)
})
}
}
// InitWithDBInfos initializes an empty new InfoSchema with a slice of DBInfo, all placement rules, and schema version.
func (b *Builder) InitWithDBInfos(dbInfos []*model.DBInfo, policies []*model.PolicyInfo, resourceGroups []*model.ResourceGroupInfo, schemaVersion int64) (*Builder, error) {
info := b.infoSchema
info.schemaMetaVersion = schemaVersion
b.initBundleInfoBuilder()
b.initMisc(dbInfos, policies, resourceGroups)
for _, di := range dbInfos {
err := b.createSchemaTablesForDB(di, b.tableFromMeta, schemaVersion)
if err != nil {
return nil, errors.Trace(err)
}
}
err := b.initVirtualTables(schemaVersion)
if err != nil {
return nil, err
}
b.sortAllTablesByID()
return b, nil
}
func (b *Builder) tableFromMeta(alloc autoid.Allocators, tblInfo *model.TableInfo) (table.Table, error) {
ret, err := tables.TableFromMeta(alloc, tblInfo)
if err != nil {
return nil, errors.Trace(err)
}
if t, ok := ret.(table.CachedTable); ok {
var tmp pools.Resource
tmp, err = b.factory()
if err != nil {
return nil, errors.Trace(err)
}
err = t.Init(tmp.(sessionctx.Context).GetSQLExecutor())
if err != nil {
return nil, errors.Trace(err)
}
}
return ret, nil
}
type tableFromMetaFunc func(alloc autoid.Allocators, tblInfo *model.TableInfo) (table.Table, error)
func (b *Builder) createSchemaTablesForDB(di *model.DBInfo, tableFromMeta tableFromMetaFunc, schemaVersion int64) error {
schTbls := &schemaTables{
dbInfo: di,
tables: make(map[string]table.Table, len(di.Tables)),
}
for _, t := range di.Tables {
allocs := autoid.NewAllocatorsFromTblInfo(b.Requirement, di.ID, t)
var tbl table.Table
tbl, err := tableFromMeta(allocs, t)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("Build table `%s`.`%s` schema failed", di.Name.O, t.Name.O))
}
schTbls.tables[t.Name.L] = tbl
b.addTable(schemaVersion, di, t, tbl)
if tblInfo := tbl.Meta(); tblInfo.TempTableType != model.TempTableNone {
b.addTemporaryTable(tblInfo.ID)
}
}
b.addDB(schemaVersion, di, schTbls)
return nil
}
func (b *Builder) addDB(schemaVersion int64, di *model.DBInfo, schTbls *schemaTables) {
if b.enableV2 {
if isSpecialDB(di.Name.L) {
b.infoData.addSpecialDB(di, schTbls)
} else {
b.infoData.addDB(schemaVersion, di)
}
} else {
b.infoSchema.schemaMap[di.Name.L] = schTbls
}
}
func (b *Builder) addTable(schemaVersion int64, di *model.DBInfo, tblInfo *model.TableInfo, tbl table.Table) {
if b.enableV2 {
b.infoData.add(tableItem{
dbName: di.Name.L,
dbID: di.ID,
tableName: tblInfo.Name.L,
tableID: tblInfo.ID,
schemaVersion: schemaVersion,
}, tbl)
} else {
sortedTbls := b.infoSchema.sortedTablesBuckets[tableBucketIdx(tblInfo.ID)]
b.infoSchema.sortedTablesBuckets[tableBucketIdx(tblInfo.ID)] = append(sortedTbls, tbl)
}
}
type virtualTableDriver struct {
*model.DBInfo
TableFromMeta tableFromMetaFunc
}
var drivers []*virtualTableDriver
// RegisterVirtualTable register virtual tables to the builder.
func RegisterVirtualTable(dbInfo *model.DBInfo, tableFromMeta tableFromMetaFunc) {
drivers = append(drivers, &virtualTableDriver{dbInfo, tableFromMeta})
}
// NewBuilder creates a new Builder with a Handle.
func NewBuilder(r autoid.Requirement, factory func() (pools.Resource, error), infoData *Data) *Builder {
return &Builder{
enableV2: variable.SchemaCacheSize.Load() > 0,
Requirement: r,
infoschemaV2: NewInfoSchemaV2(r, infoData),
dirtyDB: make(map[string]bool),
factory: factory,
infoData: infoData,
}
}
func tableBucketIdx(tableID int64) int {
intest.Assert(tableID > 0)
return int(tableID % bucketCount)
}
func tableIDIsValid(tableID int64) bool {
return tableID > 0
}