diff --git a/ddl/db_partition_test.go b/ddl/db_partition_test.go index 16d2f22214925..51a75e3401a26 100644 --- a/ddl/db_partition_test.go +++ b/ddl/db_partition_test.go @@ -336,6 +336,10 @@ partition by range (a) partition p0 values less than (200), partition p1 values less than (300), partition p2 values less than maxvalue)`) + + // Fix https://github.com/pingcap/tidb/issues/35827 + tk.MustExec(`create table t37 (id tinyint unsigned, idpart tinyint, i varchar(255)) partition by range (idpart) (partition p1 values less than (-1));`) + tk.MustGetErrCode(`create table t38 (id tinyint unsigned, idpart tinyint unsigned, i varchar(255)) partition by range (idpart) (partition p1 values less than (-1));`, errno.ErrPartitionConstDomain) } func TestCreateTableWithHashPartition(t *testing.T) { diff --git a/ddl/partition.go b/ddl/partition.go index d024ef47b4b43..bad63c95fafd2 100644 --- a/ddl/partition.go +++ b/ddl/partition.go @@ -51,6 +51,7 @@ import ( "github.com/pingcap/tidb/util/hack" "github.com/pingcap/tidb/util/logutil" "github.com/pingcap/tidb/util/mathutil" + "github.com/pingcap/tidb/util/mock" "github.com/pingcap/tidb/util/slice" "github.com/pingcap/tidb/util/sqlexec" "github.com/tikv/client-go/v2/tikv" @@ -478,6 +479,545 @@ func buildTablePartitionInfo(ctx sessionctx.Context, s *ast.PartitionOptions, tb } tbInfo.Partition.Definitions = defs +<<<<<<< HEAD +======= + + if s.Interval != nil { + // Syntactic sugar for INTERVAL partitioning + // Generate the resulting CREATE TABLE as the query string + query, ok := ctx.Value(sessionctx.QueryString).(string) + if ok { + sqlMode := ctx.GetSessionVars().SQLMode + var buf bytes.Buffer + AppendPartitionDefs(tbInfo.Partition, &buf, sqlMode) + + syntacticSugar := s.Interval.OriginalText() + syntacticStart := s.Interval.OriginTextPosition() + newQuery := query[:syntacticStart] + "(" + buf.String() + ")" + query[syntacticStart+len(syntacticSugar):] + ctx.SetValue(sessionctx.QueryString, newQuery) + } + } + return nil +} + +// getPartitionIntervalFromTable checks if a partitioned table matches a generated INTERVAL partitioned scheme +// will return nil if error occurs, i.e. not an INTERVAL partitioned table +func getPartitionIntervalFromTable(ctx sessionctx.Context, tbInfo *model.TableInfo) *ast.PartitionInterval { + if tbInfo.Partition == nil || + tbInfo.Partition.Type != model.PartitionTypeRange { + return nil + } + if len(tbInfo.Partition.Columns) > 1 { + // Multi-column RANGE COLUMNS is not supported with INTERVAL + return nil + } + if len(tbInfo.Partition.Definitions) < 2 { + // Must have at least two partitions to calculate an INTERVAL + return nil + } + + var ( + interval ast.PartitionInterval + startIdx int = 0 + endIdx int = len(tbInfo.Partition.Definitions) - 1 + isIntType bool = true + minVal string = "0" + ) + if len(tbInfo.Partition.Columns) > 0 { + partCol := findColumnByName(tbInfo.Partition.Columns[0].L, tbInfo) + if partCol.FieldType.EvalType() == types.ETInt { + min := getLowerBoundInt(partCol) + minVal = strconv.FormatInt(min, 10) + } else if partCol.FieldType.EvalType() == types.ETDatetime { + isIntType = false + minVal = "0000-01-01" + } else { + // Only INT and Datetime columns are supported for INTERVAL partitioning + return nil + } + } else { + if !isPartExprUnsigned(tbInfo) { + minVal = "-9223372036854775808" + } + } + + // Check if possible null partition + firstPartLessThan := driver.UnwrapFromSingleQuotes(tbInfo.Partition.Definitions[0].LessThan[0]) + if strings.EqualFold(firstPartLessThan, minVal) { + interval.NullPart = true + startIdx++ + firstPartLessThan = driver.UnwrapFromSingleQuotes(tbInfo.Partition.Definitions[startIdx].LessThan[0]) + } + // flag if MAXVALUE partition + lastPartLessThan := driver.UnwrapFromSingleQuotes(tbInfo.Partition.Definitions[endIdx].LessThan[0]) + if strings.EqualFold(lastPartLessThan, partitionMaxValue) { + interval.MaxValPart = true + endIdx-- + lastPartLessThan = driver.UnwrapFromSingleQuotes(tbInfo.Partition.Definitions[endIdx].LessThan[0]) + } + // Guess the interval + if startIdx >= endIdx { + // Must have at least two partitions to calculate an INTERVAL + return nil + } + var firstExpr, lastExpr ast.ExprNode + if isIntType { + exprStr := fmt.Sprintf("((%s) - (%s)) DIV %d", lastPartLessThan, firstPartLessThan, endIdx-startIdx) + exprs, err := expression.ParseSimpleExprsWithNames(ctx, exprStr, nil, nil) + if err != nil { + return nil + } + val, isNull, err := exprs[0].EvalInt(ctx, chunk.Row{}) + if isNull || err != nil || val < 1 { + // If NULL, error or interval < 1 then cannot be an INTERVAL partitioned table + return nil + } + interval.IntervalExpr.Expr = ast.NewValueExpr(val, "", "") + interval.IntervalExpr.TimeUnit = ast.TimeUnitInvalid + firstExpr, err = astIntValueExprFromStr(firstPartLessThan, minVal == "0") + if err != nil { + return nil + } + interval.FirstRangeEnd = &firstExpr + lastExpr, err = astIntValueExprFromStr(lastPartLessThan, minVal == "0") + if err != nil { + return nil + } + interval.LastRangeEnd = &lastExpr + } else { // types.ETDatetime + exprStr := fmt.Sprintf("TIMESTAMPDIFF(SECOND, '%s', '%s')", firstPartLessThan, lastPartLessThan) + exprs, err := expression.ParseSimpleExprsWithNames(ctx, exprStr, nil, nil) + if err != nil { + return nil + } + val, isNull, err := exprs[0].EvalInt(ctx, chunk.Row{}) + if isNull || err != nil || val < 1 { + // If NULL, error or interval < 1 then cannot be an INTERVAL partitioned table + return nil + } + + // This will not find all matches > 28 days, since INTERVAL 1 MONTH can generate + // 2022-01-31, 2022-02-28, 2022-03-31 etc. so we just assume that if there is a + // diff >= 28 days, we will try with Month and not retry with something else... + i := val / int64(endIdx-startIdx) + if i < (28 * 24 * 60 * 60) { + // Since it is not stored or displayed, non need to try Minute..Week! + interval.IntervalExpr.Expr = ast.NewValueExpr(i, "", "") + interval.IntervalExpr.TimeUnit = ast.TimeUnitSecond + } else { + // Since it is not stored or displayed, non need to try to match Quarter or Year! + if (endIdx - startIdx) <= 3 { + // in case February is in the range + i = i / (28 * 24 * 60 * 60) + } else { + // This should be good for intervals up to 5 years + i = i / (30 * 24 * 60 * 60) + } + interval.IntervalExpr.Expr = ast.NewValueExpr(i, "", "") + interval.IntervalExpr.TimeUnit = ast.TimeUnitMonth + } + + firstExpr = ast.NewValueExpr(firstPartLessThan, "", "") + lastExpr = ast.NewValueExpr(lastPartLessThan, "", "") + interval.FirstRangeEnd = &firstExpr + interval.LastRangeEnd = &lastExpr + } + + partitionMethod := ast.PartitionMethod{ + Tp: model.PartitionTypeRange, + Interval: &interval, + } + partOption := &ast.PartitionOptions{PartitionMethod: partitionMethod} + // Generate the definitions from interval, first and last + err := generatePartitionDefinitionsFromInterval(ctx, partOption, tbInfo) + if err != nil { + return nil + } + + return &interval +} + +// comparePartitionAstAndModel compares a generated *ast.PartitionOptions and a *model.PartitionInfo +func comparePartitionAstAndModel(ctx sessionctx.Context, pAst *ast.PartitionOptions, pModel *model.PartitionInfo) error { + a := pAst.Definitions + m := pModel.Definitions + if len(pAst.Definitions) != len(pModel.Definitions) { + return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs("INTERVAL partitioning: number of partitions generated != partition defined (%d != %d)", len(a), len(m)) + } + for i := range pAst.Definitions { + // Allow options to differ! (like Placement Rules) + // Allow names to differ! + + // Check MAXVALUE + maxVD := false + if strings.EqualFold(m[i].LessThan[0], partitionMaxValue) { + maxVD = true + } + generatedExpr := a[i].Clause.(*ast.PartitionDefinitionClauseLessThan).Exprs[0] + _, maxVG := generatedExpr.(*ast.MaxValueExpr) + if maxVG || maxVD { + if maxVG && maxVD { + continue + } + return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs(fmt.Sprintf("INTERVAL partitioning: MAXVALUE clause defined for partition %s differs between generated and defined", m[i].Name.O)) + } + + lessThan := m[i].LessThan[0] + if len(lessThan) > 1 && lessThan[:1] == "'" && lessThan[len(lessThan)-1:] == "'" { + lessThan = driver.UnwrapFromSingleQuotes(lessThan) + } + cmpExpr := &ast.BinaryOperationExpr{ + Op: opcode.EQ, + L: ast.NewValueExpr(lessThan, "", ""), + R: generatedExpr, + } + cmp, err := expression.EvalAstExpr(ctx, cmpExpr) + if err != nil { + return err + } + if cmp.GetInt64() != 1 { + return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs(fmt.Sprintf("INTERVAL partitioning: LESS THAN for partition %s differs between generated and defined", m[i].Name.O)) + } + } + return nil +} + +// comparePartitionDefinitions check if generated definitions are the same as the given ones +// Allow names to differ +// returns error in case of error or non-accepted difference +func comparePartitionDefinitions(ctx sessionctx.Context, a, b []*ast.PartitionDefinition) error { + if len(a) != len(b) { + return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs("number of partitions generated != partition defined (%d != %d)", len(a), len(b)) + } + for i := range a { + if len(b[i].Sub) > 0 { + return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs(fmt.Sprintf("partition %s does have unsupported subpartitions", b[i].Name.O)) + } + // TODO: We could extend the syntax to allow for table options too, like: + // CREATE TABLE t ... INTERVAL ... LAST PARTITION LESS THAN ('2015-01-01') PLACEMENT POLICY = 'cheapStorage' + // ALTER TABLE t LAST PARTITION LESS THAN ('2022-01-01') PLACEMENT POLICY 'defaultStorage' + // ALTER TABLE t LAST PARTITION LESS THAN ('2023-01-01') PLACEMENT POLICY 'fastStorage' + if len(b[i].Options) > 0 { + return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs(fmt.Sprintf("partition %s does have unsupported options", b[i].Name.O)) + } + lessThan, ok := b[i].Clause.(*ast.PartitionDefinitionClauseLessThan) + if !ok { + return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs(fmt.Sprintf("partition %s does not have the right type for LESS THAN", b[i].Name.O)) + } + definedExpr := lessThan.Exprs[0] + generatedExpr := a[i].Clause.(*ast.PartitionDefinitionClauseLessThan).Exprs[0] + _, maxVD := definedExpr.(*ast.MaxValueExpr) + _, maxVG := generatedExpr.(*ast.MaxValueExpr) + if maxVG || maxVD { + if maxVG && maxVD { + continue + } + return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs(fmt.Sprintf("partition %s differs between generated and defined for MAXVALUE", b[i].Name.O)) + } + cmpExpr := &ast.BinaryOperationExpr{ + Op: opcode.EQ, + L: definedExpr, + R: generatedExpr, + } + cmp, err := expression.EvalAstExpr(ctx, cmpExpr) + if err != nil { + return err + } + if cmp.GetInt64() != 1 { + return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs(fmt.Sprintf("partition %s differs between generated and defined for expression", b[i].Name.O)) + } + } + return nil +} + +func getLowerBoundInt(partCols ...*model.ColumnInfo) int64 { + ret := int64(0) + for _, col := range partCols { + if mysql.HasUnsignedFlag(col.FieldType.GetFlag()) { + return 0 + } + ret = mathutil.Min(ret, types.IntergerSignedLowerBound(col.GetType())) + } + return ret +} + +// generatePartitionDefinitionsFromInterval generates partition Definitions according to INTERVAL options on partOptions +func generatePartitionDefinitionsFromInterval(ctx sessionctx.Context, partOptions *ast.PartitionOptions, tbInfo *model.TableInfo) error { + if partOptions.Interval == nil { + return nil + } + if tbInfo.Partition.Type != model.PartitionTypeRange { + return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs("INTERVAL partitioning, only allowed on RANGE partitioning") + } + if len(partOptions.ColumnNames) > 1 || len(tbInfo.Partition.Columns) > 1 { + return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs("INTERVAL partitioning, does not allow RANGE COLUMNS with more than one column") + } + var partCol *model.ColumnInfo + if len(tbInfo.Partition.Columns) > 0 { + partCol = findColumnByName(tbInfo.Partition.Columns[0].L, tbInfo) + if partCol == nil { + return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs("INTERVAL partitioning, could not find any RANGE COLUMNS") + } + // Only support Datetime, date and INT column types for RANGE INTERVAL! + switch partCol.FieldType.EvalType() { + case types.ETInt, types.ETDatetime: + default: + return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs("INTERVAL partitioning, only supports Date, Datetime and INT types") + } + } + // Allow given partition definitions, but check it later! + definedPartDefs := partOptions.Definitions + partOptions.Definitions = make([]*ast.PartitionDefinition, 0, 1) + if partOptions.Interval.FirstRangeEnd == nil || partOptions.Interval.LastRangeEnd == nil { + return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs("INTERVAL partitioning, currently requires FIRST and LAST partitions to be defined") + } + switch partOptions.Interval.IntervalExpr.TimeUnit { + case ast.TimeUnitInvalid, ast.TimeUnitYear, ast.TimeUnitQuarter, ast.TimeUnitMonth, ast.TimeUnitWeek, ast.TimeUnitDay, ast.TimeUnitHour, ast.TimeUnitDayMinute, ast.TimeUnitSecond: + default: + return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs("INTERVAL partitioning, only supports YEAR, QUARTER, MONTH, WEEK, DAY, HOUR, MINUTE and SECOND as time unit") + } + first := ast.PartitionDefinitionClauseLessThan{ + Exprs: []ast.ExprNode{*partOptions.Interval.FirstRangeEnd}, + } + last := ast.PartitionDefinitionClauseLessThan{ + Exprs: []ast.ExprNode{*partOptions.Interval.LastRangeEnd}, + } + if len(tbInfo.Partition.Columns) > 0 { + if err := checkColumnsTypeAndValuesMatch(ctx, tbInfo, first.Exprs); err != nil { + return err + } + if err := checkColumnsTypeAndValuesMatch(ctx, tbInfo, last.Exprs); err != nil { + return err + } + } else { + if err := checkPartitionValuesIsInt(ctx, "FIRST PARTITION", first.Exprs, tbInfo); err != nil { + return err + } + if err := checkPartitionValuesIsInt(ctx, "LAST PARTITION", last.Exprs, tbInfo); err != nil { + return err + } + } + if partOptions.Interval.NullPart { + var partExpr ast.ExprNode + if len(tbInfo.Partition.Columns) == 1 && partOptions.Interval.IntervalExpr.TimeUnit != ast.TimeUnitInvalid { + // Notice compatibility with MySQL, keyword here is 'supported range' but MySQL seems to work from 0000-01-01 too + // https://dev.mysql.com/doc/refman/8.0/en/datetime.html says range 1000-01-01 - 9999-12-31 + // https://docs.pingcap.com/tidb/dev/data-type-date-and-time says The supported range is '0000-01-01' to '9999-12-31' + // set LESS THAN to ZeroTime + partExpr = ast.NewValueExpr("0000-01-01", "", "") + } else { + var min int64 + if partCol != nil { + min = getLowerBoundInt(partCol) + } else { + if !isPartExprUnsigned(tbInfo) { + min = math.MinInt64 + } + } + partExpr = ast.NewValueExpr(min, "", "") + } + partOptions.Definitions = append(partOptions.Definitions, &ast.PartitionDefinition{ + Name: model.NewCIStr("P_NULL"), + Clause: &ast.PartitionDefinitionClauseLessThan{ + Exprs: []ast.ExprNode{partExpr}, + }, + }) + } + + err := GeneratePartDefsFromInterval(ctx, ast.AlterTablePartition, tbInfo, partOptions) + if err != nil { + return err + } + + if partOptions.Interval.MaxValPart { + partOptions.Definitions = append(partOptions.Definitions, &ast.PartitionDefinition{ + Name: model.NewCIStr("P_MAXVALUE"), + Clause: &ast.PartitionDefinitionClauseLessThan{ + Exprs: []ast.ExprNode{&ast.MaxValueExpr{}}, + }, + }) + } + + if len(definedPartDefs) > 0 { + err := comparePartitionDefinitions(ctx, partOptions.Definitions, definedPartDefs) + if err != nil { + return err + } + // Seems valid, so keep the defined so that the user defined names are kept etc. + partOptions.Definitions = definedPartDefs + } else if len(tbInfo.Partition.Definitions) > 0 { + err := comparePartitionAstAndModel(ctx, partOptions, tbInfo.Partition) + if err != nil { + return err + } + } + + return nil +} + +func astIntValueExprFromStr(s string, unsigned bool) (ast.ExprNode, error) { + if unsigned { + u, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return nil, err + } + return ast.NewValueExpr(u, "", ""), nil + } + i, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return nil, err + } + return ast.NewValueExpr(i, "", ""), nil +} + +// GeneratePartDefsFromInterval generates range partitions from INTERVAL partitioning. +// Handles +// - CREATE TABLE: all partitions are generated +// - ALTER TABLE FIRST PARTITION (expr): Drops all partitions before the partition matching the expr (i.e. sets that partition as the new first partition) +// i.e. will return the partitions from old FIRST partition to (and including) new FIRST partition +// - ALTER TABLE LAST PARTITION (expr): Creates new partitions from (excluding) old LAST partition to (including) new LAST partition +// +// partition definitions will be set on partitionOptions +func GeneratePartDefsFromInterval(ctx sessionctx.Context, tp ast.AlterTableType, tbInfo *model.TableInfo, partitionOptions *ast.PartitionOptions) error { + if partitionOptions == nil { + return nil + } + var sb strings.Builder + err := partitionOptions.Interval.IntervalExpr.Expr.Restore(format.NewRestoreCtx(format.DefaultRestoreFlags, &sb)) + if err != nil { + return err + } + intervalString := driver.UnwrapFromSingleQuotes(sb.String()) + if len(intervalString) < 1 || intervalString[:1] < "1" || intervalString[:1] > "9" { + return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs("INTERVAL, should be a positive number") + } + var currVal types.Datum + var startExpr, lastExpr, currExpr ast.ExprNode + var timeUnit ast.TimeUnitType + var partCol *model.ColumnInfo + if len(tbInfo.Partition.Columns) == 1 { + partCol = findColumnByName(tbInfo.Partition.Columns[0].L, tbInfo) + if partCol == nil { + return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs("INTERVAL COLUMNS partitioning: could not find partitioning column") + } + } + timeUnit = partitionOptions.Interval.IntervalExpr.TimeUnit + switch tp { + case ast.AlterTablePartition: + // CREATE TABLE + startExpr = *partitionOptions.Interval.FirstRangeEnd + lastExpr = *partitionOptions.Interval.LastRangeEnd + case ast.AlterTableDropFirstPartition: + startExpr = *partitionOptions.Interval.FirstRangeEnd + lastExpr = partitionOptions.Expr + case ast.AlterTableAddLastPartition: + startExpr = *partitionOptions.Interval.LastRangeEnd + lastExpr = partitionOptions.Expr + default: + return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs("INTERVAL partitioning: Internal error during generating altered INTERVAL partitions, no known alter type") + } + lastVal, err := expression.EvalAstExpr(ctx, lastExpr) + if err != nil { + return err + } + var partDefs []*ast.PartitionDefinition + if len(partitionOptions.Definitions) != 0 { + partDefs = partitionOptions.Definitions + } else { + partDefs = make([]*ast.PartitionDefinition, 0, 1) + } + for i := 0; i < mysql.PartitionCountLimit; i++ { + if i == 0 { + currExpr = startExpr + // TODO: adjust the startExpr and have an offset for interval to handle + // Month/Quarters with start partition on day 28/29/30 + if tp == ast.AlterTableAddLastPartition { + // ALTER TABLE LAST PARTITION ... + // Current LAST PARTITION/start already exists, skip to next partition + continue + } + } else { + currExpr = &ast.BinaryOperationExpr{ + Op: opcode.Mul, + L: ast.NewValueExpr(i, "", ""), + R: partitionOptions.Interval.IntervalExpr.Expr, + } + if timeUnit == ast.TimeUnitInvalid { + currExpr = &ast.BinaryOperationExpr{ + Op: opcode.Plus, + L: startExpr, + R: currExpr, + } + } else { + currExpr = &ast.FuncCallExpr{ + FnName: model.NewCIStr("DATE_ADD"), + Args: []ast.ExprNode{ + startExpr, + currExpr, + &ast.TimeUnitExpr{Unit: timeUnit}, + }, + } + } + } + currVal, err = expression.EvalAstExpr(ctx, currExpr) + if err != nil { + return err + } + cmp, err := currVal.Compare(ctx.GetSessionVars().StmtCtx, &lastVal, collate.GetBinaryCollator()) + if err != nil { + return err + } + if cmp > 0 { + lastStr, err := lastVal.ToString() + if err != nil { + return err + } + sb.Reset() + err = startExpr.Restore(format.NewRestoreCtx(format.DefaultRestoreFlags, &sb)) + if err != nil { + return err + } + startStr := sb.String() + errStr := fmt.Sprintf("INTERVAL: expr (%s) not matching FIRST + n INTERVALs (%s + n * %s", + lastStr, startStr, intervalString) + if timeUnit != ast.TimeUnitInvalid { + errStr = errStr + " " + timeUnit.String() + } + return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs(errStr + ")") + } + valStr, err := currVal.ToString() + if err != nil { + return err + } + if len(valStr) == 0 || valStr[0:1] == "'" { + return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs("INTERVAL partitioning: Error when generating partition values") + } + partName := "P_LT_" + valStr + if timeUnit != ast.TimeUnitInvalid { + currExpr = ast.NewValueExpr(valStr, "", "") + } else { + if valStr[:1] == "-" { + currExpr = ast.NewValueExpr(currVal.GetInt64(), "", "") + } else { + currExpr = ast.NewValueExpr(currVal.GetUint64(), "", "") + } + } + partDefs = append(partDefs, &ast.PartitionDefinition{ + Name: model.NewCIStr(partName), + Clause: &ast.PartitionDefinitionClauseLessThan{ + Exprs: []ast.ExprNode{currExpr}, + }, + }) + if cmp == 0 { + // Last partition! + break + } + } + if len(tbInfo.Partition.Definitions)+len(partDefs) > mysql.PartitionCountLimit { + return errors.Trace(dbterror.ErrTooManyPartitions) + } + partitionOptions.Definitions = partDefs +>>>>>>> a1d135607... ddl: Fix for unsigned partitioning expressions (#36830) return nil } @@ -660,7 +1200,7 @@ func buildRangePartitionDefinitions(ctx sessionctx.Context, defs []*ast.Partitio func checkPartitionValuesIsInt(ctx sessionctx.Context, def *ast.PartitionDefinition, exprs []ast.ExprNode, tbInfo *model.TableInfo) error { tp := types.NewFieldType(mysql.TypeLonglong) - if isColUnsigned(tbInfo.Columns, tbInfo.Partition) { + if isPartExprUnsigned(tbInfo) { tp.AddFlag(mysql.UnsignedFlag) } for _, exp := range exprs { @@ -812,11 +1352,10 @@ func checkRangePartitionValue(ctx sessionctx.Context, tblInfo *model.TableInfo) return nil } - cols := tblInfo.Columns if strings.EqualFold(defs[len(defs)-1].LessThan[0], partitionMaxValue) { defs = defs[:len(defs)-1] } - isUnsigned := isColUnsigned(cols, pi) + isUnsigned := isPartExprUnsigned(tblInfo) var prevRangeValue interface{} for i := 0; i < len(defs); i++ { if strings.EqualFold(defs[i].LessThan[0], partitionMaxValue) { @@ -879,7 +1418,7 @@ func formatListPartitionValue(ctx sessionctx.Context, tblInfo *model.TableInfo) cols := make([]*model.ColumnInfo, 0, len(pi.Columns)) if len(pi.Columns) == 0 { tp := types.NewFieldType(mysql.TypeLonglong) - if isColUnsigned(tblInfo.Columns, tblInfo.Partition) { + if isPartExprUnsigned(tblInfo) { tp.AddFlag(mysql.UnsignedFlag) } colTps = []*types.FieldType{tp} @@ -1944,13 +2483,17 @@ func (cns columnNameSlice) At(i int) string { return cns[i].Name.L } -// isColUnsigned returns true if the partitioning key column is unsigned. -func isColUnsigned(cols []*model.ColumnInfo, pi *model.PartitionInfo) bool { - for _, col := range cols { - isUnsigned := mysql.HasUnsignedFlag(col.GetFlag()) - if isUnsigned && strings.Contains(strings.ToLower(pi.Expr), col.Name.L) { - return true - } +func isPartExprUnsigned(tbInfo *model.TableInfo) bool { + // We should not rely on any configuration, system or session variables, so use a mock ctx! + // Same as in tables.newPartitionExpr + ctx := mock.NewContext() + expr, err := expression.ParseSimpleExprWithTableInfo(ctx, tbInfo.Partition.Expr, tbInfo) + if err != nil { + logutil.BgLogger().Error("isPartExpr failed parsing expression!", zap.Error(err)) + return false + } + if mysql.HasUnsignedFlag(expr.GetType().GetFlag()) { + return true } return false } diff --git a/table/tables/partition_test.go b/table/tables/partition_test.go index 970a479aaa407..5880314683da9 100644 --- a/table/tables/partition_test.go +++ b/table/tables/partition_test.go @@ -526,6 +526,12 @@ func TestRangePartitionUnderNoUnsigned(t *testing.T) { tk.MustExec("drop table if exists tu;") defer tk.MustExec("drop table if exists t2;") defer tk.MustExec("drop table if exists tu;") + tk.MustGetErrCode(`CREATE TABLE tu (c1 BIGINT UNSIGNED) PARTITION BY RANGE(c1 - 10) ( + PARTITION p0 VALUES LESS THAN (-5), + PARTITION p1 VALUES LESS THAN (0), + PARTITION p2 VALUES LESS THAN (5), + PARTITION p3 VALUES LESS THAN (10), + PARTITION p4 VALUES LESS THAN (MAXVALUE));`, mysql.ErrPartitionConstDomain) tk.MustExec("SET @@sql_mode='NO_UNSIGNED_SUBTRACTION';") tk.MustExec(`create table t2 (a bigint unsigned) partition by range (a) ( partition p1 values less than (0),