1350 lines
49 KiB
Go
1350 lines
49 KiB
Go
// Copyright 2024 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 ddl
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/pingcap/errors"
|
|
"github.com/pingcap/failpoint"
|
|
"github.com/pingcap/tidb/pkg/config"
|
|
"github.com/pingcap/tidb/pkg/ddl/logutil"
|
|
"github.com/pingcap/tidb/pkg/ddl/notifier"
|
|
"github.com/pingcap/tidb/pkg/errctx"
|
|
"github.com/pingcap/tidb/pkg/expression"
|
|
"github.com/pingcap/tidb/pkg/expression/exprctx"
|
|
"github.com/pingcap/tidb/pkg/infoschema"
|
|
"github.com/pingcap/tidb/pkg/meta/autoid"
|
|
"github.com/pingcap/tidb/pkg/meta/metabuild"
|
|
"github.com/pingcap/tidb/pkg/meta/model"
|
|
"github.com/pingcap/tidb/pkg/parser/ast"
|
|
"github.com/pingcap/tidb/pkg/parser/charset"
|
|
"github.com/pingcap/tidb/pkg/parser/format"
|
|
"github.com/pingcap/tidb/pkg/parser/mysql"
|
|
"github.com/pingcap/tidb/pkg/parser/terror"
|
|
field_types "github.com/pingcap/tidb/pkg/parser/types"
|
|
"github.com/pingcap/tidb/pkg/sessionctx"
|
|
"github.com/pingcap/tidb/pkg/sessionctx/vardef"
|
|
"github.com/pingcap/tidb/pkg/table"
|
|
"github.com/pingcap/tidb/pkg/types"
|
|
driver "github.com/pingcap/tidb/pkg/types/parser_driver"
|
|
"github.com/pingcap/tidb/pkg/util/collate"
|
|
"github.com/pingcap/tidb/pkg/util/dbterror"
|
|
"github.com/pingcap/tidb/pkg/util/hack"
|
|
"github.com/pingcap/tidb/pkg/util/intest"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func (w *worker) onAddColumn(jobCtx *jobContext, job *model.Job) (ver int64, err error) {
|
|
// Handle the rolling back job.
|
|
if job.IsRollingback() {
|
|
ver, err = onDropColumn(jobCtx, job)
|
|
if err != nil {
|
|
return ver, errors.Trace(err)
|
|
}
|
|
return ver, nil
|
|
}
|
|
|
|
failpoint.Inject("errorBeforeDecodeArgs", func(val failpoint.Value) {
|
|
//nolint:forcetypeassert
|
|
if val.(bool) {
|
|
failpoint.Return(ver, errors.New("occur an error before decode args"))
|
|
}
|
|
})
|
|
|
|
tblInfo, columnInfo, colFromArgs, pos, ifNotExists, err := checkAddColumn(jobCtx.metaMut, job)
|
|
if err != nil {
|
|
if ifNotExists && infoschema.ErrColumnExists.Equal(err) {
|
|
job.Warning = toTError(err)
|
|
job.State = model.JobStateDone
|
|
return ver, nil
|
|
}
|
|
return ver, errors.Trace(err)
|
|
}
|
|
if columnInfo == nil {
|
|
columnInfo = InitAndAddColumnToTable(tblInfo, colFromArgs)
|
|
logutil.DDLLogger().Info("run add column job", zap.Stringer("job", job), zap.Reflect("columnInfo", *columnInfo))
|
|
if err = checkAddColumnTooManyColumns(len(tblInfo.Columns)); err != nil {
|
|
job.State = model.JobStateCancelled
|
|
return ver, errors.Trace(err)
|
|
}
|
|
if err = checkUnsupportedCharsetForTiFlash(tblInfo, columnInfo.Name, columnInfo.FieldType); err != nil {
|
|
job.State = model.JobStateCancelled
|
|
return ver, errors.Trace(err)
|
|
}
|
|
}
|
|
|
|
originalState := columnInfo.State
|
|
switch columnInfo.State {
|
|
case model.StateNone:
|
|
// none -> delete only
|
|
columnInfo.State = model.StateDeleteOnly
|
|
ver, err = updateVersionAndTableInfoWithCheck(jobCtx, job, tblInfo, originalState != columnInfo.State)
|
|
if err != nil {
|
|
return ver, errors.Trace(err)
|
|
}
|
|
job.SchemaState = model.StateDeleteOnly
|
|
case model.StateDeleteOnly:
|
|
// delete only -> write only
|
|
columnInfo.State = model.StateWriteOnly
|
|
ver, err = updateVersionAndTableInfo(jobCtx, job, tblInfo, originalState != columnInfo.State)
|
|
if err != nil {
|
|
return ver, errors.Trace(err)
|
|
}
|
|
// Update the job state when all affairs done.
|
|
job.SchemaState = model.StateWriteOnly
|
|
case model.StateWriteOnly:
|
|
// write only -> reorganization
|
|
columnInfo.State = model.StateWriteReorganization
|
|
ver, err = updateVersionAndTableInfo(jobCtx, job, tblInfo, originalState != columnInfo.State)
|
|
if err != nil {
|
|
return ver, errors.Trace(err)
|
|
}
|
|
// Update the job state when all affairs done.
|
|
job.SchemaState = model.StateWriteReorganization
|
|
job.MarkNonRevertible()
|
|
case model.StateWriteReorganization:
|
|
// reorganization -> public
|
|
// Adjust table column offset.
|
|
failpoint.InjectCall("onAddColumnStateWriteReorg")
|
|
offset, err := LocateOffsetToMove(columnInfo.Offset, pos, tblInfo)
|
|
if err != nil {
|
|
return ver, errors.Trace(err)
|
|
}
|
|
tblInfo.MoveColumnInfo(columnInfo.Offset, offset)
|
|
columnInfo.State = model.StatePublic
|
|
// Use updateVersionAndTableInfoWithCheck to validate the table before making it public
|
|
ver, err = updateVersionAndTableInfoWithCheck(jobCtx, job, tblInfo, originalState != columnInfo.State)
|
|
if err != nil {
|
|
return ver, errors.Trace(err)
|
|
}
|
|
|
|
addColumnEvent := notifier.NewAddColumnEvent(tblInfo, []*model.ColumnInfo{columnInfo})
|
|
err = asyncNotifyEvent(jobCtx, addColumnEvent, job, noSubJob, w.sess)
|
|
if err != nil {
|
|
return ver, errors.Trace(err)
|
|
}
|
|
// Finish this job.
|
|
job.FinishTableJob(model.JobStateDone, model.StatePublic, ver, tblInfo)
|
|
default:
|
|
err = dbterror.ErrInvalidDDLState.GenWithStackByArgs("column", columnInfo.State)
|
|
}
|
|
|
|
return ver, errors.Trace(err)
|
|
}
|
|
|
|
func checkAndCreateNewColumn(ctx sessionctx.Context, ti ast.Ident, schema *model.DBInfo, spec *ast.AlterTableSpec, t table.Table, specNewColumn *ast.ColumnDef) (*table.Column, error) {
|
|
err := checkUnsupportedColumnConstraint(specNewColumn, ti)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
|
|
colName := specNewColumn.Name.Name.O
|
|
// Check whether added column has existed.
|
|
col := table.FindCol(t.Cols(), colName)
|
|
if col != nil {
|
|
err = infoschema.ErrColumnExists.GenWithStackByArgs(colName)
|
|
if spec.IfNotExists {
|
|
ctx.GetSessionVars().StmtCtx.AppendNote(err)
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
if err = checkColumnAttributes(colName, specNewColumn.Tp); err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
if utf8.RuneCountInString(colName) > mysql.MaxColumnNameLength {
|
|
return nil, dbterror.ErrTooLongIdent.GenWithStackByArgs(colName)
|
|
}
|
|
|
|
newCol, err := CreateNewColumn(ctx, schema, spec, t, specNewColumn)
|
|
if err == nil {
|
|
err = checkUnsupportedCharsetForTiFlash(t.Meta(), newCol.Name, newCol.FieldType)
|
|
}
|
|
return newCol, errors.Trace(err)
|
|
}
|
|
|
|
func checkUnsupportedCharsetForTiFlash(tbl *model.TableInfo, colName ast.CIStr, colTp types.FieldType) error {
|
|
if tbl.TiFlashReplica == nil {
|
|
return nil
|
|
}
|
|
|
|
if _, isSupported := charset.TiFlashSupportedCharsets[colTp.GetCharset()]; !isSupported {
|
|
return dbterror.ErrUnsupportedAddColumn.GenWithStack("unsupported add column '%s' when altering '%s' with TiFlash replicas and %s encoding", colName, tbl.Name, colTp.GetCharset())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checkUnsupportedColumnConstraint(col *ast.ColumnDef, ti ast.Ident) error {
|
|
for _, constraint := range col.Options {
|
|
switch constraint.Tp {
|
|
case ast.ColumnOptionAutoIncrement:
|
|
return dbterror.ErrUnsupportedAddColumn.GenWithStack("unsupported add column '%s' constraint AUTO_INCREMENT when altering '%s.%s'", col.Name, ti.Schema, ti.Name)
|
|
case ast.ColumnOptionPrimaryKey:
|
|
return dbterror.ErrUnsupportedAddColumn.GenWithStack("unsupported add column '%s' constraint PRIMARY KEY when altering '%s.%s'", col.Name, ti.Schema, ti.Name)
|
|
case ast.ColumnOptionUniqKey:
|
|
return dbterror.ErrUnsupportedAddColumn.GenWithStack("unsupported add column '%s' constraint UNIQUE KEY when altering '%s.%s'", col.Name, ti.Schema, ti.Name)
|
|
case ast.ColumnOptionAutoRandom:
|
|
errMsg := fmt.Sprintf(autoid.AutoRandomAlterAddColumn, col.Name, ti.Schema, ti.Name)
|
|
return dbterror.ErrInvalidAutoRandom.GenWithStackByArgs(errMsg)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CreateNewColumn creates a new column according to the column information.
|
|
func CreateNewColumn(ctx sessionctx.Context, schema *model.DBInfo, spec *ast.AlterTableSpec, t table.Table, specNewColumn *ast.ColumnDef) (*table.Column, error) {
|
|
// If new column is a generated column, do validation.
|
|
// NOTE: we do check whether the column refers other generated
|
|
// columns occurring later in a table, but we don't handle the col offset.
|
|
for _, option := range specNewColumn.Options {
|
|
if option.Tp == ast.ColumnOptionGenerated {
|
|
if err := checkIllegalFn4Generated(specNewColumn.Name.Name.L, typeColumn, option.Expr); err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
|
|
if option.Stored {
|
|
return nil, dbterror.ErrUnsupportedOnGeneratedColumn.GenWithStackByArgs("Adding generated stored column through ALTER TABLE")
|
|
}
|
|
|
|
_, dependColNames, err := findDependedColumnNames(schema.Name, t.Meta().Name, specNewColumn)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
//nolint:forbidigo
|
|
if !ctx.GetSessionVars().EnableAutoIncrementInGenerated {
|
|
if err := checkAutoIncrementRef(specNewColumn.Name.Name.L, dependColNames, t.Meta()); err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
}
|
|
duplicateColNames := make(map[string]struct{}, len(dependColNames))
|
|
for k := range dependColNames {
|
|
duplicateColNames[k] = struct{}{}
|
|
}
|
|
cols := t.Cols()
|
|
|
|
if err := checkDependedColExist(dependColNames, cols); err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
|
|
if err := verifyColumnGenerationSingle(duplicateColNames, cols, spec.Position); err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
}
|
|
// Specially, since sequence has been supported, if a newly added column has a
|
|
// sequence nextval function as it's default value option, it won't fill the
|
|
// known rows with specific sequence next value under current add column logic.
|
|
// More explanation can refer: TestSequenceDefaultLogic's comment in sequence_test.go
|
|
if option.Tp == ast.ColumnOptionDefaultValue {
|
|
if f, ok := option.Expr.(*ast.FuncCallExpr); ok {
|
|
switch f.FnName.L {
|
|
case ast.NextVal:
|
|
if _, err := getSequenceDefaultValue(option); err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
return nil, errors.Trace(dbterror.ErrAddColumnWithSequenceAsDefault.GenWithStackByArgs(specNewColumn.Name.Name.O))
|
|
case ast.Rand, ast.UUID, ast.UUIDToBin, ast.Replace, ast.Upper:
|
|
return nil, errors.Trace(dbterror.ErrBinlogUnsafeSystemFunction.GenWithStackByArgs())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
tableCharset, tableCollate, err := ResolveCharsetCollation([]ast.CharsetOpt{
|
|
{Chs: t.Meta().Charset, Col: t.Meta().Collate},
|
|
{Chs: schema.Charset, Col: schema.Collate},
|
|
}, ctx.GetSessionVars().DefaultCollationForUTF8MB4)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
// Ignore table constraints now, they will be checked later.
|
|
// We use length(t.Cols()) as the default offset firstly, we will change the column's offset later.
|
|
col, _, err := buildColumnAndConstraint(
|
|
NewMetaBuildContextWithSctx(ctx),
|
|
len(t.Cols()),
|
|
specNewColumn,
|
|
nil,
|
|
tableCharset,
|
|
tableCollate,
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
|
|
originDefVal, err := generateOriginDefaultValue(col.ToInfo(), ctx, true)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
|
|
err = col.SetOriginDefaultValue(originDefVal)
|
|
return col, err
|
|
}
|
|
|
|
// buildColumnAndConstraint builds table.Column and ast.Constraint from the parameters.
|
|
// outPriKeyConstraint is the primary key constraint out of column definition. For example:
|
|
// `create table t1 (id int , age int, primary key(id));`
|
|
func buildColumnAndConstraint(
|
|
ctx *metabuild.Context,
|
|
offset int,
|
|
colDef *ast.ColumnDef,
|
|
outPriKeyConstraint *ast.Constraint,
|
|
tblCharset string,
|
|
tblCollate string,
|
|
) (*table.Column, []*ast.Constraint, error) {
|
|
if colName := colDef.Name.Name.L; colName == model.ExtraHandleName.L {
|
|
return nil, nil, dbterror.ErrWrongColumnName.GenWithStackByArgs(colName)
|
|
}
|
|
|
|
// specifiedCollate refers to the last collate specified in colDef.Options.
|
|
chs, coll, err := getCharsetAndCollateInColumnDef(colDef, ctx.GetDefaultCollationForUTF8MB4())
|
|
if err != nil {
|
|
return nil, nil, errors.Trace(err)
|
|
}
|
|
chs, coll, err = ResolveCharsetCollation([]ast.CharsetOpt{
|
|
{Chs: chs, Col: coll},
|
|
{Chs: tblCharset, Col: tblCollate},
|
|
}, ctx.GetDefaultCollationForUTF8MB4())
|
|
chs, coll = OverwriteCollationWithBinaryFlag(colDef, chs, coll, ctx.GetDefaultCollationForUTF8MB4())
|
|
if err != nil {
|
|
return nil, nil, errors.Trace(err)
|
|
}
|
|
|
|
if err := setCharsetCollationFlenDecimal(ctx, colDef.Tp, colDef.Name.Name.O, chs, coll); err != nil {
|
|
return nil, nil, errors.Trace(err)
|
|
}
|
|
decodeEnumSetBinaryLiteralToUTF8(colDef.Tp, chs)
|
|
col, cts, err := columnDefToCol(ctx, offset, colDef, outPriKeyConstraint)
|
|
if err != nil {
|
|
return nil, nil, errors.Trace(err)
|
|
}
|
|
return col, cts, nil
|
|
}
|
|
|
|
// getCharsetAndCollateInColumnDef will iterate collate in the options, validate it by checking the charset
|
|
// of column definition. If there's no collate in the option, the default collate of column's charset will be used.
|
|
func getCharsetAndCollateInColumnDef(def *ast.ColumnDef, defaultUTF8MB4Coll string) (chs, coll string, err error) {
|
|
chs = def.Tp.GetCharset()
|
|
coll = def.Tp.GetCollate()
|
|
if chs != "" && coll == "" {
|
|
if coll, err = GetDefaultCollation(chs, defaultUTF8MB4Coll); err != nil {
|
|
return "", "", errors.Trace(err)
|
|
}
|
|
}
|
|
for _, opt := range def.Options {
|
|
if opt.Tp == ast.ColumnOptionCollate {
|
|
info, err := collate.GetCollationByName(opt.StrValue)
|
|
if err != nil {
|
|
return "", "", errors.Trace(err)
|
|
}
|
|
if chs == "" {
|
|
chs = info.CharsetName
|
|
} else if chs != info.CharsetName {
|
|
return "", "", dbterror.ErrCollationCharsetMismatch.GenWithStackByArgs(info.Name, chs)
|
|
}
|
|
coll = info.Name
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// OverwriteCollationWithBinaryFlag is used to handle the case like
|
|
//
|
|
// CREATE TABLE t (a VARCHAR(255) BINARY) CHARSET utf8 COLLATE utf8_general_ci;
|
|
//
|
|
// The 'BINARY' sets the column collation to *_bin according to the table charset.
|
|
func OverwriteCollationWithBinaryFlag(colDef *ast.ColumnDef, chs, coll string, defaultUTF8MB4Coll string) (newChs string, newColl string) {
|
|
ignoreBinFlag := colDef.Tp.GetCharset() != "" && (colDef.Tp.GetCollate() != "" || containsColumnOption(colDef, ast.ColumnOptionCollate))
|
|
if ignoreBinFlag {
|
|
return chs, coll
|
|
}
|
|
needOverwriteBinColl := types.IsString(colDef.Tp.GetType()) && mysql.HasBinaryFlag(colDef.Tp.GetFlag())
|
|
if needOverwriteBinColl {
|
|
newColl, err := GetDefaultCollation(chs, defaultUTF8MB4Coll)
|
|
if err != nil {
|
|
return chs, coll
|
|
}
|
|
return chs, newColl
|
|
}
|
|
return chs, coll
|
|
}
|
|
|
|
func setCharsetCollationFlenDecimal(ctx *metabuild.Context, tp *types.FieldType, colName, colCharset, colCollate string) error {
|
|
var err error
|
|
if typesNeedCharset(tp.GetType()) {
|
|
tp.SetCharset(colCharset)
|
|
tp.SetCollate(colCollate)
|
|
} else {
|
|
tp.SetCharset(charset.CharsetBin)
|
|
tp.SetCollate(charset.CharsetBin)
|
|
}
|
|
|
|
// Use default value for flen or decimal when they are unspecified.
|
|
defaultFlen, defaultDecimal := mysql.GetDefaultFieldLengthAndDecimal(tp.GetType())
|
|
if tp.GetDecimal() == types.UnspecifiedLength {
|
|
tp.SetDecimal(defaultDecimal)
|
|
}
|
|
if tp.GetFlen() == types.UnspecifiedLength {
|
|
tp.SetFlen(defaultFlen)
|
|
if mysql.HasUnsignedFlag(tp.GetFlag()) && tp.GetType() != mysql.TypeLonglong && mysql.IsIntegerType(tp.GetType()) {
|
|
// Issue #4684: the flen of unsigned integer(except bigint) is 1 digit shorter than signed integer
|
|
// because it has no prefix "+" or "-" character.
|
|
tp.SetFlen(tp.GetFlen() - 1)
|
|
}
|
|
} else {
|
|
// Adjust the field type for blob/text types if the flen is set.
|
|
if err = adjustBlobTypesFlen(tp, colCharset); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return checkTooBigFieldLengthAndTryAutoConvert(ctx, tp, colName)
|
|
}
|
|
|
|
func decodeEnumSetBinaryLiteralToUTF8(tp *types.FieldType, chs string) {
|
|
if tp.GetType() != mysql.TypeEnum && tp.GetType() != mysql.TypeSet {
|
|
return
|
|
}
|
|
enc := charset.FindEncoding(chs)
|
|
for i, elem := range tp.GetElems() {
|
|
if !tp.GetElemIsBinaryLit(i) {
|
|
continue
|
|
}
|
|
s, err := enc.Transform(nil, hack.Slice(elem), charset.OpDecodeReplace)
|
|
if err != nil {
|
|
logutil.DDLLogger().Warn("decode enum binary literal to utf-8 failed", zap.Error(err))
|
|
}
|
|
tp.SetElem(i, string(hack.String(s)))
|
|
}
|
|
tp.CleanElemIsBinaryLit()
|
|
}
|
|
|
|
func typesNeedCharset(tp byte) bool {
|
|
switch tp {
|
|
case mysql.TypeString, mysql.TypeVarchar, mysql.TypeVarString,
|
|
mysql.TypeBlob, mysql.TypeTinyBlob, mysql.TypeMediumBlob, mysql.TypeLongBlob,
|
|
mysql.TypeEnum, mysql.TypeSet:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// checkTooBigFieldLengthAndTryAutoConvert will check whether the field length is too big
|
|
// in non-strict mode and varchar column. If it is, will try to adjust to blob or text, see issue #30328
|
|
func checkTooBigFieldLengthAndTryAutoConvert(ctx *metabuild.Context, tp *types.FieldType, colName string) error {
|
|
if !ctx.GetSQLMode().HasStrictMode() && tp.GetType() == mysql.TypeVarchar {
|
|
err := types.IsVarcharTooBigFieldLength(tp.GetFlen(), colName, tp.GetCharset())
|
|
if err != nil && terror.ErrorEqual(types.ErrTooBigFieldLength, err) {
|
|
tp.SetType(mysql.TypeBlob)
|
|
if err = adjustBlobTypesFlen(tp, tp.GetCharset()); err != nil {
|
|
return err
|
|
}
|
|
if tp.GetCharset() == charset.CharsetBin {
|
|
ctx.AppendWarning(dbterror.ErrAutoConvert.FastGenByArgs(colName, "VARBINARY", "BLOB"))
|
|
} else {
|
|
ctx.AppendWarning(dbterror.ErrAutoConvert.FastGenByArgs(colName, "VARCHAR", "TEXT"))
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// columnDefToCol converts ColumnDef to Col and TableConstraints.
|
|
// outPriKeyConstraint is the primary key constraint out of column definition. such as: create table t1 (id int , age int, primary key(id));
|
|
func columnDefToCol(ctx *metabuild.Context, offset int, colDef *ast.ColumnDef, outPriKeyConstraint *ast.Constraint) (*table.Column, []*ast.Constraint, error) {
|
|
var constraints = make([]*ast.Constraint, 0)
|
|
col := table.ToColumn(&model.ColumnInfo{
|
|
Offset: offset,
|
|
Name: colDef.Name.Name,
|
|
FieldType: *colDef.Tp,
|
|
// TODO: remove this version field after there is no old version.
|
|
Version: model.CurrLatestColumnInfoVersion,
|
|
})
|
|
|
|
if !isExplicitTimeStamp() {
|
|
// Check and set TimestampFlag, OnUpdateNowFlag and NotNullFlag.
|
|
if col.GetType() == mysql.TypeTimestamp {
|
|
col.AddFlag(mysql.TimestampFlag | mysql.OnUpdateNowFlag | mysql.NotNullFlag)
|
|
}
|
|
}
|
|
var err error
|
|
setOnUpdateNow := false
|
|
hasDefaultValue := false
|
|
hasNullFlag := false
|
|
if colDef.Options != nil {
|
|
length := types.UnspecifiedLength
|
|
|
|
keys := []*ast.IndexPartSpecification{
|
|
{
|
|
Column: colDef.Name,
|
|
Length: length,
|
|
},
|
|
}
|
|
|
|
var sb strings.Builder
|
|
restoreFlags := format.RestoreStringSingleQuotes | format.RestoreKeyWordLowercase | format.RestoreNameBackQuotes |
|
|
format.RestoreSpacesAroundBinaryOperation | format.RestoreWithoutSchemaName | format.RestoreWithoutTableName
|
|
restoreCtx := format.NewRestoreCtx(restoreFlags, &sb)
|
|
|
|
for _, v := range colDef.Options {
|
|
switch v.Tp {
|
|
case ast.ColumnOptionNotNull:
|
|
col.AddFlag(mysql.NotNullFlag)
|
|
case ast.ColumnOptionNull:
|
|
col.DelFlag(mysql.NotNullFlag)
|
|
removeOnUpdateNowFlag(col)
|
|
hasNullFlag = true
|
|
case ast.ColumnOptionAutoIncrement:
|
|
col.AddFlag(mysql.AutoIncrementFlag | mysql.NotNullFlag)
|
|
case ast.ColumnOptionPrimaryKey:
|
|
// Check PriKeyFlag first to avoid extra duplicate constraints.
|
|
if col.GetFlag()&mysql.PriKeyFlag == 0 {
|
|
constraint := &ast.Constraint{Tp: ast.ConstraintPrimaryKey, Keys: keys,
|
|
Option: &ast.IndexOption{PrimaryKeyTp: v.PrimaryKeyTp}}
|
|
if v.StrValue == "Global" {
|
|
constraint.Option.Global = true
|
|
}
|
|
constraints = append(constraints, constraint)
|
|
col.AddFlag(mysql.PriKeyFlag)
|
|
// Add NotNullFlag early so that processColumnFlags() can see it.
|
|
col.AddFlag(mysql.NotNullFlag)
|
|
}
|
|
case ast.ColumnOptionUniqKey:
|
|
// Check UniqueFlag first to avoid extra duplicate constraints.
|
|
if col.GetFlag()&mysql.UniqueFlag == 0 {
|
|
constraint := &ast.Constraint{Tp: ast.ConstraintUniqKey, Keys: keys}
|
|
if v.StrValue == "Global" {
|
|
constraint.Option = &ast.IndexOption{Global: true}
|
|
}
|
|
constraints = append(constraints, constraint)
|
|
col.AddFlag(mysql.UniqueKeyFlag)
|
|
}
|
|
case ast.ColumnOptionDefaultValue:
|
|
hasDefaultValue, err = SetDefaultValue(ctx.GetExprCtx(), col, v)
|
|
if err != nil {
|
|
return nil, nil, errors.Trace(err)
|
|
}
|
|
removeOnUpdateNowFlag(col)
|
|
case ast.ColumnOptionOnUpdate:
|
|
// TODO: Support other time functions.
|
|
if !(col.GetType() == mysql.TypeTimestamp || col.GetType() == mysql.TypeDatetime) {
|
|
return nil, nil, dbterror.ErrInvalidOnUpdate.GenWithStackByArgs(col.Name)
|
|
}
|
|
if !expression.IsValidCurrentTimestampExpr(v.Expr, colDef.Tp) {
|
|
return nil, nil, dbterror.ErrInvalidOnUpdate.GenWithStackByArgs(col.Name)
|
|
}
|
|
col.AddFlag(mysql.OnUpdateNowFlag)
|
|
setOnUpdateNow = true
|
|
case ast.ColumnOptionComment:
|
|
err := setColumnComment(ctx.GetExprCtx(), col, v)
|
|
if err != nil {
|
|
return nil, nil, errors.Trace(err)
|
|
}
|
|
case ast.ColumnOptionGenerated:
|
|
sb.Reset()
|
|
err = v.Expr.Restore(restoreCtx)
|
|
if err != nil {
|
|
return nil, nil, errors.Trace(err)
|
|
}
|
|
col.GeneratedExprString = sb.String()
|
|
col.GeneratedStored = v.Stored
|
|
_, dependColNames, err := findDependedColumnNames(ast.NewCIStr(""), ast.NewCIStr(""), colDef)
|
|
if err != nil {
|
|
return nil, nil, errors.Trace(err)
|
|
}
|
|
col.Dependences = dependColNames
|
|
case ast.ColumnOptionCollate:
|
|
if field_types.HasCharset(colDef.Tp) {
|
|
col.FieldType.SetCollate(v.StrValue)
|
|
}
|
|
case ast.ColumnOptionFulltext:
|
|
ctx.AppendWarning(dbterror.ErrTableCantHandleFt.FastGenByArgs())
|
|
case ast.ColumnOptionCheck:
|
|
if !vardef.EnableCheckConstraint.Load() {
|
|
ctx.AppendWarning(errCheckConstraintIsOff)
|
|
} else {
|
|
// Check the column CHECK constraint dependency lazily, after fill all the name.
|
|
// Extract column constraint from column option.
|
|
constraint := &ast.Constraint{
|
|
Tp: ast.ConstraintCheck,
|
|
Expr: v.Expr,
|
|
Enforced: v.Enforced,
|
|
Name: v.ConstraintName,
|
|
InColumn: true,
|
|
InColumnName: colDef.Name.Name.O,
|
|
}
|
|
constraints = append(constraints, constraint)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if err = processAndCheckDefaultValueAndColumn(ctx.GetExprCtx(), col, outPriKeyConstraint, hasDefaultValue, setOnUpdateNow, hasNullFlag); err != nil {
|
|
return nil, nil, errors.Trace(err)
|
|
}
|
|
return col, constraints, nil
|
|
}
|
|
|
|
// isExplicitTimeStamp is used to check if explicit_defaults_for_timestamp is on or off.
|
|
// Check out this link for more details.
|
|
// https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_explicit_defaults_for_timestamp
|
|
func isExplicitTimeStamp() bool {
|
|
// TODO: implement the behavior as MySQL when explicit_defaults_for_timestamp = off, then this function could return false.
|
|
return true
|
|
}
|
|
|
|
// SetDefaultValue sets the default value of the column.
|
|
func SetDefaultValue(ctx expression.BuildContext, col *table.Column, option *ast.ColumnOption) (hasDefaultValue bool, err error) {
|
|
var value any
|
|
var isSeqExpr bool
|
|
value, isSeqExpr, err = getDefaultValue(
|
|
exprctx.CtxWithHandleTruncateErrLevel(ctx, errctx.LevelError),
|
|
col, option,
|
|
)
|
|
if err != nil {
|
|
return false, errors.Trace(err)
|
|
}
|
|
if isSeqExpr {
|
|
if err := checkSequenceDefaultValue(col); err != nil {
|
|
return false, errors.Trace(err)
|
|
}
|
|
col.DefaultIsExpr = isSeqExpr
|
|
}
|
|
if _, ok := option.Expr.(*ast.ColumnNameExpr); ok {
|
|
return hasDefaultValue, errors.New("column name is not yet supported as a default value")
|
|
}
|
|
// When the default value is expression, we skip check and convert.
|
|
if !col.DefaultIsExpr {
|
|
if hasDefaultValue, value, err = checkColumnDefaultValue(ctx, col, value); err != nil {
|
|
return hasDefaultValue, errors.Trace(err)
|
|
}
|
|
value, err = convertTimestampDefaultValToUTC(ctx.GetEvalCtx().TypeCtx(), value, col)
|
|
if err != nil {
|
|
return hasDefaultValue, errors.Trace(err)
|
|
}
|
|
} else {
|
|
hasDefaultValue = true
|
|
}
|
|
err = setDefaultValueWithBinaryPadding(col, value)
|
|
if err != nil {
|
|
return hasDefaultValue, errors.Trace(err)
|
|
}
|
|
return hasDefaultValue, nil
|
|
}
|
|
|
|
// getFuncCallDefaultValue gets the default column value of function-call expression.
|
|
func getFuncCallDefaultValue(col *table.Column, option *ast.ColumnOption, expr *ast.FuncCallExpr) (any, bool, error) {
|
|
switch expr.FnName.L {
|
|
case ast.CurrentTimestamp, ast.CurrentDate: // CURRENT_TIMESTAMP() and CURRENT_DATE()
|
|
tp, fsp := col.FieldType.GetType(), col.FieldType.GetDecimal()
|
|
if tp == mysql.TypeTimestamp || tp == mysql.TypeDatetime {
|
|
defaultFsp := 0
|
|
if len(expr.Args) == 1 {
|
|
if val := expr.Args[0].(*driver.ValueExpr); val != nil {
|
|
defaultFsp = int(val.GetInt64())
|
|
}
|
|
}
|
|
if defaultFsp != fsp {
|
|
return nil, false, dbterror.ErrInvalidDefaultValue.GenWithStackByArgs(col.Name.O)
|
|
}
|
|
}
|
|
return nil, false, nil
|
|
case ast.NextVal:
|
|
// handle default next value of sequence. (keep the expr string)
|
|
str, err := getSequenceDefaultValue(option)
|
|
if err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
return str, true, nil
|
|
case ast.Rand, ast.UUID, ast.UUIDToBin: // RAND(), UUID() and UUID_TO_BIN()
|
|
if err := expression.VerifyArgsWrapper(expr.FnName.L, len(expr.Args)); err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
str, err := restoreFuncCall(expr)
|
|
if err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
col.DefaultIsExpr = true
|
|
return str, false, nil
|
|
case ast.DateFormat: // DATE_FORMAT()
|
|
if err := expression.VerifyArgsWrapper(expr.FnName.L, len(expr.Args)); err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
// Support DATE_FORMAT(NOW(),'%Y-%m'), DATE_FORMAT(NOW(),'%Y-%m-%d'),
|
|
// DATE_FORMAT(NOW(),'%Y-%m-%d %H.%i.%s'), DATE_FORMAT(NOW(),'%Y-%m-%d %H:%i:%s').
|
|
nowFunc, ok := expr.Args[0].(*ast.FuncCallExpr)
|
|
if ok && nowFunc.FnName.L == ast.Now {
|
|
if err := expression.VerifyArgsWrapper(nowFunc.FnName.L, len(nowFunc.Args)); err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
valExpr, isValue := expr.Args[1].(ast.ValueExpr)
|
|
if !isValue || (valExpr.GetString() != "%Y-%m" && valExpr.GetString() != "%Y-%m-%d" &&
|
|
valExpr.GetString() != "%Y-%m-%d %H.%i.%s" && valExpr.GetString() != "%Y-%m-%d %H:%i:%s") {
|
|
return nil, false, dbterror.ErrDefValGeneratedNamedFunctionIsNotAllowed.GenWithStackByArgs(col.Name.String(), valExpr)
|
|
}
|
|
str, err := restoreFuncCall(expr)
|
|
if err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
col.DefaultIsExpr = true
|
|
return str, false, nil
|
|
}
|
|
return nil, false, dbterror.ErrDefValGeneratedNamedFunctionIsNotAllowed.GenWithStackByArgs(col.Name.String(),
|
|
fmt.Sprintf("%s with disallowed args", expr.FnName.String()))
|
|
case ast.Replace:
|
|
if err := expression.VerifyArgsWrapper(expr.FnName.L, len(expr.Args)); err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
funcCall := expr.Args[0]
|
|
// Support REPLACE(CONVERT(UPPER(UUID()) USING UTF8MB4), '-', ''))
|
|
if convertFunc, ok := funcCall.(*ast.FuncCallExpr); ok && convertFunc.FnName.L == ast.Convert {
|
|
if err := expression.VerifyArgsWrapper(convertFunc.FnName.L, len(convertFunc.Args)); err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
funcCall = convertFunc.Args[0]
|
|
}
|
|
// Support REPLACE(UPPER(UUID()), '-', '').
|
|
if upperFunc, ok := funcCall.(*ast.FuncCallExpr); ok && upperFunc.FnName.L == ast.Upper {
|
|
if err := expression.VerifyArgsWrapper(upperFunc.FnName.L, len(upperFunc.Args)); err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
if uuidFunc, ok := upperFunc.Args[0].(*ast.FuncCallExpr); ok && uuidFunc.FnName.L == ast.UUID {
|
|
if err := expression.VerifyArgsWrapper(uuidFunc.FnName.L, len(uuidFunc.Args)); err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
str, err := restoreFuncCall(expr)
|
|
if err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
col.DefaultIsExpr = true
|
|
return str, false, nil
|
|
}
|
|
}
|
|
return nil, false, dbterror.ErrDefValGeneratedNamedFunctionIsNotAllowed.GenWithStackByArgs(col.Name.String(),
|
|
fmt.Sprintf("%s with disallowed args", expr.FnName.String()))
|
|
case ast.Upper:
|
|
if err := expression.VerifyArgsWrapper(expr.FnName.L, len(expr.Args)); err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
// Support UPPER(SUBSTRING_INDEX(USER(), '@', 1)).
|
|
if substringIndexFunc, ok := expr.Args[0].(*ast.FuncCallExpr); ok && substringIndexFunc.FnName.L == ast.SubstringIndex {
|
|
if err := expression.VerifyArgsWrapper(substringIndexFunc.FnName.L, len(substringIndexFunc.Args)); err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
if userFunc, ok := substringIndexFunc.Args[0].(*ast.FuncCallExpr); ok && userFunc.FnName.L == ast.User {
|
|
if err := expression.VerifyArgsWrapper(userFunc.FnName.L, len(userFunc.Args)); err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
valExpr, isValue := substringIndexFunc.Args[1].(ast.ValueExpr)
|
|
if !isValue || valExpr.GetString() != "@" {
|
|
return nil, false, dbterror.ErrDefValGeneratedNamedFunctionIsNotAllowed.GenWithStackByArgs(col.Name.String(), valExpr)
|
|
}
|
|
str, err := restoreFuncCall(expr)
|
|
if err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
col.DefaultIsExpr = true
|
|
return str, false, nil
|
|
}
|
|
}
|
|
return nil, false, dbterror.ErrDefValGeneratedNamedFunctionIsNotAllowed.GenWithStackByArgs(col.Name.String(),
|
|
fmt.Sprintf("%s with disallowed args", expr.FnName.String()))
|
|
case ast.StrToDate: // STR_TO_DATE()
|
|
if err := expression.VerifyArgsWrapper(expr.FnName.L, len(expr.Args)); err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
// Support STR_TO_DATE('1980-01-01', '%Y-%m-%d').
|
|
if _, ok1 := expr.Args[0].(ast.ValueExpr); ok1 {
|
|
if _, ok2 := expr.Args[1].(ast.ValueExpr); ok2 {
|
|
str, err := restoreFuncCall(expr)
|
|
if err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
col.DefaultIsExpr = true
|
|
return str, false, nil
|
|
}
|
|
}
|
|
return nil, false, dbterror.ErrDefValGeneratedNamedFunctionIsNotAllowed.GenWithStackByArgs(col.Name.String(),
|
|
fmt.Sprintf("%s with disallowed args", expr.FnName.String()))
|
|
case ast.JSONObject, ast.JSONArray, ast.JSONQuote: // JSON_OBJECT(), JSON_ARRAY(), JSON_QUOTE()
|
|
if err := expression.VerifyArgsWrapper(expr.FnName.L, len(expr.Args)); err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
str, err := restoreFuncCall(expr)
|
|
if err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
col.DefaultIsExpr = true
|
|
return str, false, nil
|
|
|
|
case ast.VecFromText:
|
|
if err := expression.VerifyArgsWrapper(expr.FnName.L, len(expr.Args)); err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
str, err := restoreFuncCall(expr)
|
|
if err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
col.DefaultIsExpr = true
|
|
return str, false, nil
|
|
|
|
default:
|
|
return nil, false, dbterror.ErrDefValGeneratedNamedFunctionIsNotAllowed.GenWithStackByArgs(col.Name.String(), expr.FnName.String())
|
|
}
|
|
}
|
|
|
|
// getDefaultValue will get the default value for column.
|
|
// 1: get the expr restored string for the column which uses sequence next value as default value.
|
|
// 2: get specific default value for the other column.
|
|
func getDefaultValue(ctx exprctx.BuildContext, col *table.Column, option *ast.ColumnOption) (any, bool, error) {
|
|
// handle default value with function call
|
|
tp, fsp := col.FieldType.GetType(), col.FieldType.GetDecimal()
|
|
if x, ok := option.Expr.(*ast.FuncCallExpr); ok {
|
|
val, isSeqExpr, err := getFuncCallDefaultValue(col, option, x)
|
|
if val != nil || isSeqExpr || err != nil {
|
|
return val, isSeqExpr, err
|
|
}
|
|
// If the function call is ast.CurrentTimestamp, it needs to be continuously processed.
|
|
}
|
|
|
|
if tp == mysql.TypeTimestamp || tp == mysql.TypeDatetime || tp == mysql.TypeDate {
|
|
vd, err := expression.GetTimeValue(ctx, option.Expr, tp, fsp, nil)
|
|
value := vd.GetValue()
|
|
if err != nil {
|
|
return nil, false, dbterror.ErrInvalidDefaultValue.GenWithStackByArgs(col.Name.O)
|
|
}
|
|
|
|
// Value is nil means `default null`.
|
|
if value == nil {
|
|
return nil, false, nil
|
|
}
|
|
|
|
// If value is types.Time, convert it to string.
|
|
if vv, ok := value.(types.Time); ok {
|
|
return vv.String(), false, nil
|
|
}
|
|
|
|
return value, false, nil
|
|
}
|
|
|
|
// evaluate the non-function-call expr to a certain value.
|
|
v, err := expression.EvalSimpleAst(ctx, option.Expr)
|
|
if err != nil {
|
|
return nil, false, errors.Trace(err)
|
|
}
|
|
|
|
if v.IsNull() {
|
|
return nil, false, nil
|
|
}
|
|
|
|
if v.Kind() == types.KindBinaryLiteral || v.Kind() == types.KindMysqlBit {
|
|
if types.IsTypeBlob(tp) || tp == mysql.TypeJSON || tp == mysql.TypeTiDBVectorFloat32 {
|
|
// BLOB/TEXT/JSON column cannot have a default value.
|
|
// Skip the unnecessary decode procedure.
|
|
return v.GetString(), false, err
|
|
}
|
|
if tp == mysql.TypeBit || tp == mysql.TypeString || tp == mysql.TypeVarchar ||
|
|
tp == mysql.TypeVarString || tp == mysql.TypeEnum || tp == mysql.TypeSet {
|
|
// For BinaryLiteral or bit fields, we decode the default value to utf8 string.
|
|
str, err := v.GetBinaryStringDecoded(types.StrictFlags, col.GetCharset())
|
|
if err != nil {
|
|
// Overwrite the decoding error with invalid default value error.
|
|
err = dbterror.ErrInvalidDefaultValue.GenWithStackByArgs(col.Name.O)
|
|
}
|
|
return str, false, err
|
|
}
|
|
// For other kind of fields (e.g. INT), we supply its integer as string value.
|
|
value, err := v.GetBinaryLiteral().ToInt(ctx.GetEvalCtx().TypeCtx())
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
return strconv.FormatUint(value, 10), false, nil
|
|
}
|
|
|
|
switch tp {
|
|
case mysql.TypeSet:
|
|
val, err := getSetDefaultValue(v, col)
|
|
return val, false, err
|
|
case mysql.TypeEnum:
|
|
val, err := getEnumDefaultValue(v, col)
|
|
return val, false, err
|
|
case mysql.TypeDuration, mysql.TypeDate:
|
|
if v, err = v.ConvertTo(ctx.GetEvalCtx().TypeCtx(), &col.FieldType); err != nil {
|
|
return "", false, errors.Trace(err)
|
|
}
|
|
case mysql.TypeBit:
|
|
if v.Kind() == types.KindInt64 || v.Kind() == types.KindUint64 {
|
|
// For BIT fields, convert int into BinaryLiteral.
|
|
return types.NewBinaryLiteralFromUint(v.GetUint64(), -1).ToString(), false, nil
|
|
}
|
|
case mysql.TypeTiny, mysql.TypeShort, mysql.TypeInt24, mysql.TypeLong, mysql.TypeLonglong, mysql.TypeFloat, mysql.TypeDouble:
|
|
// For these types, convert it to standard format firstly.
|
|
// like integer fields, convert it into integer string literals. like convert "1.25" into "1" and "2.8" into "3".
|
|
// if raise a error, we will use original expression. We will handle it in check phase
|
|
if temp, err := v.ConvertTo(ctx.GetEvalCtx().TypeCtx(), &col.FieldType); err == nil {
|
|
v = temp
|
|
}
|
|
}
|
|
|
|
val, err := v.ToString()
|
|
return val, false, err
|
|
}
|
|
|
|
func getSequenceDefaultValue(c *ast.ColumnOption) (expr string, err error) {
|
|
var sb strings.Builder
|
|
restoreFlags := format.RestoreStringSingleQuotes | format.RestoreKeyWordLowercase | format.RestoreNameBackQuotes |
|
|
format.RestoreSpacesAroundBinaryOperation
|
|
restoreCtx := format.NewRestoreCtx(restoreFlags, &sb)
|
|
if err := c.Expr.Restore(restoreCtx); err != nil {
|
|
return "", err
|
|
}
|
|
return sb.String(), nil
|
|
}
|
|
|
|
func setDefaultValueWithBinaryPadding(col *table.Column, value any) error {
|
|
err := col.SetDefaultValue(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// https://dev.mysql.com/doc/refman/8.0/en/binary-varbinary.html
|
|
// Set the default value for binary type should append the paddings.
|
|
if value != nil {
|
|
if col.GetType() == mysql.TypeString && types.IsBinaryStr(&col.FieldType) && len(value.(string)) < col.GetFlen() {
|
|
padding := make([]byte, col.GetFlen()-len(value.(string)))
|
|
col.DefaultValue = string(append([]byte(col.DefaultValue.(string)), padding...))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func setColumnComment(ctx expression.BuildContext, col *table.Column, option *ast.ColumnOption) error {
|
|
value, err := expression.EvalSimpleAst(ctx, option.Expr)
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
if col.Comment, err = value.ToString(); err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
|
|
evalCtx := ctx.GetEvalCtx()
|
|
col.Comment, err = validateCommentLength(evalCtx.ErrCtx(), evalCtx.SQLMode(), col.Name.L, &col.Comment, dbterror.ErrTooLongFieldComment)
|
|
return errors.Trace(err)
|
|
}
|
|
|
|
func processAndCheckDefaultValueAndColumn(ctx expression.BuildContext, col *table.Column,
|
|
outPriKeyConstraint *ast.Constraint, hasDefaultValue, setOnUpdateNow, hasNullFlag bool) error {
|
|
processDefaultValue(col, hasDefaultValue, setOnUpdateNow)
|
|
processColumnFlags(col)
|
|
|
|
err := checkPriKeyConstraint(col, hasDefaultValue, hasNullFlag, outPriKeyConstraint)
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
if err = checkColumnValueConstraint(col, col.GetCollate()); err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
if err = checkDefaultValue(ctx, col, hasDefaultValue); err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
if err = checkColumnFieldLength(col); err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func restoreFuncCall(expr *ast.FuncCallExpr) (string, error) {
|
|
var sb strings.Builder
|
|
restoreFlags := format.RestoreStringSingleQuotes | format.RestoreKeyWordLowercase | format.RestoreNameBackQuotes |
|
|
format.RestoreSpacesAroundBinaryOperation
|
|
restoreCtx := format.NewRestoreCtx(restoreFlags, &sb)
|
|
if err := expr.Restore(restoreCtx); err != nil {
|
|
return "", err
|
|
}
|
|
return sb.String(), nil
|
|
}
|
|
|
|
// getSetDefaultValue gets the default value for the set type. See https://dev.mysql.com/doc/refman/5.7/en/set.html.
|
|
func getSetDefaultValue(v types.Datum, col *table.Column) (string, error) {
|
|
if v.Kind() == types.KindInt64 {
|
|
setCnt := len(col.GetElems())
|
|
maxLimit := int64(1<<uint(setCnt) - 1)
|
|
val := v.GetInt64()
|
|
if val < 1 || val > maxLimit {
|
|
return "", dbterror.ErrInvalidDefaultValue.GenWithStackByArgs(col.Name.O)
|
|
}
|
|
setVal, err := types.ParseSetValue(col.GetElems(), uint64(val))
|
|
if err != nil {
|
|
return "", errors.Trace(err)
|
|
}
|
|
v.SetMysqlSet(setVal, col.GetCollate())
|
|
return v.ToString()
|
|
}
|
|
|
|
str, err := v.ToString()
|
|
if err != nil {
|
|
return "", errors.Trace(err)
|
|
}
|
|
if str == "" {
|
|
return str, nil
|
|
}
|
|
setVal, err := types.ParseSetName(col.GetElems(), str, col.GetCollate())
|
|
if err != nil {
|
|
return "", dbterror.ErrInvalidDefaultValue.GenWithStackByArgs(col.Name.O)
|
|
}
|
|
v.SetMysqlSet(setVal, col.GetCollate())
|
|
|
|
return v.ToString()
|
|
}
|
|
|
|
// getEnumDefaultValue gets the default value for the enum type. See https://dev.mysql.com/doc/refman/5.7/en/enum.html.
|
|
func getEnumDefaultValue(v types.Datum, col *table.Column) (string, error) {
|
|
if v.Kind() == types.KindInt64 {
|
|
val := v.GetInt64()
|
|
if val < 1 || val > int64(len(col.GetElems())) {
|
|
return "", dbterror.ErrInvalidDefaultValue.GenWithStackByArgs(col.Name.O)
|
|
}
|
|
enumVal, err := types.ParseEnumValue(col.GetElems(), uint64(val))
|
|
if err != nil {
|
|
return "", errors.Trace(err)
|
|
}
|
|
v.SetMysqlEnum(enumVal, col.GetCollate())
|
|
return v.ToString()
|
|
}
|
|
str, err := v.ToString()
|
|
if err != nil {
|
|
return "", errors.Trace(err)
|
|
}
|
|
// Ref: https://dev.mysql.com/doc/refman/8.0/en/enum.html
|
|
// Trailing spaces are automatically deleted from ENUM member values in the table definition when a table is created.
|
|
str = strings.TrimRight(str, " ")
|
|
enumVal, err := types.ParseEnumName(col.GetElems(), str, col.GetCollate())
|
|
if err != nil {
|
|
return "", dbterror.ErrInvalidDefaultValue.GenWithStackByArgs(col.Name.O)
|
|
}
|
|
v.SetMysqlEnum(enumVal, col.GetCollate())
|
|
|
|
return v.ToString()
|
|
}
|
|
|
|
func removeOnUpdateNowFlag(c *table.Column) {
|
|
// For timestamp Col, if it is set null or default value,
|
|
// OnUpdateNowFlag should be removed.
|
|
if mysql.HasTimestampFlag(c.GetFlag()) {
|
|
c.DelFlag(mysql.OnUpdateNowFlag)
|
|
}
|
|
}
|
|
|
|
func processDefaultValue(c *table.Column, hasDefaultValue bool, setOnUpdateNow bool) {
|
|
setTimestampDefaultValue(c, hasDefaultValue, setOnUpdateNow)
|
|
|
|
setYearDefaultValue(c, hasDefaultValue)
|
|
|
|
// Set `NoDefaultValueFlag` if this field doesn't have a default value and
|
|
// it is `not null` and not an `AUTO_INCREMENT` field or `TIMESTAMP` field.
|
|
setNoDefaultValueFlag(c, hasDefaultValue)
|
|
}
|
|
|
|
func setYearDefaultValue(c *table.Column, hasDefaultValue bool) {
|
|
if hasDefaultValue {
|
|
return
|
|
}
|
|
|
|
if c.GetType() == mysql.TypeYear && mysql.HasNotNullFlag(c.GetFlag()) {
|
|
if err := c.SetDefaultValue("0000"); err != nil {
|
|
logutil.DDLLogger().Error("set default value failed", zap.Error(err))
|
|
}
|
|
}
|
|
}
|
|
|
|
func setTimestampDefaultValue(c *table.Column, hasDefaultValue bool, setOnUpdateNow bool) {
|
|
if hasDefaultValue {
|
|
return
|
|
}
|
|
|
|
// For timestamp Col, if is not set default value or not set null, use current timestamp.
|
|
if mysql.HasTimestampFlag(c.GetFlag()) && mysql.HasNotNullFlag(c.GetFlag()) {
|
|
if setOnUpdateNow {
|
|
if err := c.SetDefaultValue(types.ZeroDatetimeStr); err != nil {
|
|
logutil.DDLLogger().Error("set default value failed", zap.Error(err))
|
|
}
|
|
} else {
|
|
if err := c.SetDefaultValue(strings.ToUpper(ast.CurrentTimestamp)); err != nil {
|
|
logutil.DDLLogger().Error("set default value failed", zap.Error(err))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func setNoDefaultValueFlag(c *table.Column, hasDefaultValue bool) {
|
|
if hasDefaultValue {
|
|
return
|
|
}
|
|
|
|
if !mysql.HasNotNullFlag(c.GetFlag()) {
|
|
return
|
|
}
|
|
|
|
// Check if it is an `AUTO_INCREMENT` field or `TIMESTAMP` field.
|
|
if !mysql.HasAutoIncrementFlag(c.GetFlag()) && !mysql.HasTimestampFlag(c.GetFlag()) {
|
|
c.AddFlag(mysql.NoDefaultValueFlag)
|
|
}
|
|
}
|
|
|
|
func checkDefaultValue(ctx exprctx.BuildContext, c *table.Column, hasDefaultValue bool) (err error) {
|
|
if !hasDefaultValue {
|
|
return nil
|
|
}
|
|
|
|
if c.GetDefaultValue() != nil {
|
|
if c.DefaultIsExpr {
|
|
if mysql.HasAutoIncrementFlag(c.GetFlag()) {
|
|
return types.ErrInvalidDefault.GenWithStackByArgs(c.Name)
|
|
}
|
|
return nil
|
|
}
|
|
_, err = table.GetColDefaultValue(
|
|
exprctx.CtxWithHandleTruncateErrLevel(ctx, errctx.LevelError),
|
|
c.ToInfo(),
|
|
)
|
|
if err != nil {
|
|
return types.ErrInvalidDefault.GenWithStackByArgs(c.Name)
|
|
}
|
|
return nil
|
|
}
|
|
// Primary key default null is invalid.
|
|
if mysql.HasPriKeyFlag(c.GetFlag()) {
|
|
return dbterror.ErrPrimaryCantHaveNull
|
|
}
|
|
|
|
// Set not null but default null is invalid.
|
|
if mysql.HasNotNullFlag(c.GetFlag()) {
|
|
return types.ErrInvalidDefault.GenWithStackByArgs(c.Name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func checkColumnFieldLength(col *table.Column) error {
|
|
if col.GetType() == mysql.TypeVarchar {
|
|
if err := types.IsVarcharTooBigFieldLength(col.GetFlen(), col.Name.O, col.GetCharset()); err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkPriKeyConstraint check all parts of a PRIMARY KEY must be NOT NULL
|
|
func checkPriKeyConstraint(col *table.Column, hasDefaultValue, hasNullFlag bool, outPriKeyConstraint *ast.Constraint) error {
|
|
// Primary key should not be null.
|
|
if mysql.HasPriKeyFlag(col.GetFlag()) && hasDefaultValue && col.GetDefaultValue() == nil {
|
|
return types.ErrInvalidDefault.GenWithStackByArgs(col.Name)
|
|
}
|
|
// Set primary key flag for outer primary key constraint.
|
|
// Such as: create table t1 (id int , age int, primary key(id))
|
|
if !mysql.HasPriKeyFlag(col.GetFlag()) && outPriKeyConstraint != nil {
|
|
for _, key := range outPriKeyConstraint.Keys {
|
|
if key.Expr == nil && key.Column.Name.L != col.Name.L {
|
|
continue
|
|
}
|
|
col.AddFlag(mysql.PriKeyFlag)
|
|
break
|
|
}
|
|
}
|
|
// Primary key should not be null.
|
|
if mysql.HasPriKeyFlag(col.GetFlag()) && hasNullFlag {
|
|
return dbterror.ErrPrimaryCantHaveNull
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checkColumnValueConstraint(col *table.Column, collation string) error {
|
|
if col.GetType() != mysql.TypeEnum && col.GetType() != mysql.TypeSet {
|
|
return nil
|
|
}
|
|
valueMap := make(map[string]bool, len(col.GetElems()))
|
|
ctor := collate.GetCollator(collation)
|
|
enumLengthLimit := config.GetGlobalConfig().EnableEnumLengthLimit
|
|
desc, err := charset.GetCharsetInfo(col.GetCharset())
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
for i := range col.GetElems() {
|
|
val := string(ctor.Key(col.GetElems()[i]))
|
|
// According to MySQL 8.0 Refman:
|
|
// The maximum supported length of an individual ENUM element is M <= 255 and (M x w) <= 1020,
|
|
// where M is the element literal length and w is the number of bytes required for the maximum-length character in the character set.
|
|
// See https://dev.mysql.com/doc/refman/8.0/en/string-type-syntax.html for more details.
|
|
if enumLengthLimit && (len(val) > 255 || len(val)*desc.Maxlen > 1020) {
|
|
return dbterror.ErrTooLongValueForType.GenWithStackByArgs(col.Name)
|
|
}
|
|
if _, ok := valueMap[val]; ok {
|
|
tpStr := "ENUM"
|
|
if col.GetType() == mysql.TypeSet {
|
|
tpStr = "SET"
|
|
}
|
|
return types.ErrDuplicatedValueInType.GenWithStackByArgs(col.Name, col.GetElems()[i], tpStr)
|
|
}
|
|
valueMap[val] = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkColumnDefaultValue checks the default value of the column.
|
|
// In non-strict SQL mode, if the default value of the column is an empty string, the default value can be ignored.
|
|
// In strict SQL mode, TEXT/BLOB/JSON can't have not null default values.
|
|
// In NO_ZERO_DATE SQL mode, TIMESTAMP/DATE/DATETIME type can't have zero date like '0000-00-00' or '0000-00-00 00:00:00'.
|
|
func checkColumnDefaultValue(ctx exprctx.BuildContext, col *table.Column, value any) (bool, any, error) {
|
|
hasDefaultValue := true
|
|
|
|
if value != nil && col.GetType() == mysql.TypeTiDBVectorFloat32 {
|
|
// In any SQL mode we don't allow VECTOR column to have a default value.
|
|
// Note that expression default is still supported.
|
|
return hasDefaultValue, value, errors.Errorf("VECTOR column '%-.192s' can't have a literal default. Use expression default instead: ((VEC_FROM_TEXT('...')))", col.Name.O)
|
|
}
|
|
if value != nil && (col.GetType() == mysql.TypeJSON ||
|
|
col.GetType() == mysql.TypeTinyBlob || col.GetType() == mysql.TypeMediumBlob ||
|
|
col.GetType() == mysql.TypeLongBlob || col.GetType() == mysql.TypeBlob) {
|
|
// In non-strict SQL mode.
|
|
if !ctx.GetEvalCtx().SQLMode().HasStrictMode() && value == "" {
|
|
if col.GetType() == mysql.TypeBlob || col.GetType() == mysql.TypeLongBlob {
|
|
// The TEXT/BLOB default value can be ignored.
|
|
hasDefaultValue = false
|
|
}
|
|
// In non-strict SQL mode, if the column type is json and the default value is null, it is initialized to an empty array.
|
|
if col.GetType() == mysql.TypeJSON {
|
|
value = `null`
|
|
}
|
|
ctx.GetEvalCtx().AppendWarning(dbterror.ErrBlobCantHaveDefault.FastGenByArgs(col.Name.O))
|
|
return hasDefaultValue, value, nil
|
|
}
|
|
// In strict SQL mode or default value is not an empty string.
|
|
return hasDefaultValue, value, dbterror.ErrBlobCantHaveDefault.GenWithStackByArgs(col.Name.O)
|
|
}
|
|
if value != nil && ctx.GetEvalCtx().SQLMode().HasNoZeroDateMode() &&
|
|
ctx.GetEvalCtx().SQLMode().HasStrictMode() && types.IsTypeTime(col.GetType()) {
|
|
if vv, ok := value.(string); ok {
|
|
timeValue, err := expression.GetTimeValue(ctx, vv, col.GetType(), col.GetDecimal(), nil)
|
|
if err != nil {
|
|
return hasDefaultValue, value, errors.Trace(err)
|
|
}
|
|
if timeValue.GetMysqlTime().CoreTime() == types.ZeroCoreTime {
|
|
return hasDefaultValue, value, types.ErrInvalidDefault.GenWithStackByArgs(col.Name.O)
|
|
}
|
|
}
|
|
}
|
|
if value != nil && col.GetType() == mysql.TypeBit {
|
|
v, ok := value.(string)
|
|
if !ok {
|
|
return hasDefaultValue, value, types.ErrInvalidDefault.GenWithStackByArgs(col.Name.O)
|
|
}
|
|
|
|
uintVal, err := types.BinaryLiteral(v).ToInt(ctx.GetEvalCtx().TypeCtx())
|
|
if err != nil {
|
|
return hasDefaultValue, value, types.ErrInvalidDefault.GenWithStackByArgs(col.Name.O)
|
|
}
|
|
intest.Assert(col.GetFlen() > 0 && col.GetFlen() <= 64)
|
|
if col.GetFlen() < 64 && uintVal >= 1<<(uint64(col.GetFlen())) {
|
|
return hasDefaultValue, value, types.ErrInvalidDefault.GenWithStackByArgs(col.Name.O)
|
|
}
|
|
}
|
|
return hasDefaultValue, value, nil
|
|
}
|
|
|
|
func checkSequenceDefaultValue(col *table.Column) error {
|
|
if mysql.IsIntegerType(col.GetType()) {
|
|
return nil
|
|
}
|
|
return dbterror.ErrColumnTypeUnsupportedNextValue.GenWithStackByArgs(col.ColumnInfo.Name.O)
|
|
}
|
|
|
|
func convertTimestampDefaultValToUTC(tc types.Context, defaultVal any, col *table.Column) (any, error) {
|
|
if defaultVal == nil || col.GetType() != mysql.TypeTimestamp {
|
|
return defaultVal, nil
|
|
}
|
|
if vv, ok := defaultVal.(string); ok {
|
|
if vv != types.ZeroDatetimeStr && !strings.EqualFold(vv, ast.CurrentTimestamp) {
|
|
t, err := types.ParseTime(tc, vv, col.GetType(), col.GetDecimal())
|
|
if err != nil {
|
|
return defaultVal, errors.Trace(err)
|
|
}
|
|
err = t.ConvertTimeZone(tc.Location(), time.UTC)
|
|
if err != nil {
|
|
return defaultVal, errors.Trace(err)
|
|
}
|
|
defaultVal = t.String()
|
|
}
|
|
}
|
|
return defaultVal, nil
|
|
}
|
|
|
|
// processColumnFlags is used by columnDefToCol and processColumnOptions. It is intended to unify behaviors on `create/add` and `modify/change` statements. Check tidb#issue#19342.
|
|
func processColumnFlags(col *table.Column) {
|
|
if col.FieldType.EvalType().IsStringKind() {
|
|
if col.GetCharset() == charset.CharsetBin {
|
|
col.AddFlag(mysql.BinaryFlag)
|
|
} else {
|
|
col.DelFlag(mysql.BinaryFlag)
|
|
}
|
|
}
|
|
if col.GetType() == mysql.TypeBit {
|
|
// For BIT field, it's charset is binary but does not have binary flag.
|
|
col.DelFlag(mysql.BinaryFlag)
|
|
col.AddFlag(mysql.UnsignedFlag)
|
|
}
|
|
if col.GetType() == mysql.TypeYear {
|
|
// For Year field, it's charset is binary but does not have binary flag.
|
|
col.DelFlag(mysql.BinaryFlag)
|
|
col.AddFlag(mysql.ZerofillFlag)
|
|
}
|
|
|
|
// If you specify ZEROFILL for a numeric column, MySQL automatically adds the UNSIGNED attribute to the column.
|
|
// See https://dev.mysql.com/doc/refman/5.7/en/numeric-type-overview.html for more details.
|
|
// But some types like bit and year, won't show its unsigned flag in `show create table`.
|
|
if mysql.HasZerofillFlag(col.GetFlag()) {
|
|
col.AddFlag(mysql.UnsignedFlag)
|
|
}
|
|
}
|
|
|
|
func adjustBlobTypesFlen(tp *types.FieldType, colCharset string) error {
|
|
cs, err := charset.GetCharsetInfo(colCharset)
|
|
// when we meet the unsupported charset, we do not adjust.
|
|
if err != nil {
|
|
return err
|
|
}
|
|
l := tp.GetFlen() * cs.Maxlen
|
|
if tp.GetType() == mysql.TypeBlob {
|
|
if l <= tinyBlobMaxLength {
|
|
logutil.DDLLogger().Info(fmt.Sprintf("Automatically convert BLOB(%d) to TINYBLOB", tp.GetFlen()))
|
|
tp.SetFlen(tinyBlobMaxLength)
|
|
tp.SetType(mysql.TypeTinyBlob)
|
|
} else if l <= blobMaxLength {
|
|
tp.SetFlen(blobMaxLength)
|
|
} else if l <= mediumBlobMaxLength {
|
|
logutil.DDLLogger().Info(fmt.Sprintf("Automatically convert BLOB(%d) to MEDIUMBLOB", tp.GetFlen()))
|
|
tp.SetFlen(mediumBlobMaxLength)
|
|
tp.SetType(mysql.TypeMediumBlob)
|
|
} else if l <= longBlobMaxLength {
|
|
logutil.DDLLogger().Info(fmt.Sprintf("Automatically convert BLOB(%d) to LONGBLOB", tp.GetFlen()))
|
|
tp.SetFlen(longBlobMaxLength)
|
|
tp.SetType(mysql.TypeLongBlob)
|
|
}
|
|
}
|
|
return nil
|
|
}
|