diff --git a/ddl/db_partition_test.go b/ddl/db_partition_test.go index 2b63760962..39c21b0831 100644 --- a/ddl/db_partition_test.go +++ b/ddl/db_partition_test.go @@ -260,6 +260,19 @@ func (s *testIntegrationSuite3) TestCreateTableWithPartition(c *C) { PARTITION p2 VALUES LESS THAN (2000), PARTITION p3 VALUES LESS THAN (2005) );`, tmysql.ErrBadField) + + // Fix a timezone dependent check bug introduced in https://github.com/pingcap/tidb/pull/10655 + tk.MustExec(`create table t34 (dt timestamp(3)) partition by range (floor(unix_timestamp(dt))) ( + partition p0 values less than (unix_timestamp('2020-04-04 00:00:00')), + partition p1 values less than (unix_timestamp('2020-04-05 00:00:00')));`) + + tk.MustGetErrCode(`create table t34 (dt timestamp(3)) partition by range (unix_timestamp(date(dt))) ( + partition p0 values less than (unix_timestamp('2020-04-04 00:00:00')), + partition p1 values less than (unix_timestamp('2020-04-05 00:00:00')));`, tmysql.ErrWrongExprInPartitionFunc) + + tk.MustGetErrCode(`create table t34 (dt datetime) partition by range (unix_timestamp(dt)) ( + partition p0 values less than (unix_timestamp('2020-04-04 00:00:00')), + partition p1 values less than (unix_timestamp('2020-04-05 00:00:00')));`, tmysql.ErrWrongExprInPartitionFunc) } func (s *testIntegrationSuite2) TestCreateTableWithHashPartition(c *C) { diff --git a/ddl/partition.go b/ddl/partition.go index aa2647c8f1..01e0092289 100644 --- a/ddl/partition.go +++ b/ddl/partition.go @@ -294,32 +294,72 @@ func defaultTimezoneDependent(ctx sessionctx.Context, tblInfo *model.TableInfo, return !v, nil } +func checkPartitionFuncCallValid(ctx sessionctx.Context, tblInfo *model.TableInfo, expr *ast.FuncCallExpr) error { + // We assume the result of any function that has a TIMESTAMP argument to be + // timezone-dependent, since a TIMESTAMP value in both numeric and string + // contexts is interpreted according to the current timezone. + // The only exception is UNIX_TIMESTAMP() which returns the internal + // representation of a TIMESTAMP argument verbatim, and thus does not depend on + // the timezone. + // See https://github.com/mysql/mysql-server/blob/5.7/sql/item_func.h#L445 + if expr.FnName.L != ast.UnixTimestamp { + for _, arg := range expr.Args { + if colName, ok := arg.(*ast.ColumnNameExpr); ok { + col := findColumnByName(colName.Name.Name.L, tblInfo) + if col == nil { + return ErrBadField.GenWithStackByArgs(colName.Name.Name.O, "expression") + } + + if ok && col.FieldType.Tp == mysql.TypeTimestamp { + return errors.Trace(errWrongExprInPartitionFunc) + } + } + } + } + + // check function which allowed in partitioning expressions + // see https://dev.mysql.com/doc/mysql-partitioning-excerpt/5.7/en/partitioning-limitations-functions.html + switch expr.FnName.L { + // Mysql don't allow creating partitions with expressions with non matching + // arguments as a (sub)partitioning function, + // but we want to allow such expressions when opening existing tables for + // easier maintenance. This exception should be deprecated at some point in future so that we always throw an error. + // See https://github.com/mysql/mysql-server/blob/5.7/sql/sql_partition.cc#L1072 + case ast.Day, ast.DayOfMonth, ast.DayOfWeek, ast.DayOfYear, ast.Month, ast.Quarter, ast.ToDays, ast.ToSeconds, + ast.Weekday, ast.Year, ast.YearWeek: + return checkResultOK(hasDateField(ctx, tblInfo, expr)) + case ast.Hour, ast.MicroSecond, ast.Minute, ast.Second, ast.TimeToSec: + return checkResultOK(hasTimeField(ctx, tblInfo, expr)) + case ast.UnixTimestamp: + if len(expr.Args) != 1 { + return errors.Trace(errWrongExprInPartitionFunc) + } + col, err := expression.RewriteSimpleExprWithTableInfo(ctx, tblInfo, expr.Args[0]) + if err != nil { + return errors.Trace(err) + } + if col.GetType().Tp != mysql.TypeTimestamp { + return errors.Trace(errWrongExprInPartitionFunc) + } + return nil + case ast.Abs, ast.Ceiling, ast.DateDiff, ast.Extract, ast.Floor, ast.Mod: + for _, arg := range expr.Args { + if err := checkPartitionExprValid(ctx, tblInfo, arg); err != nil { + return err + } + } + return nil + } + return errors.Trace(ErrPartitionFunctionIsNotAllowed) +} + // checkPartitionExprValid checks partition expression validly. func checkPartitionExprValid(ctx sessionctx.Context, tblInfo *model.TableInfo, expr ast.ExprNode) error { switch v := expr.(type) { case *ast.FuncCastExpr, *ast.CaseExpr, *ast.SubqueryExpr, *ast.WindowFuncExpr, *ast.RowExpr, *ast.DefaultExpr, *ast.ValuesExpr: return errors.Trace(ErrPartitionFunctionIsNotAllowed) case *ast.FuncCallExpr: - // check function which allowed in partitioning expressions - // see https://dev.mysql.com/doc/mysql-partitioning-excerpt/5.7/en/partitioning-limitations-functions.html - switch v.FnName.L { - // Mysql don't allow creating partitions with expressions with non matching - // arguments as a (sub)partitioning function, - // but we want to allow such expressions when opening existing tables for - // easier maintenance. This exception should be deprecated at some point in future so that we always throw an error. - // See https://github.com/mysql/mysql-server/blob/5.7/sql/sql_partition.cc#L1072 - case ast.Day, ast.DayOfMonth, ast.DayOfWeek, ast.DayOfYear, ast.Month, ast.Quarter, ast.ToDays, ast.ToSeconds, - ast.Weekday, ast.Year, ast.YearWeek: - return checkPartitionFunc(hasDateField(ctx, tblInfo, expr)) - case ast.Hour, ast.MicroSecond, ast.Minute, ast.Second, ast.TimeToSec: - return checkPartitionFunc(hasTimeField(ctx, tblInfo, expr)) - case ast.UnixTimestamp: - return checkPartitionFunc(hasTimestampField(ctx, tblInfo, expr)) - case ast.Abs, ast.Ceiling, ast.DateDiff, ast.Extract, ast.Floor, ast.Mod: - return checkPartitionFunc(defaultTimezoneDependent(ctx, tblInfo, expr)) - default: - return errors.Trace(ErrPartitionFunctionIsNotAllowed) - } + return checkPartitionFuncCallValid(ctx, tblInfo, v) case *ast.BinaryOperationExpr: // The DIV operator (opcode.IntDiv) is also supported; the / operator ( opcode.Div ) is not permitted. // see https://dev.mysql.com/doc/refman/5.7/en/partitioning-limitations.html @@ -361,12 +401,12 @@ func checkPartitionFuncValid(ctx sessionctx.Context, tblInfo *model.TableInfo, e // For partition tables, mysql do not support Constant, random or timezone-dependent expressions // Based on mysql code to check whether field is valid, every time related type has check_valid_arguments_processor function. // See https://github.com/mysql/mysql-server/blob/5.7/sql/item_timefunc. -func checkPartitionFunc(isTimezoneDependent bool, err error) error { +func checkResultOK(ok bool, err error) error { if err != nil { return err } - if !isTimezoneDependent { + if !ok { return errors.Trace(errWrongExprInPartitionFunc) } @@ -770,11 +810,10 @@ func (cne *columnNameExtractor) Enter(node ast.Node) (ast.Node, bool) { func (cne *columnNameExtractor) Leave(node ast.Node) (ast.Node, bool) { if c, ok := node.(*ast.ColumnNameExpr); ok { - for _, info := range cne.tblInfo.Columns { - if info.Name.L == c.Name.Name.L { - cne.extractedColumns = append(cne.extractedColumns, info) - return node, true - } + info := findColumnByName(c.Name.Name.L, cne.tblInfo) + if info != nil { + cne.extractedColumns = append(cne.extractedColumns, info) + return node, true } cne.err = ErrBadField.GenWithStackByArgs(c.Name.Name.O, "expression") return nil, false @@ -782,6 +821,15 @@ func (cne *columnNameExtractor) Leave(node ast.Node) (ast.Node, bool) { return node, true } +func findColumnByName(colName string, tblInfo *model.TableInfo) *model.ColumnInfo { + for _, info := range tblInfo.Columns { + if info.Name.L == colName { + return info + } + } + return nil +} + func extractPartitionColumns(partExpr string, tblInfo *model.TableInfo) ([]*model.ColumnInfo, error) { partExpr = "select " + partExpr stmts, _, err := parser.New().Parse(partExpr, "", "") diff --git a/planner/core/rule_partition_processor.go b/planner/core/rule_partition_processor.go index b0933425af..ff8999910b 100644 --- a/planner/core/rule_partition_processor.go +++ b/planner/core/rule_partition_processor.go @@ -371,7 +371,13 @@ func makePartitionByFnCol(sctx sessionctx.Context, columns []*expression.Column, case *expression.ScalarFunction: if _, ok := monotoneIncFuncs[raw.FuncName.L]; ok { fn = raw - col = fn.GetArgs()[0].(*expression.Column) + args := fn.GetArgs() + if len(args) > 0 { + arg0 := args[0] + if c, ok1 := arg0.(*expression.Column); ok1 { + col = c + } + } } case *expression.Column: col = raw