diff --git a/ddl/db_partition_test.go b/ddl/db_partition_test.go index 01263fc7e11fa..116a508bba732 100644 --- a/ddl/db_partition_test.go +++ b/ddl/db_partition_test.go @@ -168,6 +168,7 @@ func TestCreateTableWithPartition(t *testing.T) { partition p3 values less than (65,30,13), partition p4 values less than (maxvalue,30,40) );`) + tk.MustQuery("show warnings").Check(testkit.Rows()) sql6 := `create table employees ( id int not null, @@ -270,15 +271,14 @@ func TestCreateTableWithPartition(t *testing.T) { partition p4 values less than (18446744073709551000 + 10) );`) - tk.MustExec("set @@tidb_enable_table_partition = 1") tk.MustExec("set @@tidb_enable_table_partition = 1") tk.MustExec(`create table t30 ( a int, - b float, + b varchar(20), c varchar(30)) partition by range columns (a, b) - (partition p0 values less than (10, 10.0))`) - tk.MustQuery("show warnings").Check(testkit.Rows("Warning 8200 Unsupported partition type RANGE, treat as normal table")) + (partition p0 values less than (10, '10.0'))`) + tk.MustQuery("show warnings").Check(testkit.Rows()) tk.MustGetErrCode(`create table t31 (a int not null) partition by range( a );`, errno.ErrPartitionsMustBeDefined) tk.MustGetErrCode(`create table t32 (a int not null) partition by range columns( a );`, errno.ErrPartitionsMustBeDefined) @@ -404,7 +404,6 @@ func TestCreateTableWithRangeColumnPartition(t *testing.T) { tk := testkit.NewTestKit(t, store) tk.MustExec("use test;") tk.MustExec("drop table if exists log_message_1;") - tk.MustExec("set @@session.tidb_enable_list_partition = ON") tk.MustExec(` create table log_message_1 ( add_time datetime not null default '2000-01-01 00:00:00', @@ -437,6 +436,36 @@ create table log_message_1 ( tk.MustExec("drop table if exists t") + tk.MustExec("create table t (a varchar(255), b varchar(255)) partition by range columns (a,b)" + + `(partition pNull values less than ("",""), partition p0 values less than ("A",""),` + + `partition p1 values less than ("A","A"), partition p2 values less than ("A","b"),` + + `partition p3 values less than ("A",maxvalue), partition p4 values less than ("B",""),` + + `partition pMax values less than (maxvalue,""))`) + err := tk.ExecToErr("create table t (a varchar(255), b varchar(255)) partition by range columns (a,b)" + + `(partition pNull values less than ("",""), partition p0 values less than ("A",""),` + + `partition p1 values less than ("A","A"), partition p2 values less than ("A","b"),` + + `partition p3 values less than ("A",maxvalue), partition p4 values less than ("B",""),` + + // If one column has maxvalue set, the next column does not matter, so we should not allow it! + `partition pMax values less than (maxvalue,""), partition pMax2 values less than (maxvalue,"a"))`) + require.Error(t, err) + require.EqualError(t, err, "[ddl:1493]VALUES LESS THAN value must be strictly increasing for each partition") + err = tk.ExecToErr("create table t (a varchar(255), b varchar(255)) partition by range columns (a,b)" + + `(partition pNull values less than ("",""), partition p0 values less than ("A",""),` + + `partition p1 values less than ("A","A"), partition p2 values less than ("A","b"),` + + `partition p3 values less than ("A",maxvalue), partition p4 values less than ("B",""),` + + // If one column has maxvalue set, the next column does not matter, so we should not allow it! + `partition pMax values less than ("b",MAXVALUE), partition pMax2 values less than ("b","a"))`) + require.Error(t, err) + require.EqualError(t, err, "[ddl:1493]VALUES LESS THAN value must be strictly increasing for each partition") + err = tk.ExecToErr("create table t (a varchar(255), b varchar(255)) partition by range columns (a,b)" + + `(partition pNull values less than ("",""), partition p0 values less than ("A",""),` + + `partition p1 values less than ("A","A"), partition p2 values less than ("A","b"),` + + `partition p3 values less than ("A",maxvalue), partition p4 values less than ("B",""),` + + // If one column has maxvalue set, the next column does not matter, so we should not allow it! + `partition pMax values less than ("b",MAXVALUE), partition pMax2 values less than ("b",MAXVALUE))`) + require.Error(t, err) + require.EqualError(t, err, "[ddl:1493]VALUES LESS THAN value must be strictly increasing for each partition") + type testCase struct { sql string err *terror.Error @@ -451,14 +480,6 @@ create table log_message_1 ( "create table t(a datetime) partition by range columns (a) (partition p1 values less than ('2000-02-01'), partition p2 values less than ('20000102'));", dbterror.ErrRangeNotIncreasing, }, - { - "create table t(a time) partition by range columns (a) (partition p1 values less than ('202020'), partition p2 values less than ('20:20:10'));", - dbterror.ErrRangeNotIncreasing, - }, - { - "create table t(a time) partition by range columns (a) (partition p1 values less than ('202090'));", - dbterror.ErrWrongTypeColumnValue, - }, { "create table t (id int) partition by range columns (id) (partition p0 values less than (1, 2));", ast.ErrPartitionColumnList, @@ -492,12 +513,12 @@ create table log_message_1 ( dbterror.ErrNotAllowedTypeInPartition, }, // create as normal table, warning. - // { - // "create table t (a int, b varchar(64)) partition by range columns (a, b) (" + - // "partition p0 values less than (1, 'a')," + - // "partition p1 values less than (1, 'a'))", - // dbterror.ErrRangeNotIncreasing, - // }, + { + "create table t (a int, b varchar(64)) partition by range columns (a, b) (" + + "partition p0 values less than (1, 'a')," + + "partition p1 values less than (1, 'a'))", + dbterror.ErrRangeNotIncreasing, + }, { "create table t (a int, b varchar(64)) partition by range columns ( b) (" + "partition p0 values less than ( 'a')," + @@ -505,12 +526,12 @@ create table log_message_1 ( dbterror.ErrRangeNotIncreasing, }, // create as normal table, warning. - // { - // "create table t (a int, b varchar(64)) partition by range columns (a, b) (" + - // "partition p0 values less than (1, 'b')," + - // "partition p1 values less than (1, 'a'))", - // dbterror.ErrRangeNotIncreasing, - // }, + { + "create table t (a int, b varchar(64)) partition by range columns (a, b) (" + + "partition p0 values less than (1, 'b')," + + "partition p1 values less than (1, 'a'))", + dbterror.ErrRangeNotIncreasing, + }, { "create table t (a int, b varchar(64)) partition by range columns (b) (" + "partition p0 values less than ('b')," + @@ -518,12 +539,12 @@ create table log_message_1 ( dbterror.ErrRangeNotIncreasing, }, // create as normal table, warning. - // { - // "create table t (a int, b varchar(64)) partition by range columns (a, b) (" + - // "partition p0 values less than (1, maxvalue)," + - // "partition p1 values less than (1, 'a'))", - // dbterror.ErrRangeNotIncreasing, - // }, + { + "create table t (a int, b varchar(64)) partition by range columns (a, b) (" + + "partition p0 values less than (1, maxvalue)," + + "partition p1 values less than (1, 'a'))", + dbterror.ErrRangeNotIncreasing, + }, { "create table t (a int, b varchar(64)) partition by range columns ( b) (" + "partition p0 values less than ( maxvalue)," + @@ -620,6 +641,7 @@ create table log_message_1 ( tk.MustExec("create table t1 (a int, b char(3)) partition by range columns (a, b) (" + "partition p0 values less than (1, 'a')," + "partition p1 values less than (2, maxvalue))") + tk.MustQuery("show warnings").Check(testkit.Rows()) tk.MustExec("drop table if exists t2;") tk.MustExec("create table t2 (a int, b char(3)) partition by range columns (b) (" + @@ -653,6 +675,38 @@ create table log_message_1 ( tk.MustExec(`alter table t add partition (partition p1 values less than (X'0D'), partition p2 values less than (X'0E'));`) tk.MustExec(`insert into t values (X'0B'), (X'0C'), (X'0D')`) tk.MustQuery(`select * from t where a < X'0D' order by a`).Check(testkit.Rows("\x0B", "\x0C")) + tk.MustExec(`drop table t`) + + tk.MustExec(`create table t(a time) partition by range columns (a) (partition p1 values less than ('2020'))`) + tk.MustExec(`insert into t values ('2019')`) + tk.MustQuery(`show create table t`).Check(testkit.Rows( + "t CREATE TABLE `t` (\n" + + " `a` time DEFAULT NULL\n" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin\n" + + "PARTITION BY RANGE COLUMNS(`a`)\n" + + "(PARTITION `p1` VALUES LESS THAN ('2020'))")) + tk.MustExec(`drop table t`) + tk.MustExec(`create table t (a time, b time) partition by range columns (a) (partition p1 values less than ('2020'), partition p2 values less than ('20:20:10'))`) + tk.MustQuery(`show create table t`).Check(testkit.Rows( + "t CREATE TABLE `t` (\n" + + " `a` time DEFAULT NULL,\n" + + " `b` time DEFAULT NULL\n" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin\n" + + "PARTITION BY RANGE COLUMNS(`a`)\n" + + "(PARTITION `p1` VALUES LESS THAN ('2020'),\n" + + " PARTITION `p2` VALUES LESS THAN ('20:20:10'))")) + tk.MustExec(`insert into t values ('2019','2019'),('20:20:09','20:20:09')`) + tk.MustExec(`drop table t`) + tk.MustExec(`create table t (a time, b time) partition by range columns (a,b) (partition p1 values less than ('2020','2020'), partition p2 values less than ('20:20:10','20:20:10'))`) + tk.MustExec(`insert into t values ('2019','2019'),('20:20:09','20:20:09')`) + tk.MustQuery(`show create table t`).Check(testkit.Rows( + "t CREATE TABLE `t` (\n" + + " `a` time DEFAULT NULL,\n" + + " `b` time DEFAULT NULL\n" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin\n" + + "PARTITION BY RANGE COLUMNS(`a`,`b`)\n" + + "(PARTITION `p1` VALUES LESS THAN ('00:20:20','00:20:20'),\n" + + " PARTITION `p2` VALUES LESS THAN ('20:20:10','20:20:10'))")) } func TestPartitionRangeColumnsCollate(t *testing.T) { @@ -2939,7 +2993,6 @@ func TestPartitionUniqueKeyNeedAllFieldsInPf(t *testing.T) { )` tk.MustGetErrCode(sql10, errno.ErrUniqueKeyNeedAllFieldsInPf) - // after we support multiple columns partition, this sql should fail. For now, it will be a normal table. sql11 := `create table part9 ( a int not null, b int not null, @@ -2954,7 +3007,7 @@ func TestPartitionUniqueKeyNeedAllFieldsInPf(t *testing.T) { partition p1 values less than (7, 9), partition p2 values less than (11, 22) )` - tk.MustExec(sql11) + tk.MustGetErrCode(sql11, errno.ErrUniqueKeyNeedAllFieldsInPf) sql12 := `create table part12 (a varchar(20), b binary, unique index (a(5))) partition by range columns (a) ( partition p0 values less than ('aaaaa'), @@ -4337,6 +4390,7 @@ func TestPartitionTableWithAnsiQuotes(t *testing.T) { "(PARTITION \"p0\" VALUES LESS THAN (1638288000),\n" + " PARTITION \"p1\" VALUES LESS THAN (1640966400))")) tk.MustExec("drop table t") + tk.MustExec("set @@time_zone = default") // Test values in. tk.MustExec(`CREATE TABLE t (a int DEFAULT NULL, b varchar(255) DEFAULT NULL) PARTITION BY LIST COLUMNS(a,b) ( @@ -4354,12 +4408,96 @@ func TestPartitionTableWithAnsiQuotes(t *testing.T) { // Test escaped characters in single quotes. tk.MustExec(`CREATE TABLE t (a varchar(255) DEFAULT NULL) PARTITION BY LIST COLUMNS(a) ( PARTITION p0 VALUES IN ('\'','\'\'',''''''''), - PARTITION p1 VALUES IN ('""','\\','\\\'\t\n'))`) + PARTITION p1 VALUES IN ('""','\\','\\\'\t\n'))`) tk.MustQuery("show create table t").Check(testkit.Rows("t CREATE TABLE \"t\" (\n" + " \"a\" varchar(255) DEFAULT NULL\n" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin\n" + "PARTITION BY LIST COLUMNS(\"a\")\n" + - "(PARTITION \"p0\" VALUES IN ('''','''''',''''''''),\n" + - " PARTITION \"p1\" VALUES IN ('\"\"','\\\\','\\\\''\t\n'))")) + `(PARTITION "p0" VALUES IN ('''','''''',''''''''),` + "\n" + + ` PARTITION "p1" VALUES IN ('""','\\','\\''\t\n'))`)) + tk.MustExec(`insert into t values (0x5c27090a),('\\''\t\n')`) tk.MustExec("drop table t") + tk.MustExec(`CREATE TABLE t (a varchar(255) DEFAULT NULL) PARTITION BY LIST COLUMNS(a) ( + PARTITION p0 VALUES IN ('\'','\'\'',''''''''), + PARTITION p1 VALUES IN ('\"\"','\\',0x5c27090a))`) + tk.MustExec(`insert into t values (0x5c27090a),('\\''\t\n')`) + tk.MustQuery("show create table t").Check(testkit.Rows("t CREATE TABLE \"t\" (\n" + + " \"a\" varchar(255) DEFAULT NULL\n" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin\n" + + "PARTITION BY LIST COLUMNS(\"a\")\n" + + `(PARTITION "p0" VALUES IN ('''','''''',''''''''),` + "\n" + + ` PARTITION "p1" VALUES IN ('""','\\',x'5c27090a'))`)) + tk.MustExec("drop table t") + tk.MustExec(`CREATE TABLE t (a varchar(255) DEFAULT NULL) PARTITION BY LIST COLUMNS(a) ( + PARTITION p0 VALUES IN ('\'','\'\'',''''''''), + PARTITION p1 VALUES IN ('""','\\',x'5c27090a'))`) + tk.MustExec(`insert into t values (0x5c27090a),('\\''\t\n')`) + tk.MustQuery("show create table t").Check(testkit.Rows("t CREATE TABLE \"t\" (\n" + + " \"a\" varchar(255) DEFAULT NULL\n" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin\n" + + "PARTITION BY LIST COLUMNS(\"a\")\n" + + `(PARTITION "p0" VALUES IN ('''','''''',''''''''),` + "\n" + + ` PARTITION "p1" VALUES IN ('""','\\',x'5c27090a'))`)) + + // https://github.com/pingcap/tidb/issues/37692 + tk.MustExec("drop table t") + tk.MustExec(`CREATE TABLE t (a varchar(255)) PARTITION BY RANGE COLUMNS(a) ( + PARTITION p0 VALUES LESS THAN ('"'), + PARTITION p1 VALUES LESS THAN ('""'), + PARTITION p2 VALUES LESS THAN ('\''), + PARTITION p3 VALUES LESS THAN (''''''), + PARTITION p4 VALUES LESS THAN ('\\''\t\n'), + PARTITION pMax VALUES LESS THAN (MAXVALUE))`) + //PARTITION p4 VALUES IN (x'5c27090a'))`) + tk.MustExec(`insert into t values (0x5c27090a),('\\''\t\n')`) + tk.MustQuery("show create table t").Check(testkit.Rows("t CREATE TABLE \"t\" (\n" + + " \"a\" varchar(255) DEFAULT NULL\n" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin\n" + + `PARTITION BY RANGE COLUMNS("a")` + "\n" + + `(PARTITION "p0" VALUES LESS THAN ('"'),` + "\n" + + ` PARTITION "p1" VALUES LESS THAN ('""'),` + "\n" + + ` PARTITION "p2" VALUES LESS THAN (''''),` + "\n" + + ` PARTITION "p3" VALUES LESS THAN (''''''),` + "\n" + + ` PARTITION "p4" VALUES LESS THAN ('\\''\t\n'),` + "\n" + + ` PARTITION "pMax" VALUES LESS THAN (MAXVALUE))`)) + tk.MustExec("drop table t") + tk.MustExec(`CREATE TABLE t (a varchar(255)) PARTITION BY RANGE COLUMNS(a) ( + PARTITION p0 VALUES LESS THAN ('"'), + PARTITION p1 VALUES LESS THAN ('""'), + PARTITION p2 VALUES LESS THAN ('\''), + PARTITION p3 VALUES LESS THAN (''''''), + PARTITION p4 VALUES LESS THAN (0x5c27090a), + PARTITION pMax VALUES LESS THAN (MAXVALUE))`) + tk.MustExec(`insert into t values (0x5c27090a),('\\''\t\n')`) + tk.MustQuery("show create table t").Check(testkit.Rows("t CREATE TABLE \"t\" (\n" + + " \"a\" varchar(255) DEFAULT NULL\n" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin\n" + + `PARTITION BY RANGE COLUMNS("a")` + "\n" + + `(PARTITION "p0" VALUES LESS THAN ('"'),` + "\n" + + ` PARTITION "p1" VALUES LESS THAN ('""'),` + "\n" + + ` PARTITION "p2" VALUES LESS THAN (''''),` + "\n" + + ` PARTITION "p3" VALUES LESS THAN (''''''),` + "\n" + + ` PARTITION "p4" VALUES LESS THAN (x'5c27090a'),` + "\n" + + ` PARTITION "pMax" VALUES LESS THAN (MAXVALUE))`)) + + tk.MustExec("drop table t") + tk.MustExec(`CREATE TABLE t (a varchar(255), b varchar(255)) PARTITION BY RANGE COLUMNS(a,b) ( + PARTITION p0 VALUES LESS THAN ('"','"'), + PARTITION p1 VALUES LESS THAN ('""','""'), + PARTITION p2 VALUES LESS THAN ('\'','\''), + PARTITION p3 VALUES LESS THAN ('''''',''''''), + PARTITION p4 VALUES LESS THAN ('\\''\t\n',0x5c27090a), + PARTITION pMax VALUES LESS THAN (MAXVALUE,maxvalue))`) + tk.MustExec(`insert into t values (0x5c27090a,'\\''\t\n')`) + tk.MustQuery("show create table t").Check(testkit.Rows("t CREATE TABLE \"t\" (\n" + + " \"a\" varchar(255) DEFAULT NULL,\n" + + " \"b\" varchar(255) DEFAULT NULL\n" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin\n" + + `PARTITION BY RANGE COLUMNS("a","b")` + "\n" + + `(PARTITION "p0" VALUES LESS THAN ('"','"'),` + "\n" + + ` PARTITION "p1" VALUES LESS THAN ('""','""'),` + "\n" + + ` PARTITION "p2" VALUES LESS THAN ('''',''''),` + "\n" + + ` PARTITION "p3" VALUES LESS THAN ('''''',''''''),` + "\n" + + ` PARTITION "p4" VALUES LESS THAN ('\\''\t\n','\\''\t\n'),` + "\n" + + ` PARTITION "pMax" VALUES LESS THAN (MAXVALUE,MAXVALUE))`)) } diff --git a/ddl/ddl_api.go b/ddl/ddl_api.go index 1d26ec7569fe6..5f483ab221400 100644 --- a/ddl/ddl_api.go +++ b/ddl/ddl_api.go @@ -2777,6 +2777,7 @@ func checkColumnsPartitionType(tbInfo *model.TableInfo) error { // DATE and DATETIME // CHAR, VARCHAR, BINARY, and VARBINARY // See https://dev.mysql.com/doc/mysql-partitioning-excerpt/5.7/en/partitioning-columns.html + // Note that also TIME is allowed in MySQL. Also see https://bugs.mysql.com/bug.php?id=84362 switch colInfo.FieldType.GetType() { case mysql.TypeTiny, mysql.TypeShort, mysql.TypeInt24, mysql.TypeLong, mysql.TypeLonglong: case mysql.TypeDate, mysql.TypeDatetime, mysql.TypeDuration: @@ -6461,20 +6462,21 @@ func buildAddedPartitionDefs(ctx sessionctx.Context, meta *model.TableInfo, spec return GeneratePartDefsFromInterval(ctx, spec.Tp, meta, spec.Partition) } -func checkColumnsTypeAndValuesMatch(ctx sessionctx.Context, meta *model.TableInfo, exprs []ast.ExprNode) error { +func checkAndGetColumnsTypeAndValuesMatch(ctx sessionctx.Context, colTypes []types.FieldType, exprs []ast.ExprNode) ([]string, error) { // Validate() has already checked len(colNames) = len(exprs) // create table ... partition by range columns (cols) // partition p0 values less than (expr) // check the type of cols[i] and expr is consistent. - colTypes := collectColumnsType(meta) + valStrings := make([]string, 0, len(colTypes)) for i, colExpr := range exprs { if _, ok := colExpr.(*ast.MaxValueExpr); ok { + valStrings = append(valStrings, partitionMaxValue) continue } colType := colTypes[i] val, err := expression.EvalAstExpr(ctx, colExpr) if err != nil { - return err + return nil, err } // Check val.ConvertTo(colType) doesn't work, so we need this case by case check. vkind := val.Kind() @@ -6483,33 +6485,38 @@ func checkColumnsTypeAndValuesMatch(ctx sessionctx.Context, meta *model.TableInf switch vkind { case types.KindString, types.KindBytes: default: - return dbterror.ErrWrongTypeColumnValue.GenWithStackByArgs() + return nil, dbterror.ErrWrongTypeColumnValue.GenWithStackByArgs() } case mysql.TypeTiny, mysql.TypeShort, mysql.TypeInt24, mysql.TypeLong, mysql.TypeLonglong: switch vkind { case types.KindInt64, types.KindUint64, types.KindNull: default: - return dbterror.ErrWrongTypeColumnValue.GenWithStackByArgs() + return nil, dbterror.ErrWrongTypeColumnValue.GenWithStackByArgs() } case mysql.TypeFloat, mysql.TypeDouble: switch vkind { case types.KindFloat32, types.KindFloat64, types.KindNull: default: - return dbterror.ErrWrongTypeColumnValue.GenWithStackByArgs() + return nil, dbterror.ErrWrongTypeColumnValue.GenWithStackByArgs() } case mysql.TypeString, mysql.TypeVarString: switch vkind { case types.KindString, types.KindBytes, types.KindNull, types.KindBinaryLiteral: default: - return dbterror.ErrWrongTypeColumnValue.GenWithStackByArgs() + return nil, dbterror.ErrWrongTypeColumnValue.GenWithStackByArgs() } } - _, err = val.ConvertTo(ctx.GetSessionVars().StmtCtx, &colType) + newVal, err := val.ConvertTo(ctx.GetSessionVars().StmtCtx, &colType) + if err != nil { + return nil, dbterror.ErrWrongTypeColumnValue.GenWithStackByArgs() + } + s, err := newVal.ToString() if err != nil { - return dbterror.ErrWrongTypeColumnValue.GenWithStackByArgs() + return nil, err } + valStrings = append(valStrings, s) } - return nil + return valStrings, nil } // LockTables uses to execute lock tables statement. diff --git a/ddl/partition.go b/ddl/partition.go index e0cc6756b2075..b2fa6293dfd86 100644 --- a/ddl/partition.go +++ b/ddl/partition.go @@ -17,6 +17,7 @@ package ddl import ( "bytes" "context" + "encoding/hex" "fmt" "math" "strconv" @@ -420,14 +421,7 @@ func buildTablePartitionInfo(ctx sessionctx.Context, s *ast.PartitionOptions, tb switch s.Tp { case model.PartitionTypeRange: if s.Sub == nil { - // Partition by range expression is enabled by default. - if s.ColumnNames == nil { - enable = true - } - // Partition by range columns and just one column. - if len(s.ColumnNames) == 1 { - enable = true - } + enable = true } case model.PartitionTypeHash: // Partition by hash is enabled by default. @@ -784,10 +778,11 @@ func generatePartitionDefinitionsFromInterval(ctx sessionctx.Context, partOption Exprs: []ast.ExprNode{*partOptions.Interval.LastRangeEnd}, } if len(tbInfo.Partition.Columns) > 0 { - if err := checkColumnsTypeAndValuesMatch(ctx, tbInfo, first.Exprs); err != nil { + colTypes := collectColumnsType(tbInfo) + if _, err := checkAndGetColumnsTypeAndValuesMatch(ctx, colTypes, first.Exprs); err != nil { return err } - if err := checkColumnsTypeAndValuesMatch(ctx, tbInfo, last.Exprs); err != nil { + if _, err := checkAndGetColumnsTypeAndValuesMatch(ctx, colTypes, last.Exprs); err != nil { return err } } else { @@ -1082,6 +1077,7 @@ func buildHashPartitionDefinitions(_ sessionctx.Context, defs []*ast.PartitionDe func buildListPartitionDefinitions(ctx sessionctx.Context, defs []*ast.PartitionDefinition, tbInfo *model.TableInfo) ([]model.PartitionDefinition, error) { definitions := make([]model.PartitionDefinition, 0, len(defs)) exprChecker := newPartitionExprChecker(ctx, nil, checkPartitionExprAllowed) + colTypes := collectColumnsType(tbInfo) for _, def := range defs { if err := def.Clause.Validate(model.PartitionTypeList, len(tbInfo.Partition.Columns)); err != nil { return nil, err @@ -1089,7 +1085,9 @@ func buildListPartitionDefinitions(ctx sessionctx.Context, defs []*ast.Partition clause := def.Clause.(*ast.PartitionDefinitionClauseIn) if len(tbInfo.Partition.Columns) > 0 { for _, vs := range clause.Values { - if err := checkColumnsTypeAndValuesMatch(ctx, tbInfo, vs); err != nil { + // TODO: use the generated strings / normalized partition values + _, err := checkAndGetColumnsTypeAndValuesMatch(ctx, colTypes, vs) + if err != nil { return nil, err } } @@ -1150,13 +1148,16 @@ func collectColumnsType(tbInfo *model.TableInfo) []types.FieldType { func buildRangePartitionDefinitions(ctx sessionctx.Context, defs []*ast.PartitionDefinition, tbInfo *model.TableInfo) ([]model.PartitionDefinition, error) { definitions := make([]model.PartitionDefinition, 0, len(defs)) exprChecker := newPartitionExprChecker(ctx, nil, checkPartitionExprAllowed) + colTypes := collectColumnsType(tbInfo) for _, def := range defs { if err := def.Clause.Validate(model.PartitionTypeRange, len(tbInfo.Partition.Columns)); err != nil { return nil, err } clause := def.Clause.(*ast.PartitionDefinitionClauseLessThan) + var partValStrings []string if len(tbInfo.Partition.Columns) > 0 { - if err := checkColumnsTypeAndValuesMatch(ctx, tbInfo, clause.Exprs); err != nil { + var err error + if partValStrings, err = checkAndGetColumnsTypeAndValuesMatch(ctx, colTypes, clause.Exprs); err != nil { return nil, err } } else { @@ -1184,14 +1185,31 @@ func buildRangePartitionDefinitions(ctx sessionctx.Context, defs []*ast.Partitio buf := new(bytes.Buffer) // Range columns partitions support multi-column partitions. - for _, expr := range clause.Exprs { + for i, expr := range clause.Exprs { expr.Accept(exprChecker) if exprChecker.err != nil { return nil, exprChecker.err } - expr.Format(buf) - piDef.LessThan = append(piDef.LessThan, buf.String()) - buf.Reset() + // If multi-column use new evaluated+normalized output, instead of just formatted expression + if len(partValStrings) > i && len(colTypes) > 1 { + partVal := partValStrings[i] + switch colTypes[i].EvalType() { + case types.ETInt: + // no wrapping + case types.ETDatetime, types.ETString, types.ETDuration: + if _, ok := clause.Exprs[i].(*ast.MaxValueExpr); !ok { + // Don't wrap MAXVALUE + partVal = driver.WrapInSingleQuotes(partVal) + } + default: + return nil, dbterror.ErrWrongTypeColumnValue.GenWithStackByArgs() + } + piDef.LessThan = append(piDef.LessThan, partVal) + } else { + expr.Format(buf) + piDef.LessThan = append(piDef.LessThan, buf.String()) + buf.Reset() + } } definitions = append(definitions, piDef) } @@ -2170,7 +2188,7 @@ func checkExchangePartitionRecordValidation(w *worker, pt *model.TableInfo, inde // For range expression and range columns if len(pi.Columns) == 0 { sql, paramList = buildCheckSQLForRangeExprPartition(pi, index, schemaName, tableName) - } else if len(pi.Columns) == 1 { + } else { sql, paramList = buildCheckSQLForRangeColumnsPartition(pi, index, schemaName, tableName) } case model.PartitionTypeList: @@ -2736,6 +2754,56 @@ func checkNoTimestampArgs(tbInfo *model.TableInfo, exprs ...ast.ExprNode) error return nil } +// hexIfNonPrint checks if printable UTF-8 characters from a single quoted string, +// if so, just returns the string +// else returns a hex string of the binary string (i.e. actual encoding, not unicode code points!) +func hexIfNonPrint(s string) string { + isPrint := true + // https://go.dev/blog/strings `for range` of string converts to runes! + for _, runeVal := range s { + if !strconv.IsPrint(runeVal) { + isPrint = false + break + } + } + if isPrint { + return s + } + // To avoid 'simple' MySQL accepted escape characters, to be showed as hex, just escape them + // \0 \b \n \r \t \Z, see https://dev.mysql.com/doc/refman/8.0/en/string-literals.html + isPrint = true + res := "" + for _, runeVal := range s { + switch runeVal { + case 0: // Null + res += `\0` + case 7: // Bell + res += `\b` + case '\t': // 9 + res += `\t` + case '\n': // 10 + res += `\n` + case '\r': // 13 + res += `\r` + case 26: // ctrl-z / Substitute + res += `\Z` + default: + if strconv.IsPrint(runeVal) { + res += string(runeVal) + } else { + isPrint = false + break + } + } + } + if isPrint { + return res + } + // Not possible to create an easy interpreted MySQL string, return as hex string + // Can be converted to string in MySQL like: CAST(UNHEX('') AS CHAR(255)) + return "0x" + hex.EncodeToString([]byte(driver.UnwrapFromSingleQuotes(s))) +} + // AppendPartitionDefs generates a list of partition definitions needed for SHOW CREATE TABLE (in executor/show.go) // as well as needed for generating the ADD PARTITION query for INTERVAL partitioning of ALTER TABLE t LAST PARTITION // and generating the CREATE TABLE query from CREATE TABLE ... INTERVAL @@ -2747,8 +2815,11 @@ func AppendPartitionDefs(partitionInfo *model.PartitionInfo, buf *bytes.Buffer, fmt.Fprintf(buf, "PARTITION %s", stringutil.Escape(def.Name.O, sqlMode)) // PartitionTypeHash does not have any VALUES definition if partitionInfo.Type == model.PartitionTypeRange { - lessThans := strings.Join(def.LessThan, ",") - fmt.Fprintf(buf, " VALUES LESS THAN (%s)", lessThans) + lessThans := make([]string, len(def.LessThan)) + for idx, v := range def.LessThan { + lessThans[idx] = hexIfNonPrint(v) + } + fmt.Fprintf(buf, " VALUES LESS THAN (%s)", strings.Join(lessThans, ",")) } else if partitionInfo.Type == model.PartitionTypeList { values := bytes.NewBuffer(nil) for j, inValues := range def.InValues { @@ -2757,10 +2828,14 @@ func AppendPartitionDefs(partitionInfo *model.PartitionInfo, buf *bytes.Buffer, } if len(inValues) > 1 { values.WriteString("(") - values.WriteString(strings.Join(inValues, ",")) + tmpVals := make([]string, len(inValues)) + for idx, v := range inValues { + tmpVals[idx] = hexIfNonPrint(v) + } + values.WriteString(strings.Join(tmpVals, ",")) values.WriteString(")") - } else { - values.WriteString(strings.Join(inValues, ",")) + } else if len(inValues) == 1 { + values.WriteString(hexIfNonPrint(inValues[0])) } } fmt.Fprintf(buf, " VALUES IN (%s)", values.String()) diff --git a/executor/seqtest/seq_executor_test.go b/executor/seqtest/seq_executor_test.go index c5ee8df40bee3..cfea57658df11 100644 --- a/executor/seqtest/seq_executor_test.go +++ b/executor/seqtest/seq_executor_test.go @@ -504,19 +504,24 @@ func TestShow(t *testing.T) { // Test range columns partition tk.MustExec(`drop table if exists t`) - tk.MustExec(`CREATE TABLE t (a int, b int, c char, d int) PARTITION BY RANGE COLUMNS(a,d,c) ( + tk.MustExec(`CREATE TABLE t (a int, b int, c varchar(25), d int) PARTITION BY RANGE COLUMNS(a,d,c) ( PARTITION p0 VALUES LESS THAN (5,10,'ggg'), PARTITION p1 VALUES LESS THAN (10,20,'mmm'), PARTITION p2 VALUES LESS THAN (15,30,'sss'), PARTITION p3 VALUES LESS THAN (50,MAXVALUE,MAXVALUE))`) + tk.MustQuery("show warnings").Check(testkit.Rows()) tk.MustQuery("show create table t").Check(testkit.RowsWithSep("|", "t CREATE TABLE `t` (\n"+ " `a` int(11) DEFAULT NULL,\n"+ " `b` int(11) DEFAULT NULL,\n"+ - " `c` char(1) DEFAULT NULL,\n"+ + " `c` varchar(25) DEFAULT NULL,\n"+ " `d` int(11) DEFAULT NULL\n"+ - ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin", - )) + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin\n"+ + "PARTITION BY RANGE COLUMNS(`a`,`d`,`c`)\n"+ + "(PARTITION `p0` VALUES LESS THAN (5,10,'ggg'),\n"+ + " PARTITION `p1` VALUES LESS THAN (10,20,'mmm'),\n"+ + " PARTITION `p2` VALUES LESS THAN (15,30,'sss'),\n"+ + " PARTITION `p3` VALUES LESS THAN (50,MAXVALUE,MAXVALUE))")) // Test hash partition tk.MustExec(`drop table if exists t`) diff --git a/executor/showtest/show_test.go b/executor/showtest/show_test.go index 3945a42ce0a37..e9fc181c0700e 100644 --- a/executor/showtest/show_test.go +++ b/executor/showtest/show_test.go @@ -472,6 +472,17 @@ func TestShowCreateTable(t *testing.T) { "t CREATE TABLE `t` (\n"+ " `a` bit(1) DEFAULT rand()\n"+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin")) + + tk.MustExec(`drop table if exists t`) + err := tk.ExecToErr(`create table t (a varchar(255) character set ascii) partition by range columns (a) (partition p values less than (0xff))`) + require.ErrorContains(t, err, "[ddl:1654]Partition column values of incorrect type") + tk.MustExec(`create table t (a varchar(255) character set ascii) partition by range columns (a) (partition p values less than (0x7f))`) + tk.MustQuery(`show create table t`).Check(testkit.Rows( + "t CREATE TABLE `t` (\n" + + " `a` varchar(255) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL\n" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin\n" + + "PARTITION BY RANGE COLUMNS(`a`)\n" + + "(PARTITION `p` VALUES LESS THAN (x'7f'))")) } func TestShowCreateTablePlacement(t *testing.T) { @@ -589,21 +600,27 @@ func TestShowCreateTablePlacement(t *testing.T) { )) tk.MustExec(`DROP TABLE IF EXISTS t`) - // RANGE COLUMNS with multiple columns is not supported! + tk.MustExec("create table t(a int, b varchar(255))" + "/*T![placement] PLACEMENT POLICY=\"x\" */" + "PARTITION BY RANGE COLUMNS (a,b)\n" + "(PARTITION pLow VALUES less than (1000000,'1000000') COMMENT 'a comment' placement policy 'x'," + " PARTITION pMidLow VALUES less than (1000000,MAXVALUE) COMMENT 'another comment' placement policy 'x'," + - " PARTITION pMadMax VALUES less than (MAXVALUE,'1000000') COMMENT ='Not a comment' placement policy 'x'," + - "partition pMax values LESS THAN (MAXVALUE, MAXVALUE))") - tk.MustQuery("show warnings").Check(testkit.Rows("Warning 8200 Unsupported partition type RANGE, treat as normal table")) - tk.MustQuery(`show create table t`).Check(testkit.RowsWithSep("|", ""+ - "t CREATE TABLE `t` (\n"+ - " `a` int(11) DEFAULT NULL,\n"+ - " `b` varchar(255) DEFAULT NULL\n"+ - ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin /*T![placement] PLACEMENT POLICY=`x` */", - )) + " PARTITION pMadMax VALUES less than (9000000,'1000000') COMMENT ='Not a comment' placement policy 'x'," + + "partition pMax values LESS THAN (MAXVALUE, 'Does not matter...'))") + tk.MustQuery("show warnings").Check(testkit.Rows()) + tk.MustExec(`insert into t values (1,'1')`) + tk.MustQuery("select * from t").Check(testkit.Rows("1 1")) + tk.MustQuery(`show create table t`).Check(testkit.Rows( + "t CREATE TABLE `t` (\n" + + " `a` int(11) DEFAULT NULL,\n" + + " `b` varchar(255) DEFAULT NULL\n" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin /*T![placement] PLACEMENT POLICY=`x` */\n" + + "PARTITION BY RANGE COLUMNS(`a`,`b`)\n" + + "(PARTITION `pLow` VALUES LESS THAN (1000000,'1000000') COMMENT 'a comment' /*T![placement] PLACEMENT POLICY=`x` */,\n" + + " PARTITION `pMidLow` VALUES LESS THAN (1000000,MAXVALUE) COMMENT 'another comment' /*T![placement] PLACEMENT POLICY=`x` */,\n" + + " PARTITION `pMadMax` VALUES LESS THAN (9000000,'1000000') COMMENT 'Not a comment' /*T![placement] PLACEMENT POLICY=`x` */,\n" + + " PARTITION `pMax` VALUES LESS THAN (MAXVALUE,'Does not matter...'))")) tk.MustExec(`DROP TABLE IF EXISTS t`) tk.MustExec("create table t(a int, b varchar(255))" + diff --git a/planner/core/integration_partition_test.go b/planner/core/integration_partition_test.go index 353fc379be794..d9e49f47981a9 100644 --- a/planner/core/integration_partition_test.go +++ b/planner/core/integration_partition_test.go @@ -17,16 +17,20 @@ package core_test import ( "bytes" "fmt" + "math" "math/rand" + "strconv" "strings" "testing" "github.com/pingcap/failpoint" + "github.com/pingcap/tidb/errno" "github.com/pingcap/tidb/parser/auth" "github.com/pingcap/tidb/planner/core" "github.com/pingcap/tidb/session" "github.com/pingcap/tidb/testkit" "github.com/pingcap/tidb/testkit/testdata" + "github.com/pingcap/tidb/util/benchdaily" "github.com/stretchr/testify/require" ) @@ -62,9 +66,6 @@ func TestListPartitionPushDown(t *testing.T) { } func TestListColVariousTypes(t *testing.T) { - failpoint.Enable("github.com/pingcap/tidb/planner/core/forceDynamicPrune", `return(true)`) - defer failpoint.Disable("github.com/pingcap/tidb/planner/core/forceDynamicPrune") - store := testkit.CreateMockStore(t) tk := testkit.NewTestKit(t, store) @@ -88,6 +89,9 @@ func TestListColVariousTypes(t *testing.T) { tk.MustExec(`insert into tint values (0), (1), (2), (3)`) tk.MustExec(`insert into tdate values ('2000-01-01'), ('2000-01-02'), ('2000-01-03'), ('2000-01-04')`) tk.MustExec(`insert into tstring values ('a'), ('b'), ('c'), ('d')`) + tk.MustExec(`analyze table tint`) + tk.MustExec(`analyze table tdate`) + tk.MustExec(`analyze table tstring`) var input []string var output []struct { @@ -126,6 +130,8 @@ func TestListPartitionPruning(t *testing.T) { partition p1 values in (3, 4, 5), partition p2 values in (6, 7, 8), partition p3 values in (9, 10, 11))`) + tk.MustExec(`analyze table tlist`) + tk.MustExec(`analyze table tcollist`) var input []string var output []struct { @@ -1114,3 +1120,483 @@ func TestIssue27532(t *testing.T) { tk.MustQuery(`select * from t2`).Sort().Check(testkit.Rows("1 1 1 1", "2 2 2 2", "3 3 3 3", "4 4 4 4")) tk.MustExec(`drop table t2`) } + +func TestRangeColumnsMultiColumn(t *testing.T) { + store := testkit.CreateMockStore(t) + + tk := testkit.NewTestKit(t, store) + tk.MustExec("create database RangeColumnsMulti") + tk.MustExec("use RangeColumnsMulti") + + tk.MustGetErrCode(`create table t (a int, b datetime, c varchar(255)) partition by range columns (a,b,c)`+ + `(partition p0 values less than (NULL,NULL,NULL))`, + errno.ErrWrongTypeColumnValue) + tk.MustGetErrCode(`create table t (a int, b datetime, c varchar(255)) partition by range columns (a,b,c)`+ + `(partition p1 values less than (`+strconv.FormatInt(math.MinInt32-1, 10)+`,'0000-00-00',""))`, + errno.ErrWrongTypeColumnValue) + tk.MustExec(`create table t (a int, b datetime, c varchar(255)) partition by range columns (a,b,c)` + + `(partition p1 values less than (` + strconv.FormatInt(math.MinInt32, 10) + `,'0000-00-00',""),` + + `partition p2 values less than (10,'2022-01-01',"Wow"),` + + `partition p3 values less than (11,'2022-01-01',MAXVALUE),` + + `partition p4 values less than (MAXVALUE,'2022-01-01',"Wow"))`) + tk.MustGetErrCode(`insert into t values (`+strconv.FormatInt(math.MinInt32, 10)+`,'0000-00-00',null)`, errno.ErrTruncatedWrongValue) + tk.MustExec(`insert into t values (NULL,NULL,NULL)`) + tk.MustExec(`set @@sql_mode = ''`) + tk.MustExec(`insert into t values (` + strconv.FormatInt(math.MinInt32, 10) + `,'0000-00-00',null)`) + tk.MustExec(`insert into t values (` + strconv.FormatInt(math.MinInt32, 10) + `,'0000-00-00',"")`) + tk.MustExec(`insert into t values (5,'0000-00-00',null)`) + tk.MustQuery(`show warnings`).Check(testkit.Rows()) + tk.MustExec(`insert into t values (5,'0000-00-00',"Hi")`) + tk.MustQuery(`show warnings`).Check(testkit.Rows()) + tk.MustExec(`set @@sql_mode = DEFAULT`) + tk.MustExec(`insert into t values (10,'2022-01-01',"Hi")`) + tk.MustExec(`insert into t values (10,'2022-01-01',"Wow")`) + tk.MustExec(`insert into t values (10,'2022-01-01',"Wowe")`) + tk.MustExec(`insert into t values (11,'2022-01-01',"Wow")`) + tk.MustExec(`insert into t values (1,null,"Wow")`) + tk.MustExec(`insert into t values (NULL,'2022-01-01',"Wow")`) + tk.MustExec(`insert into t values (11,null,"Wow")`) + tk.MustExec(`analyze table t`) + tk.MustQuery(`select a,b,c from t partition(p1)`).Sort().Check(testkit.Rows( + "-2147483648 0000-00-00 00:00:00 ", + " 2022-01-01 00:00:00 Wow", + " ")) + tk.MustQuery(`select a,b,c from t partition(p2)`).Sort().Check(testkit.Rows( + "-2147483648 0000-00-00 00:00:00 ", + "1 Wow", + "10 2022-01-01 00:00:00 Hi", + "5 0000-00-00 00:00:00 ", + "5 0000-00-00 00:00:00 Hi")) + tk.MustQuery(`select a,b,c from t partition(p3)`).Sort().Check(testkit.Rows( + "10 2022-01-01 00:00:00 Wow", + "10 2022-01-01 00:00:00 Wowe", + "11 2022-01-01 00:00:00 Wow", + "11 Wow")) + tk.MustQuery(`select * from t where a = 10 and b = "2022-01-01" and c = "Wow"`).Sort().Check(testkit.Rows( + "10 2022-01-01 00:00:00 Wow")) + tk.MustQuery(`select * from t where a = 10 and b = "2022-01-01" and c <= "Wow"`).Sort().Check(testkit.Rows( + "10 2022-01-01 00:00:00 Hi", + "10 2022-01-01 00:00:00 Wow")) + tk.MustQuery(`select * from t where a = 10 and b = "2022-01-01" and c < "Wow"`).Sort().Check(testkit.Rows( + "10 2022-01-01 00:00:00 Hi")) + tk.MustQuery(`select * from t where a = 10 and b = "2022-01-01" and c > "Wow"`).Sort().Check(testkit.Rows( + "10 2022-01-01 00:00:00 Wowe")) + tk.MustQuery(`select * from t where a = 10 and b = "2022-01-01" and c >= "Wow"`).Sort().Check(testkit.Rows( + "10 2022-01-01 00:00:00 Wow", + "10 2022-01-01 00:00:00 Wowe")) + tk.MustQuery(`explain format = 'brief' select * from t where a = 10 and b = "2022-01-01" and c = "Wow"`).Check(testkit.Rows( + "TableReader 0.52 root partition:p3 data:Selection", + `└─Selection 0.52 cop[tikv] eq(rangecolumnsmulti.t.a, 10), eq(rangecolumnsmulti.t.b, 2022-01-01 00:00:00.000000), eq(rangecolumnsmulti.t.c, "Wow")`, + ` └─TableFullScan 12.00 cop[tikv] table:t keep order:false`)) + tk.MustQuery(`explain format = 'brief' select * from t where a = 10 and b = "2022-01-01" and c <= "Wow"`).Check(testkit.Rows( + `TableReader 0.83 root partition:p2,p3 data:Selection`, + `└─Selection 0.83 cop[tikv] eq(rangecolumnsmulti.t.a, 10), eq(rangecolumnsmulti.t.b, 2022-01-01 00:00:00.000000), le(rangecolumnsmulti.t.c, "Wow")`, + ` └─TableFullScan 12.00 cop[tikv] table:t keep order:false`)) + tk.MustQuery(`explain format = 'brief' select * from t where a = 10 and b = "2022-01-01" and c < "Wow"`).Check(testkit.Rows( + `TableReader 0.31 root partition:p2 data:Selection`, + `└─Selection 0.31 cop[tikv] eq(rangecolumnsmulti.t.a, 10), eq(rangecolumnsmulti.t.b, 2022-01-01 00:00:00.000000), lt(rangecolumnsmulti.t.c, "Wow")`, + ` └─TableFullScan 12.00 cop[tikv] table:t keep order:false`)) + tk.MustQuery(`explain format = 'brief' select * from t where a = 10 and b = "2022-01-01" and c > "Wow"`).Check(testkit.Rows( + `TableReader 0.10 root partition:p3 data:Selection`, + `└─Selection 0.10 cop[tikv] eq(rangecolumnsmulti.t.a, 10), eq(rangecolumnsmulti.t.b, 2022-01-01 00:00:00.000000), gt(rangecolumnsmulti.t.c, "Wow")`, + ` └─TableFullScan 12.00 cop[tikv] table:t keep order:false`)) + tk.MustQuery(`explain format = 'brief' select * from t where a = 10 and b = "2022-01-01" and c >= "Wow"`).Check(testkit.Rows( + `TableReader 0.62 root partition:p3 data:Selection`, + `└─Selection 0.62 cop[tikv] eq(rangecolumnsmulti.t.a, 10), eq(rangecolumnsmulti.t.b, 2022-01-01 00:00:00.000000), ge(rangecolumnsmulti.t.c, "Wow")`, + ` └─TableFullScan 12.00 cop[tikv] table:t keep order:false`)) + tk.MustQuery(`select * from t where a <= 10 and b <= '2022-01-01' and c < "Wow"`).Sort().Check(testkit.Rows( + "-2147483648 0000-00-00 00:00:00 ", + "10 2022-01-01 00:00:00 Hi", + "5 0000-00-00 00:00:00 Hi")) + tk.MustQuery(`select * from t where a = 10 and b = "2022-01-01" and c = "Wow"`).Sort().Check(testkit.Rows( + "10 2022-01-01 00:00:00 Wow")) + tk.MustQuery(`select * from t where a <= 10 and b <= '2022-01-01' and c <= "Wow"`).Sort().Check(testkit.Rows( + "-2147483648 0000-00-00 00:00:00 ", + "10 2022-01-01 00:00:00 Hi", + "10 2022-01-01 00:00:00 Wow", + "5 0000-00-00 00:00:00 Hi")) + tk.MustQuery(`explain format = 'brief' select * from t where a <= 10 and b <= '2022-01-01' and c < "Wow"`).Check(testkit.Rows( + `TableReader 1.50 root partition:p1,p2,p3 data:Selection`, + `└─Selection 1.50 cop[tikv] le(rangecolumnsmulti.t.a, 10), le(rangecolumnsmulti.t.b, 2022-01-01 00:00:00.000000), lt(rangecolumnsmulti.t.c, "Wow")`, + ` └─TableFullScan 12.00 cop[tikv] table:t keep order:false`)) + tk.MustQuery(`select * from t where a <= 11 and b <= '2022-01-01' and c < "Wow"`).Sort().Check(testkit.Rows( + "-2147483648 0000-00-00 00:00:00 ", + "10 2022-01-01 00:00:00 Hi", + "5 0000-00-00 00:00:00 Hi")) + // Possible optimization: p3 should not be included!!! The range optimizer will just use a <= 10 here + // But same with non-partitioned index, so the range optimizer needs to be improved. + tk.MustQuery(`explain format = 'brief' select * from t where a <= 10 and b <= '2022-01-01' and c < "Wow"`).Check(testkit.Rows( + `TableReader 1.50 root partition:p1,p2,p3 data:Selection`, + `└─Selection 1.50 cop[tikv] le(rangecolumnsmulti.t.a, 10), le(rangecolumnsmulti.t.b, 2022-01-01 00:00:00.000000), lt(rangecolumnsmulti.t.c, "Wow")`, + ` └─TableFullScan 12.00 cop[tikv] table:t keep order:false`)) + tk.MustExec(`create table tref (a int, b datetime, c varchar(255), key (a,b,c))`) + tk.MustExec(`set @@sql_mode = ''`) + tk.MustExec(`insert into tref select * from t`) + tk.MustExec(`set @@sql_mode = DEFAULT`) + tk.MustQuery(`explain format = 'brief' select * from tref where a <= 10 and b <= '2022-01-01' and c < "Wow"`).Check(testkit.Rows( + `IndexReader 367.05 root index:Selection`, + `└─Selection 367.05 cop[tikv] le(rangecolumnsmulti.tref.b, 2022-01-01 00:00:00.000000), lt(rangecolumnsmulti.tref.c, "Wow")`, + ` └─IndexRangeScan 3323.33 cop[tikv] table:tref, index:a(a, b, c) range:[-inf,10], keep order:false, stats:pseudo`)) + tk.MustQuery(`explain format = 'brief' select * from t where a <= 10 and b <= '2022-01-01' and c <= "Wow"`).Check(testkit.Rows( + `TableReader 4.00 root partition:p1,p2,p3 data:Selection`, + `└─Selection 4.00 cop[tikv] le(rangecolumnsmulti.t.a, 10), le(rangecolumnsmulti.t.b, 2022-01-01 00:00:00.000000), le(rangecolumnsmulti.t.c, "Wow")`, + ` └─TableFullScan 12.00 cop[tikv] table:t keep order:false`)) + tk.MustQuery(`select * from t where a = 2 and b = "2022-01-02" and c = "Hi" or b = '2022-01-01' and c = "Wow"`).Sort().Check(testkit.Rows( + "10 2022-01-01 00:00:00 Wow", + "11 2022-01-01 00:00:00 Wow", + " 2022-01-01 00:00:00 Wow")) + tk.MustQuery(`select * from t where a = 2 and b = "2022-01-02" and c = "Hi" or a = 10 and b = '2022-01-01' and c = "Wow"`).Sort().Check(testkit.Rows("10 2022-01-01 00:00:00 Wow")) + tk.MustQuery(`select * from t where a = 2 and b = "2022-01-02" and c = "Hi"`).Sort().Check(testkit.Rows()) + tk.MustQuery(`select * from t where a = 2 and b = "2022-01-02" and c < "Hi"`).Sort().Check(testkit.Rows()) + tk.MustQuery(`select * from t where a < 2`).Sort().Check(testkit.Rows( + "-2147483648 0000-00-00 00:00:00 ", + "-2147483648 0000-00-00 00:00:00 ", + "1 Wow")) + tk.MustQuery(`select * from t where a <= 2 and b <= "2022-01-02" and c < "Hi"`).Sort().Check(testkit.Rows( + "-2147483648 0000-00-00 00:00:00 ")) + tk.MustQuery(`explain format = 'brief' select * from t where a < 2`).Check(testkit.Rows( + "TableReader 3.00 root partition:p1,p2 data:Selection", + "└─Selection 3.00 cop[tikv] lt(rangecolumnsmulti.t.a, 2)", + " └─TableFullScan 12.00 cop[tikv] table:t keep order:false")) + tk.MustQuery(`select * from t where a < 2 and a > -22`).Sort().Check(testkit.Rows( + "1 Wow")) + tk.MustQuery(`explain format = 'brief' select * from t where a < 2 and a > -22`).Check(testkit.Rows( + "TableReader 1.00 root partition:p2 data:Selection", + "└─Selection 1.00 cop[tikv] gt(rangecolumnsmulti.t.a, -22), lt(rangecolumnsmulti.t.a, 2)", + " └─TableFullScan 12.00 cop[tikv] table:t keep order:false")) + tk.MustQuery(`select * from t where c = ""`).Sort().Check(testkit.Rows("-2147483648 0000-00-00 00:00:00 ")) + tk.MustQuery(`explain format = 'brief' select * from t where c = ""`).Check(testkit.Rows( + "TableReader 1.00 root partition:all data:Selection", + `└─Selection 1.00 cop[tikv] eq(rangecolumnsmulti.t.c, "")`, + " └─TableFullScan 12.00 cop[tikv] table:t keep order:false")) +} + +func TestRangeMultiColumnsPruning(t *testing.T) { + store := testkit.CreateMockStore(t) + + tk := testkit.NewTestKit(t, store) + tk.MustExec("create database RColumnsMulti") + tk.MustExec("use RColumnsMulti") + tk.MustExec(`create table t (a int, b datetime, c varchar(255), key (a,b,c))` + + ` partition by range columns (a,b,c) ` + + `(partition p0 values less than (-2147483648, '0000-01-01', ""),` + + ` partition p1 values less than (-2147483648, '0001-01-01', ""),` + + ` partition p2 values less than (-2, '0001-01-01', ""),` + + ` partition p3 values less than (0, '0001-01-01', ""),` + + ` partition p4 values less than (0, '2031-01-01', ""),` + + ` partition p5 values less than (0, '2031-01-01', "Wow"),` + + ` partition p6 values less than (0, '2031-01-01', MAXVALUE),` + + ` partition p7 values less than (0, MAXVALUE, MAXVALUE),` + + ` partition p8 values less than (MAXVALUE, MAXVALUE, MAXVALUE))`) + tk.MustGetErrCode(`insert into t values (`+strconv.FormatInt(math.MinInt32, 10)+`,'0000-00-00',null)`, errno.ErrTruncatedWrongValue) + tk.MustExec(`insert into t values (NULL,NULL,NULL)`) + tk.MustExec(`set @@sql_mode = ''`) + tk.MustExec(`insert into t values (` + strconv.FormatInt(math.MinInt32, 10) + `,'0000-00-00',null)`) + tk.MustExec(`insert into t values (` + strconv.FormatInt(math.MinInt32, 10) + `,'0000-00-00',"")`) + tk.MustExec(`insert into t values (5,'0000-00-00',null)`) + tk.MustQuery(`show warnings`).Check(testkit.Rows()) + tk.MustExec(`insert into t values (5,'0000-00-00',"Hi")`) + tk.MustQuery(`show warnings`).Check(testkit.Rows()) + tk.MustExec(`set @@sql_mode = DEFAULT`) + tk.MustExec(`insert into t values (10,'2022-01-01',"Hi")`) + tk.MustExec(`insert into t values (10,'2022-01-01',"Wow")`) + tk.MustExec(`insert into t values (11,'2022-01-01',"Wow")`) + tk.MustExec(`insert into t values (0,'2020-01-01',"Wow")`) + tk.MustExec(`insert into t values (1,null,"Wow")`) + tk.MustExec(`insert into t values (NULL,'2022-01-01',"Wow")`) + tk.MustExec(`insert into t values (11,null,"Wow")`) + tk.MustExec(`analyze table t`) + tk.MustQuery(`select a,b from t where b = '2022-01-01'`).Sort().Check(testkit.Rows( + "10 2022-01-01 00:00:00", + "10 2022-01-01 00:00:00", + "11 2022-01-01 00:00:00", + " 2022-01-01 00:00:00")) + tk.MustQuery(`select a,b,c from t where a = 1`).Check(testkit.Rows("1 Wow")) + tk.MustQuery(`select a,b,c from t where a = 1 AND c = "Wow"`).Check(testkit.Rows("1 Wow")) + tk.MustQuery(`explain format = 'brief' select a,b,c from t where a = 1 AND c = "Wow"`).Check(testkit.Rows( + `IndexReader 0.50 root partition:p8 index:Selection`, + `└─Selection 0.50 cop[tikv] eq(rcolumnsmulti.t.c, "Wow")`, + ` └─IndexRangeScan 1.00 cop[tikv] table:t, index:a(a, b, c) range:[1,1], keep order:false`)) + // WAS HERE, Why is the start return TRUE making this to work and FALSE disapear? + tk.MustQuery(`select a,b,c from t where a = 0 AND c = "Wow"`).Check(testkit.Rows("0 2020-01-01 00:00:00 Wow")) + tk.MustQuery(`explain format = 'brief' select a,b,c from t where a = 0 AND c = "Wow"`).Check(testkit.Rows( + `IndexReader 0.50 root partition:p3,p4,p5,p6,p7,p8 index:Selection`, + `└─Selection 0.50 cop[tikv] eq(rcolumnsmulti.t.c, "Wow")`, + ` └─IndexRangeScan 1.00 cop[tikv] table:t, index:a(a, b, c) range:[0,0], keep order:false`)) +} + +func TestRangeColumnsExpr(t *testing.T) { + store := testkit.CreateMockStore(t) + tk := testkit.NewTestKit(t, store) + tk.MustExec(`create database rce`) + tk.MustExec(`use rce`) + tk.MustExec(`create table tref (a int unsigned, b int, c int)`) + tk.MustExec(`create table t (a int unsigned, b int, c int) partition by range columns (a,b) ` + + `(partition p0 values less than (3, MAXVALUE), ` + + ` partition p1 values less than (4, -2147483648), ` + + ` partition p2 values less than (4, 1), ` + + ` partition p3 values less than (4, 4), ` + + ` partition p4 values less than (4, 7), ` + + ` partition p5 values less than (4, 11), ` + + ` partition p6 values less than (4, 14), ` + + ` partition p7 values less than (4, 17), ` + + ` partition p8 values less than (4, MAXVALUE), ` + + ` partition p9 values less than (7, 0), ` + + ` partition p10 values less than (11, MAXVALUE), ` + + ` partition p11 values less than (14, -2147483648), ` + + ` partition p12 values less than (17, 17), ` + + ` partition p13 values less than (MAXVALUE, -2147483648))`) + allRows := []string{ + "0 0 0", + "11 2147483647 2147483647", + "14 10 4", + "14 2", + "14 ", + "17 16 16", + "17 17 17", + "3 2147483647 9", + "4 -2147483648 -2147483648", + "4 1 1", + "4 10 3", + "4 13 1", + "4 14 2", + "4 2147483647 2147483647", + "4 4 4", + "4 5 6", + "4 4", + "5 0 0", + "7 0 0", + " -2147483648 ", + " ", + } + insertStr := []string{} + for _, row := range allRows { + s := strings.ReplaceAll(row, " ", ",") + s = strings.ReplaceAll(s, "", "NULL") + insertStr = append(insertStr, "("+s+")") + } + tk.MustExec(`insert into t values ` + strings.Join(insertStr, ",")) + tk.MustExec(`insert into tref select * from t`) + tk.MustExec(`analyze table t`) + tk.MustQuery(`select * from tref`).Sort().Check(testkit.Rows(allRows...)) + tk.MustQuery(`select * from t`).Sort().Check(testkit.Rows(allRows...)) + tk.MustQuery(`select * from t partition (p0)`).Sort().Check(testkit.Rows( + "0 0 0", + "3 2147483647 9", + " -2147483648 ", + " ")) + tk.MustQuery(`select * from t partition (p1)`).Sort().Check(testkit.Rows( + "4 4")) + tk.MustQuery(`select * from t partition (p2)`).Sort().Check(testkit.Rows( + "4 -2147483648 -2147483648")) + tk.MustQuery(`select * from t partition (p3)`).Sort().Check(testkit.Rows( + "4 1 1")) + tk.MustQuery(`select * from t partition (p4)`).Sort().Check(testkit.Rows( + "4 4 4", + "4 5 6")) + tk.MustQuery(`select * from t partition (p5)`).Sort().Check(testkit.Rows( + "4 10 3")) + tk.MustQuery(`select * from t partition (p6)`).Sort().Check(testkit.Rows( + "4 13 1")) + tk.MustQuery(`select * from t partition (p7)`).Sort().Check(testkit.Rows( + "4 14 2")) + tk.MustQuery(`select * from t partition (p8)`).Sort().Check(testkit.Rows( + "4 2147483647 2147483647")) + tk.MustQuery(`select * from t partition (p9)`).Sort().Check(testkit.Rows( + "5 0 0")) + tk.MustQuery(`select * from t partition (p10)`).Sort().Check(testkit.Rows( + "11 2147483647 2147483647", + "7 0 0")) + tk.MustQuery(`select * from t partition (p11)`).Sort().Check(testkit.Rows( + "14 2", + "14 ")) + tk.MustQuery(`select * from t partition (p12)`).Sort().Check(testkit.Rows( + "14 10 4", + "17 16 16")) + tk.MustQuery(`select * from t partition (p13)`).Sort().Check(testkit.Rows( + "17 17 17")) + tk.MustQuery(`explain format = 'brief' select * from t where c = 3`).Check(testkit.Rows( + "TableReader 1.00 root partition:all data:Selection", + "└─Selection 1.00 cop[tikv] eq(rce.t.c, 3)", + " └─TableFullScan 21.00 cop[tikv] table:t keep order:false")) + tk.MustQuery(`explain format = 'brief' select * from t where b > 3 and c = 3`).Check(testkit.Rows( + "TableReader 0.52 root partition:all data:Selection", + "└─Selection 0.52 cop[tikv] eq(rce.t.c, 3), gt(rce.t.b, 3)", + " └─TableFullScan 21.00 cop[tikv] table:t keep order:false")) + tk.MustQuery(`explain format = 'brief' select * from t where a = 5 and c = 3`).Check(testkit.Rows( + "TableReader 0.05 root partition:p9 data:Selection", + "└─Selection 0.05 cop[tikv] eq(rce.t.a, 5), eq(rce.t.c, 3)", + " └─TableFullScan 21.00 cop[tikv] table:t keep order:false")) + tk.MustQuery(`explain format = 'brief' select * from t where a = 4 and c = 3`).Check(testkit.Rows( + "TableReader 0.43 root partition:p1,p2,p3,p4,p5,p6,p7,p8,p9 data:Selection", + "└─Selection 0.43 cop[tikv] eq(rce.t.a, 4), eq(rce.t.c, 3)", + " └─TableFullScan 21.00 cop[tikv] table:t keep order:false")) + tk.MustQuery(`explain format = 'brief' select * from t where a in (4,14) and c = 3`).Check(testkit.Rows( + "TableReader 0.57 root partition:p1,p2,p3,p4,p5,p6,p7,p8,p9,p11,p12 data:Selection", + "└─Selection 0.57 cop[tikv] eq(rce.t.c, 3), in(rce.t.a, 4, 14)", + " └─TableFullScan 21.00 cop[tikv] table:t keep order:false")) + tk.MustQuery(`explain format = 'brief' select * from t where a in (4,14) and b in (null,10)`).Check(testkit.Rows( + "TableReader 1.14 root partition:p5,p12 data:Selection", + "└─Selection 1.14 cop[tikv] in(rce.t.a, 4, 14), in(rce.t.b, NULL, 10)", + " └─TableFullScan 21.00 cop[tikv] table:t keep order:false")) + tk.MustQuery(`select * from tref where a in (4,14) and b in (null,10)`).Check(testkit.Rows( + "4 10 3", + "14 10 4")) + tk.MustQuery(`select * from t where a in (4,14) and b in (null,10)`).Check(testkit.Rows( + "4 10 3", + "14 10 4")) + tk.MustQuery(`explain format = 'brief' select * from t where a in (4,14) and (b in (11,10) OR b is null)`).Check(testkit.Rows( + "TableReader 3.43 root partition:p1,p5,p6,p11,p12 data:Selection", + "└─Selection 3.43 cop[tikv] in(rce.t.a, 4, 14), or(in(rce.t.b, 11, 10), isnull(rce.t.b))", + " └─TableFullScan 21.00 cop[tikv] table:t keep order:false")) + tk.MustQuery(`select * from tref where a in (4,14) and (b in (11,10) OR b is null)`).Sort().Check(testkit.Rows( + "14 10 4", + "14 2", + "14 ", + "4 10 3", + "4 4")) + tk.MustQuery(`select * from t where a in (4,14) and (b in (11,10) OR b is null)`).Sort().Check(testkit.Rows( + "14 10 4", + "14 2", + "14 ", + "4 10 3", + "4 4")) +} + +func TestPartitionRangePrunerCharWithCollation(t *testing.T) { + store := testkit.CreateMockStore(t) + tk := testkit.NewTestKit(t, store) + tk.MustExec(`create database cwc`) + tk.MustExec(`use cwc`) + // "'c'", "'F'", "'h'", "'L'", "'t'", "MAXVALUE" + tk.MustExec( + `create table t (a char(32) collate utf8mb4_unicode_ci) ` + + `partition by range columns (a) ` + + `(partition p0 values less than ('c'),` + + ` partition p1 values less than ('F'),` + + ` partition p2 values less than ('h'),` + + ` partition p3 values less than ('L'),` + + ` partition p4 values less than ('t'),` + + ` partition p5 values less than (MAXVALUE))`) + + tk.MustExec(`insert into t values ('a'),('A'),('c'),('C'),('f'),('F'),('h'),('H'),('l'),('L'),('t'),('T'),('z'),('Z')`) + tk.MustExec(`analyze table t`) + tk.MustQuery(`select * from t partition(p0)`).Sort().Check(testkit.Rows("A", "a")) + tk.MustQuery(`select * from t partition(p1)`).Sort().Check(testkit.Rows("C", "c")) + tk.MustQuery(`select * from t partition(p2)`).Sort().Check(testkit.Rows("F", "f")) + tk.MustQuery(`select * from t partition(p3)`).Sort().Check(testkit.Rows("H", "h")) + tk.MustQuery(`select * from t partition(p4)`).Sort().Check(testkit.Rows("L", "l")) + tk.MustQuery(`select * from t partition(p5)`).Sort().Check(testkit.Rows("T", "Z", "t", "z")) + tk.MustQuery(`select * from t where a > 'C' and a < 'q'`).Sort().Check(testkit.Rows("F", "H", "L", "f", "h", "l")) + tk.MustQuery(`select * from t where a > 'c' and a < 'Q'`).Sort().Check(testkit.Rows("F", "H", "L", "f", "h", "l")) + tk.MustQuery(`explain format = 'brief' select * from t where a > 'C' and a < 'q'`).Check(testkit.Rows( + `TableReader 6.00 root partition:p1,p2,p3,p4 data:Selection`, + `└─Selection 6.00 cop[tikv] gt(cwc.t.a, "C"), lt(cwc.t.a, "q")`, + ` └─TableFullScan 14.00 cop[tikv] table:t keep order:false`)) + tk.MustQuery(`explain format = 'brief' select * from t where a > 'c' and a < 'Q'`).Check(testkit.Rows( + `TableReader 6.00 root partition:p1,p2,p3,p4 data:Selection`, + `└─Selection 6.00 cop[tikv] gt(cwc.t.a, "c"), lt(cwc.t.a, "Q")`, + ` └─TableFullScan 14.00 cop[tikv] table:t keep order:false`)) +} + +func TestPartitionRangePrunerDate(t *testing.T) { + store := testkit.CreateMockStore(t) + tk := testkit.NewTestKit(t, store) + tk.MustExec(`create database rcd`) + tk.MustExec(`use rcd`) + tk.MustExec(`set @@tidb_partition_prune_mode = 'dynamic'`) + tk.MustExec(`create table i (a int, b int, key (a,b))`) + tk.MustQuery(`select * from i where a < 1 and a > 2`).Check(testkit.Rows()) + tk.MustQuery(`explain format = 'brief' select * from i where a < 1 and a > 2`).Check(testkit.Rows("TableDual 0.00 root rows:0")) + tk.MustExec( + `create table t (a date) ` + + `partition by range columns (a) ` + + `(partition p0 values less than ('19990601'),` + + ` partition p1 values less than ('2000-05-01'),` + + ` partition p2 values less than ('20080401'),` + + ` partition p3 values less than ('2010-03-01'),` + + ` partition p4 values less than ('20160201'),` + + ` partition p5 values less than ('2020-01-01'),` + + ` partition p6 values less than (MAXVALUE))`) + tk.MustQuery(`show create table t`).Check(testkit.Rows( + "t CREATE TABLE `t` (\n" + + " `a` date DEFAULT NULL\n" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin\n" + + "PARTITION BY RANGE COLUMNS(`a`)\n" + + "(PARTITION `p0` VALUES LESS THAN ('19990601'),\n" + + " PARTITION `p1` VALUES LESS THAN ('2000-05-01'),\n" + + " PARTITION `p2` VALUES LESS THAN ('20080401'),\n" + + " PARTITION `p3` VALUES LESS THAN ('2010-03-01'),\n" + + " PARTITION `p4` VALUES LESS THAN ('20160201'),\n" + + " PARTITION `p5` VALUES LESS THAN ('2020-01-01'),\n" + + " PARTITION `p6` VALUES LESS THAN (MAXVALUE))")) + tk.MustExec(`insert into t values ('19990101'),('1999-06-01'),('2000-05-01'),('20080401'),('2010-03-01'),('2016-02-01'),('2020-01-01')`) + tk.MustExec(`analyze table t`) + tk.MustQuery(`select * from t partition(p0)`).Sort().Check(testkit.Rows("1999-01-01")) + tk.MustQuery(`select * from t partition(p1)`).Sort().Check(testkit.Rows("1999-06-01")) + tk.MustQuery(`select * from t partition(p2)`).Sort().Check(testkit.Rows("2000-05-01")) + tk.MustQuery(`select * from t partition(p3)`).Sort().Check(testkit.Rows("2008-04-01")) + tk.MustQuery(`select * from t partition(p4)`).Sort().Check(testkit.Rows("2010-03-01")) + tk.MustQuery(`select * from t partition(p5)`).Sort().Check(testkit.Rows("2016-02-01")) + tk.MustQuery(`select * from t partition(p6)`).Sort().Check(testkit.Rows("2020-01-01")) + tk.UsedPartitions(`select * from t where a < '1943-02-12'`).Check(testkit.Rows("p0")) + tk.UsedPartitions(`select * from t where a >= '19690213'`).Check(testkit.Rows("all")) + tk.UsedPartitions(`select * from t where a > '2003-03-13'`).Check(testkit.Rows("p2 p3 p4 p5 p6")) + tk.UsedPartitions(`select * from t where a < '2006-02-03'`).Check(testkit.Rows("p0 p1 p2")) + tk.UsedPartitions(`select * from t where a = '20070707'`).Check(testkit.Rows("p2")) + tk.UsedPartitions(`select * from t where a > '1949-10-10'`).Check(testkit.Rows("all")) + tk.UsedPartitions(`select * from t where a > '2016-02-01' AND a < '20000103'`).Check(testkit.Rows("dual")) + tk.UsedPartitions(`select * from t where a < '19691112' or a >= '2019-09-18'`).Check(testkit.Rows("p0 p5 p6")) + tk.UsedPartitions(`select * from t where a is null`).Check(testkit.Rows("p0")) + tk.UsedPartitions(`select * from t where '2003-02-27' >= a`).Check(testkit.Rows("p0 p1 p2")) + tk.UsedPartitions(`select * from t where '20141024' < a`).Check(testkit.Rows("p4 p5 p6")) + tk.UsedPartitions(`select * from t where '2003-03-30' > a`).Check(testkit.Rows("p0 p1 p2")) + tk.UsedPartitions(`select * from t where a between '2003-03-30' AND '2014-01-01'`).Check(testkit.Rows("p2 p3 p4")) +} + +func BenchmarkPartitionRangeColumns(b *testing.B) { + store := testkit.CreateMockStore(b) + + tk := testkit.NewTestKit(b, store) + tk.MustExec("set @@tidb_partition_prune_mode = 'dynamic'") + tk.MustExec("create schema rcb") + tk.MustExec("use rcb") + tk.MustExec(`create table t (` + + `c1 int primary key clustered,` + + `c2 varchar(255))` + + ` partition by range columns (c1)` + + ` interval (10000) first partition less than (10000) last partition less than (5120000)`) + b.ResetTimer() + for i := 0; i < b.N; i++ { + val := strconv.FormatInt(int64(rand.Intn(5120000)), 10) + tk.MustExec("select * from t where c1 = " + val) + //tk.MustExec("insert ignore into t values (" + val + ",'" + val + "')") + } + b.StopTimer() +} + +func TestBenchDaily(t *testing.T) { + benchdaily.Run( + BenchmarkPartitionRangeColumns, + ) +} + +func TestPartitionRangeColumnPruning(t *testing.T) { + store := testkit.CreateMockStore(t) + tk := testkit.NewTestKit(t, store) + tk.MustExec(`create database rcd`) + tk.MustExec(`use rcd`) + tk.MustExec(`create table t1 (a char, b char, c char) ` + + `partition by range columns (a,b,c) ` + + `( partition p0 values less than ('a','b','c'),` + + ` partition p1 values less than ('b','c','d'),` + + ` partition p2 values less than ('d','e','f'))`) + tk.MustExec(`insert into t1 values ('a', NULL, 'd')`) + tk.MustExec(`analyze table t1`) + tk.MustQuery(`explain format=brief select * from t1 where a = 'a' AND c = 'd'`).Check(testkit.Rows( + `TableReader 1.00 root partition:p0,p1 data:Selection`, + `└─Selection 1.00 cop[tikv] eq(rcd.t1.a, "a"), eq(rcd.t1.c, "d")`, + ` └─TableFullScan 1.00 cop[tikv] table:t1 keep order:false`)) + tk.MustQuery(`select * from t1 where a = 'a' AND c = 'd'`).Check(testkit.Rows("a d")) + tk.MustExec(`drop table t1`) +} diff --git a/planner/core/partition_pruning_test.go b/planner/core/partition_pruning_test.go index 9d0f11aeeb579..3059567da6d5a 100644 --- a/planner/core/partition_pruning_test.go +++ b/planner/core/partition_pruning_test.go @@ -15,6 +15,9 @@ package core import ( + "math" + "strconv" + "strings" "testing" "github.com/pingcap/tidb/expression" @@ -126,6 +129,38 @@ type testCtx struct { fn *expression.ScalarFunction } +func prepareBenchCtx(createTable string, partitionExpr string) *testCtx { + p := parser.New() + stmt, err := p.ParseOneStmt(createTable, "", "") + if err != nil { + return nil + } + sctx := mock.NewContext() + tblInfo, err := ddlhelper.BuildTableInfoFromAST(stmt.(*ast.CreateTableStmt)) + if err != nil { + return nil + } + columns, names, err := expression.ColumnInfos2ColumnsAndNames(sctx, model.NewCIStr("t"), tblInfo.Name, tblInfo.Cols(), tblInfo) + if err != nil { + return nil + } + schema := expression.NewSchema(columns...) + + col, fn, _, err := makePartitionByFnCol(sctx, columns, names, partitionExpr) + if err != nil { + return nil + } + return &testCtx{ + require: nil, + sctx: sctx, + schema: schema, + columns: columns, + names: names, + col: col, + fn: fn, + } +} + func prepareTestCtx(t *testing.T, createTable string, partitionExpr string) *testCtx { p := parser.New() stmt, err := p.ParseOneStmt(createTable, "", "") @@ -159,11 +194,12 @@ func (tc *testCtx) expr(expr string) []expression.Expression { func TestPartitionRangeForExpr(t *testing.T) { tc := prepareTestCtx(t, "create table t (a int)", "a") lessThan := lessThanDataInt{data: []int64{4, 7, 11, 14, 17, 0}, maxvalue: true} - prunner := &rangePruner{lessThan, tc.columns[0], nil, monotoneModeInvalid} + pruner := &rangePruner{lessThan, tc.columns[0], nil, monotoneModeInvalid} cases := []struct { input string result partitionRangeOR }{ + {"a < 2 and a > 10", partitionRangeOR{}}, {"a > 3", partitionRangeOR{{1, 6}}}, {"a < 3", partitionRangeOR{{0, 1}}}, {"a >= 11", partitionRangeOR{{3, 6}}}, @@ -182,7 +218,7 @@ func TestPartitionRangeForExpr(t *testing.T) { expr, err := expression.ParseSimpleExprsWithNames(tc.sctx, ca.input, tc.schema, tc.names) require.NoError(t, err) result := fullRange(lessThan.length()) - result = partitionRangeForExpr(tc.sctx, expr[0], prunner, result) + result = partitionRangeForExpr(tc.sctx, expr[0], pruner, result) require.Truef(t, equalPartitionRangeOR(ca.result, result), "unexpected: %v", ca.input) } } @@ -261,17 +297,23 @@ func TestPartitionRangeOperation(t *testing.T) { } } -func TestPartitionRangePrunner2VarChar(t *testing.T) { +func TestPartitionRangePruner2VarChar(t *testing.T) { tc := prepareTestCtx(t, "create table t (a varchar(32))", "a") - lessThanDataInt := []string{"'c'", "'f'", "'h'", "'l'", "'t'"} - lessThan := make([]expression.Expression, len(lessThanDataInt)+1) // +1 for maxvalue + lessThanDataInt := []string{"'c'", "'f'", "'h'", "'l'", "'t'", "maxvalue"} + lessThan := make([][]*expression.Expression, len(lessThanDataInt)) for i, str := range lessThanDataInt { - tmp, err := expression.ParseSimpleExprsWithNames(tc.sctx, str, tc.schema, tc.names) - require.NoError(t, err) - lessThan[i] = tmp[0] + e := make([]*expression.Expression, 0, 1) + if strings.EqualFold(str, "MAXVALUE") { + e = append(e, nil) + } else { + tmp, err := expression.ParseSimpleExprsWithNames(tc.sctx, str, tc.schema, tc.names) + require.NoError(t, err) + e = append(e, &tmp[0]) + } + lessThan[i] = e } - pruner := &rangeColumnsPruner{lessThan, tc.columns[0], true} + pruner := &rangeColumnsPruner{lessThan, tc.columns} cases := []struct { input string result partitionRangeOR @@ -300,20 +342,26 @@ func TestPartitionRangePrunner2VarChar(t *testing.T) { } } -func TestPartitionRangePrunner2CharWithCollation(t *testing.T) { +func TestPartitionRangePruner2CharWithCollation(t *testing.T) { tc := prepareTestCtx(t, "create table t (a char(32) collate utf8mb4_unicode_ci)", "a", ) - lessThanDataInt := []string{"'c'", "'F'", "'h'", "'L'", "'t'"} - lessThan := make([]expression.Expression, len(lessThanDataInt)+1) // +1 for maxvalue + lessThanDataInt := []string{"'c'", "'F'", "'h'", "'L'", "'t'", "MAXVALUE"} + lessThan := make([][]*expression.Expression, len(lessThanDataInt)) for i, str := range lessThanDataInt { - tmp, err := expression.ParseSimpleExprsWithNames(tc.sctx, str, tc.schema, tc.names) - require.NoError(t, err) - lessThan[i] = tmp[0] + e := make([]*expression.Expression, 0, 1) + if strings.EqualFold(str, "MAXVALUE") { + e = append(e, nil) + } else { + tmp, err := expression.ParseSimpleExprsWithNames(tc.sctx, str, tc.schema, tc.names) + require.NoError(t, err) + e = append(e, &tmp[0]) + } + lessThan[i] = e } - prunner := &rangeColumnsPruner{lessThan, tc.columns[0], true} + pruner := &rangeColumnsPruner{lessThan, tc.columns} cases := []struct { input string result partitionRangeOR @@ -339,12 +387,12 @@ func TestPartitionRangePrunner2CharWithCollation(t *testing.T) { expr, err := expression.ParseSimpleExprsWithNames(tc.sctx, ca.input, tc.schema, tc.names) require.NoError(t, err) result := fullRange(len(lessThan)) - result = partitionRangeForExpr(tc.sctx, expr[0], prunner, result) - require.Truef(t, equalPartitionRangeOR(ca.result, result), "unexpected: %v", ca.input) + result = partitionRangeForExpr(tc.sctx, expr[0], pruner, result) + require.Truef(t, equalPartitionRangeOR(ca.result, result), "unexpected: %v %v != %v", ca.input, ca.result, result) } } -func TestPartitionRangePrunner2Date(t *testing.T) { +func TestPartitionRangePruner2Date(t *testing.T) { tc := prepareTestCtx(t, "create table t (a date)", "a", @@ -355,38 +403,191 @@ func TestPartitionRangePrunner2Date(t *testing.T) { "'20080401'", "'2010-03-01'", "'20160201'", - "'2020-01-01'"} - lessThan := make([]expression.Expression, len(lessThanDataInt)) + "'2020-01-01'", + "MAXVALUE"} + lessThan := make([][]*expression.Expression, len(lessThanDataInt)) for i, str := range lessThanDataInt { - tmp, err := expression.ParseSimpleExprsWithNames(tc.sctx, str, tc.schema, tc.names) - require.NoError(t, err) - lessThan[i] = tmp[0] + e := make([]*expression.Expression, 0, 1) + if strings.EqualFold(str, "MAXVALUE") { + e = append(e, nil) + } else { + tmp, err := expression.ParseSimpleExprsWithNames(tc.sctx, str, tc.schema, tc.names) + require.NoError(t, err) + e = append(e, &tmp[0]) + } + lessThan[i] = e } - prunner := &rangeColumnsPruner{lessThan, tc.columns[0], false} + pruner := &rangeColumnsPruner{lessThan, tc.columns} cases := []struct { input string result partitionRangeOR }{ {"a < '1943-02-12'", partitionRangeOR{{0, 1}}}, - {"a >= '19690213'", partitionRangeOR{{0, 6}}}, - {"a > '2003-03-13'", partitionRangeOR{{2, 6}}}, + {"a >= '19690213'", partitionRangeOR{{0, 7}}}, + {"a > '2003-03-13'", partitionRangeOR{{2, 7}}}, {"a < '2006-02-03'", partitionRangeOR{{0, 3}}}, {"a = '20070707'", partitionRangeOR{{2, 3}}}, - {"a > '1949-10-10'", partitionRangeOR{{0, 6}}}, + {"a > '1949-10-10'", partitionRangeOR{{0, 7}}}, {"a > '2016-02-01' and a < '20000103'", partitionRangeOR{}}, - {"a < '19691112' or a >= '2019-09-18'", partitionRangeOR{{0, 1}, {5, 6}}}, + {"a < '19691112' or a >= '2019-09-18'", partitionRangeOR{{0, 1}, {5, 7}}}, {"a is null", partitionRangeOR{{0, 1}}}, {"'2003-02-27' >= a", partitionRangeOR{{0, 3}}}, - {"'20141024' < a", partitionRangeOR{{4, 6}}}, + {"'20141024' < a", partitionRangeOR{{4, 7}}}, {"'2003-03-30' > a", partitionRangeOR{{0, 3}}}, + {"'2003-03-30' < a AND a < '20080808'", partitionRangeOR{{2, 4}}}, + {"a between '2003-03-30' AND '20080808'", partitionRangeOR{{2, 4}}}, } for _, ca := range cases { expr, err := expression.ParseSimpleExprsWithNames(tc.sctx, ca.input, tc.schema, tc.names) require.NoError(t, err) result := fullRange(len(lessThan)) - result = partitionRangeForExpr(tc.sctx, expr[0], prunner, result) - require.Truef(t, equalPartitionRangeOR(ca.result, result), "unexpected: %v", ca.input) + result = partitionRangeForExpr(tc.sctx, expr[0], pruner, result) + require.Truef(t, equalPartitionRangeOR(ca.result, result), "unexpected: %v, %v != %v", ca.input, ca.result, result) + } +} + +func TestPartitionRangeColumnsForExpr(t *testing.T) { + tc := prepareTestCtx(t, "create table t (a int unsigned, b int, c int)", "a,b") + lessThan := make([][]*expression.Expression, 0, 6) + partDefs := [][]int64{{3, -99}, + {4, math.MinInt64}, + {4, 1}, + {4, 4}, + {4, 7}, + {4, 11}, // p5 + {4, 14}, + {4, 17}, + {4, -99}, + {7, 0}, + {11, -99}, // p10 + {14, math.MinInt64}, + {17, 17}, + {-99, math.MinInt64}} + for i := range partDefs { + l := make([]*expression.Expression, 0, 2) + for j := range []int{0, 1} { + v := partDefs[i][j] + var e *expression.Expression + if v == -99 { + e = nil // MAXVALUE + } else { + expr, err := expression.ParseSimpleExprsWithNames(tc.sctx, strconv.FormatInt(v, 10), tc.schema, tc.names) + require.NoError(t, err) + tmp := expr[0] + e = &tmp + } + l = append(l, e) + } + lessThan = append(lessThan, l) + } + pruner := &rangeColumnsPruner{lessThan, tc.columns[:2]} + cases := []struct { + input string + result partitionRangeOR + }{ + {"a < 1 and a > 1", partitionRangeOR{}}, + {"c = 3", partitionRangeOR{{0, len(partDefs)}}}, + {"b > 3 AND c = 3", partitionRangeOR{{0, len(partDefs)}}}, + {"a = 5 AND c = 3", partitionRangeOR{{9, 10}}}, + {"a = 4 AND c = 3", partitionRangeOR{{1, 10}}}, + {"b > 3", partitionRangeOR{{0, len(partDefs)}}}, + {"a > 3", partitionRangeOR{{1, len(partDefs)}}}, + {"a < 3", partitionRangeOR{{0, 1}}}, + {"a >= 11", partitionRangeOR{{10, len(partDefs)}}}, + {"a > 11", partitionRangeOR{{11, len(partDefs)}}}, + {"a > 4", partitionRangeOR{{9, len(partDefs)}}}, + {"a >= 4", partitionRangeOR{{1, len(partDefs)}}}, + {"a < 11", partitionRangeOR{{0, 11}}}, + {"a = 16", partitionRangeOR{{12, 13}}}, + {"a > 66", partitionRangeOR{{13, 14}}}, + {"a > 2 and a < 10", partitionRangeOR{{0, 11}}}, + {"a < 2 or a >= 15", partitionRangeOR{{0, 1}, {12, 14}}}, + {"a is null", partitionRangeOR{{0, 1}}}, + {"12 > a", partitionRangeOR{{0, 12}}}, + {"4 <= a", partitionRangeOR{{1, 14}}}, + // The expression is converted to 'if ...', see constructBinaryOpFunction, so not possible to break down to ranges + {"(a,b) < (4,4)", partitionRangeOR{{0, 14}}}, + {"(a,b) = (4,4)", partitionRangeOR{{4, 5}}}, + {"a < 4 OR (a = 4 AND b < 4)", partitionRangeOR{{0, 4}}}, + // The expression is converted to 'if ...', see constructBinaryOpFunction, so not possible to break down to ranges + {"(a,b,c) < (4,4,4)", partitionRangeOR{{0, 14}}}, + {"a < 4 OR (a = 4 AND b < 4) OR (a = 4 AND b = 4 AND c < 4)", partitionRangeOR{{0, 5}}}, + {"(a,b,c) >= (4,7,4)", partitionRangeOR{{0, len(partDefs)}}}, + {"(a,b,c) = (4,7,4)", partitionRangeOR{{5, 6}}}, + {"a < 2 and a > 10", partitionRangeOR{}}, + {"a < 1 and a > 1", partitionRangeOR{}}, + } + + for _, ca := range cases { + exprs, err := expression.ParseSimpleExprsWithNames(tc.sctx, ca.input, tc.schema, tc.names) + require.NoError(t, err) + result := fullRange(len(lessThan)) + e := expression.SplitCNFItems(exprs[0]) + result = partitionRangeForCNFExpr(tc.sctx, e, pruner, result) + require.Truef(t, equalPartitionRangeOR(ca.result, result), "unexpected: %v %v != %v", ca.input, ca.result, result) + } +} + +func benchmarkRangeColumnsPruner(b *testing.B, parts int) { + tc := prepareBenchCtx("create table t (a bigint unsigned, b int, c int)", "a") + if tc == nil { + panic("Failed to initialize benchmark") + } + lessThan := make([][]*expression.Expression, 0, parts) + partDefs := make([][]int64, 0, parts) + for i := 0; i < parts-1; i++ { + partDefs = append(partDefs, []int64{int64(i * 10000)}) } + partDefs = append(partDefs, []int64{-99}) + for i := range partDefs { + v := partDefs[i][0] + var e *expression.Expression + if v == -99 { + e = nil // MAXVALUE + } else { + expr, err := expression.ParseSimpleExprsWithNames(tc.sctx, strconv.FormatInt(v, 10), tc.schema, tc.names) + if err != nil { + panic(err.Error()) + } + tmp := expr[0] + e = &tmp + } + lessThan = append(lessThan, []*expression.Expression{e}) + } + pruner := &rangeColumnsPruner{lessThan, tc.columns[:1]} + + exprs, err := expression.ParseSimpleExprsWithNames(tc.sctx, "a > 11000", tc.schema, tc.names) + if err != nil { + panic(err.Error()) + } + result := fullRange(len(lessThan)) + e := expression.SplitCNFItems(exprs[0]) + b.ResetTimer() + for i := 0; i < b.N; i++ { + result[0] = partitionRange{0, parts} + result = result[:1] + result = partitionRangeForCNFExpr(tc.sctx, e, pruner, result) + } +} + +func BenchmarkRangeColumnsPruner2(b *testing.B) { + benchmarkRangeColumnsPruner(b, 2) +} + +func BenchmarkRangeColumnsPruner10(b *testing.B) { + benchmarkRangeColumnsPruner(b, 10) +} + +func BenchmarkRangeColumnsPruner100(b *testing.B) { + benchmarkRangeColumnsPruner(b, 100) +} + +func BenchmarkRangeColumnsPruner1000(b *testing.B) { + benchmarkRangeColumnsPruner(b, 1000) +} + +func BenchmarkRangeColumnsPruner8000(b *testing.B) { + benchmarkRangeColumnsPruner(b, 8000) } diff --git a/planner/core/rule_partition_processor.go b/planner/core/rule_partition_processor.go index b24e64e7e402e..e44e9e1af27a0 100644 --- a/planner/core/rule_partition_processor.go +++ b/planner/core/rule_partition_processor.go @@ -706,7 +706,7 @@ func (lt *lessThanDataInt) compare(ith int, v int64, unsigned bool) int { return -1 } -// partitionRange represents [start, range) +// partitionRange represents [start, end) type partitionRange struct { start int end int @@ -832,7 +832,7 @@ func (s *partitionProcessor) pruneRangePartition(ctx sessionctx.Context, pi *mod // Partition by range columns. if len(pi.Columns) > 0 { - result, err := s.pruneRangeColumnsPartition(ctx, conds, pi, partExpr, columns, names) + result, err := s.pruneRangeColumnsPartition(ctx, conds, pi, partExpr, columns) return result, err } @@ -925,8 +925,173 @@ func makePartitionByFnCol(sctx sessionctx.Context, columns []*expression.Column, return col, fn, monotonous, nil } +func minCmp(ctx sessionctx.Context, lowVal []types.Datum, columnsPruner *rangeColumnsPruner, comparer []collate.Collator, lowExclude bool, gotError *bool) func(i int) bool { + return func(i int) bool { + for j := range lowVal { + expr := columnsPruner.lessThan[i][j] + + if expr == nil { + // MAXVALUE + return true + } + if con, ok := (*expr).(*expression.Constant); ok { + // Add Null as point here? + cmp, err := con.Value.Compare(ctx.GetSessionVars().StmtCtx, &lowVal[j], comparer[j]) + if err != nil { + *gotError = true + } + if cmp > 0 { + return true + } + if cmp < 0 { + return false + } + } else { + // Not a constant, pruning not possible, so value is considered less than all partitions + return true + } + } + if len(lowVal) < len(columnsPruner.lessThan[i]) { + // Not all columns given + if lowExclude { + // prefix cols > const, do not include this partition + return false + } + + colIdx := len(lowVal) + col := columnsPruner.partCols[colIdx] + conExpr := columnsPruner.lessThan[i][colIdx] + if conExpr == nil { + // MAXVALUE + return true + } + + // Possible to optimize by getting minvalue of the column type + // and if lessThan is equal to that + // we can return false, since the partition definition is + // LESS THAN (..., colN, minValOfColM, ... ) which cannot match colN == LowVal + if !mysql.HasNotNullFlag(col.RetType.GetFlag()) { + // NULL cannot be part of the partitioning expression: VALUES LESS THAN (NULL...) + // NULL is allowed in the column and will be considered as lower than any other value + // so this partition needs to be included! + return true + } + if con, ok := (*conExpr).(*expression.Constant); ok && col != nil { + switch col.RetType.EvalType() { + case types.ETInt: + if mysql.HasUnsignedFlag(col.RetType.GetFlag()) { + if con.Value.GetUint64() == 0 { + return false + } + } else { + if con.Value.GetInt64() == types.IntergerSignedLowerBound(col.GetType().GetType()) { + return false + } + } + case types.ETDatetime: + if con.Value.GetMysqlTime().IsZero() { + return false + } + case types.ETString: + if len(con.Value.GetString()) == 0 { + return false + } + } + } + // Also if not a constant, pruning not possible, so value is considered less than all partitions + return true + } + return false + } +} + +func maxCmp(ctx sessionctx.Context, hiVal []types.Datum, columnsPruner *rangeColumnsPruner, comparer []collate.Collator, hiExclude bool, gotError *bool) func(i int) bool { + return func(i int) bool { + for j := range hiVal { + expr := columnsPruner.lessThan[i][j] + if expr == nil { + // MAXVALUE + return true + } + if con, ok := (*expr).(*expression.Constant); ok { + // Add Null as point here? + cmp, err := con.Value.Compare(ctx.GetSessionVars().StmtCtx, &hiVal[j], comparer[j]) + if err != nil { + *gotError = true + // error pushed, we will still use the cmp value + } + if cmp > 0 { + return true + } + if cmp < 0 { + return false + } + } else { + // Not a constant, include every partition, i.e. value is not less than any partition + return false + } + } + // if point is included, then false, due to LESS THAN + return hiExclude + } +} + +func multiColumnRangeColumnsPruner(sctx sessionctx.Context, exprs []expression.Expression, + columnsPruner *rangeColumnsPruner, result partitionRangeOR) partitionRangeOR { + lens := make([]int, 0, len(columnsPruner.partCols)) + for i := range columnsPruner.partCols { + lens = append(lens, columnsPruner.partCols[i].RetType.GetFlen()) + } + + res, err := ranger.DetachCondAndBuildRangeForIndex(sctx, exprs, columnsPruner.partCols, lens) + if err != nil { + return fullRange(len(columnsPruner.lessThan)) + } + if len(res.Ranges) == 0 { + if len(res.AccessConds) == 0 && len(res.RemainedConds) == 0 { + // Impossible conditions, like: a > 2 AND a < 1 + return partitionRangeOR{} + } + // Could not extract any valid range, use all partitions + return fullRange(len(columnsPruner.lessThan)) + } + + rangeOr := make([]partitionRange, 0, len(res.Ranges)) + + comparer := make([]collate.Collator, 0, len(columnsPruner.partCols)) + for i := range columnsPruner.partCols { + comparer = append(comparer, collate.GetCollator(columnsPruner.partCols[i].RetType.GetCollate())) + } + gotError := false + // Create a sort.Search where the compare loops over ColumnValues + // Loop over the different ranges and extend/include all the partitions found + for idx := range res.Ranges { + minComparer := minCmp(sctx, res.Ranges[idx].LowVal, columnsPruner, comparer, res.Ranges[idx].LowExclude, &gotError) + maxComparer := maxCmp(sctx, res.Ranges[idx].HighVal, columnsPruner, comparer, res.Ranges[idx].HighExclude, &gotError) + if gotError { + // the compare function returned error, use all partitions. + return fullRange(len(columnsPruner.lessThan)) + } + // Can optimize if the range start is types.KindNull/types.MinNotNull + // or range end is types.KindMaxValue + start := sort.Search(len(columnsPruner.lessThan), minComparer) + end := sort.Search(len(columnsPruner.lessThan), maxComparer) + + if end < len(columnsPruner.lessThan) { + end++ + } + rangeOr = append(rangeOr, partitionRange{start, end}) + } + return result.intersection(rangeOr).simplify() +} + func partitionRangeForCNFExpr(sctx sessionctx.Context, exprs []expression.Expression, pruner partitionRangePruner, result partitionRangeOR) partitionRangeOR { + // TODO: When the ranger/detacher handles varchar_col_general_ci cmp constant bin collation + // remove the check for single column RANGE COLUMNS and remove the single column implementation + if columnsPruner, ok := pruner.(*rangeColumnsPruner); ok && len(columnsPruner.partCols) > 1 { + return multiColumnRangeColumnsPruner(sctx, exprs, columnsPruner, result) + } for i := 0; i < len(exprs); i++ { result = partitionRangeForExpr(sctx, exprs[i], pruner, result) } @@ -1014,7 +1179,7 @@ func partitionRangeForOrExpr(sctx sessionctx.Context, expr1, expr2 expression.Ex func partitionRangeColumnForInExpr(sctx sessionctx.Context, args []expression.Expression, pruner *rangeColumnsPruner) partitionRangeOR { col, ok := args[0].(*expression.Column) - if !ok || col.ID != pruner.partCol.ID { + if !ok || col.ID != pruner.partCols[0].ID { return pruner.fullRange() } @@ -1400,7 +1565,7 @@ func appendWarnForUnknownPartitions(ctx sessionctx.Context, hintName string, unk return } - warning := fmt.Errorf("Unknown partitions (%s) in optimizer hint %s", strings.Join(unknownPartitions, ","), hintName) + warning := fmt.Errorf("unknown partitions (%s) in optimizer hint %s", strings.Join(unknownPartitions, ","), hintName) ctx.GetSessionVars().StmtCtx.AppendWarning(warning) } @@ -1474,14 +1639,14 @@ func (s *partitionProcessor) makeUnionAllChildren(ds *DataSource, pi *model.Part return unionAll, nil } -func (s *partitionProcessor) pruneRangeColumnsPartition(ctx sessionctx.Context, conds []expression.Expression, pi *model.PartitionInfo, pe *tables.PartitionExpr, columns []*expression.Column, names types.NameSlice) (partitionRangeOR, error) { +func (s *partitionProcessor) pruneRangeColumnsPartition(ctx sessionctx.Context, conds []expression.Expression, pi *model.PartitionInfo, pe *tables.PartitionExpr, columns []*expression.Column) (partitionRangeOR, error) { result := fullRange(len(pi.Definitions)) - if len(pi.Columns) != 1 { + if len(pi.Columns) < 1 { return result, nil } - pruner, err := makeRangeColumnPruner(columns, names, pi, pe.ForRangeColumnsPruning) + pruner, err := makeRangeColumnPruner(columns, pi, pe.ForRangeColumnsPruning, pe.ColumnOffset) if err == nil { result = partitionRangeForCNFExpr(ctx, conds, pruner, result) } @@ -1492,61 +1657,88 @@ var _ partitionRangePruner = &rangeColumnsPruner{} // rangeColumnsPruner is used by 'partition by range columns'. type rangeColumnsPruner struct { - data []expression.Expression - partCol *expression.Column - maxvalue bool + lessThan [][]*expression.Expression + partCols []*expression.Column } -func makeRangeColumnPruner(columns []*expression.Column, names types.NameSlice, pi *model.PartitionInfo, from *tables.ForRangeColumnsPruning) (*rangeColumnsPruner, error) { +func makeRangeColumnPruner(columns []*expression.Column, pi *model.PartitionInfo, from *tables.ForRangeColumnsPruning, offsets []int) (*rangeColumnsPruner, error) { + if len(pi.Definitions) != len(from.LessThan) { + return nil, errors.Trace(fmt.Errorf("internal error len(pi.Definitions) != len(from.LessThan) %d != %d", len(pi.Definitions), len(from.LessThan))) + } schema := expression.NewSchema(columns...) - idx := expression.FindFieldNameIdxByColName(names, pi.Columns[0].L) - partCol := schema.Columns[idx] - data := make([]expression.Expression, len(from.LessThan)) - for i := 0; i < len(from.LessThan); i++ { - if from.LessThan[i] != nil { - data[i] = from.LessThan[i].Clone() + partCols := make([]*expression.Column, len(offsets)) + for i, offset := range offsets { + partCols[i] = schema.Columns[offset] + } + lessThan := make([][]*expression.Expression, 0, len(from.LessThan)) + for i := range from.LessThan { + colVals := make([]*expression.Expression, 0, len(from.LessThan[i])) + for j := range from.LessThan[i] { + if from.LessThan[i][j] != nil { + tmp := (*from.LessThan[i][j]).Clone() + colVals = append(colVals, &tmp) + } else { + colVals = append(colVals, nil) + } } + lessThan = append(lessThan, colVals) } - return &rangeColumnsPruner{data, partCol, from.MaxValue}, nil + return &rangeColumnsPruner{lessThan, partCols}, nil } func (p *rangeColumnsPruner) fullRange() partitionRangeOR { - return fullRange(len(p.data)) + return fullRange(len(p.lessThan)) +} + +func (p *rangeColumnsPruner) getPartCol(colID int64) *expression.Column { + for i := range p.partCols { + if colID == p.partCols[i].ID { + return p.partCols[i] + } + } + return nil } func (p *rangeColumnsPruner) partitionRangeForExpr(sctx sessionctx.Context, expr expression.Expression) (int, int, bool) { op, ok := expr.(*expression.ScalarFunction) if !ok { - return 0, len(p.data), false + return 0, len(p.lessThan), false } switch op.FuncName.L { case ast.EQ, ast.LT, ast.GT, ast.LE, ast.GE: case ast.IsNull: // isnull(col) - if arg0, ok := op.GetArgs()[0].(*expression.Column); ok && arg0.ID == p.partCol.ID { + if arg0, ok := op.GetArgs()[0].(*expression.Column); ok && len(p.partCols) == 1 && arg0.ID == p.partCols[0].ID { + // Single column RANGE COLUMNS, NULL sorts before all other values: match first partition return 0, 1, true } - return 0, len(p.data), false + return 0, len(p.lessThan), false default: - return 0, len(p.data), false + return 0, len(p.lessThan), false } opName := op.FuncName.L var col *expression.Column var con *expression.Constant - if arg0, ok := op.GetArgs()[0].(*expression.Column); ok && arg0.ID == p.partCol.ID { - if arg1, ok := op.GetArgs()[1].(*expression.Constant); ok { - col, con = arg0, arg1 - } - } else if arg0, ok := op.GetArgs()[1].(*expression.Column); ok && arg0.ID == p.partCol.ID { - if arg1, ok := op.GetArgs()[0].(*expression.Constant); ok { - opName = opposite(opName) - col, con = arg0, arg1 - } + var argCol0, argCol1 *expression.Column + var argCon0, argCon1 *expression.Constant + var okCol0, okCol1, okCon0, okCon1 bool + argCol0, okCol0 = op.GetArgs()[0].(*expression.Column) + argCol1, okCol1 = op.GetArgs()[1].(*expression.Column) + argCon0, okCon0 = op.GetArgs()[0].(*expression.Constant) + argCon1, okCon1 = op.GetArgs()[1].(*expression.Constant) + if okCol0 && okCon1 { + col, con = argCol0, argCon1 + } else if okCol1 && okCon0 { + col, con = argCol1, argCon0 + opName = opposite(opName) + } else { + return 0, len(p.lessThan), false } - if col == nil || con == nil { - return 0, len(p.data), false + partCol := p.getPartCol(col.ID) + if partCol == nil { + return 0, len(p.lessThan), false } // If different collation, we can only prune if: @@ -1554,9 +1746,9 @@ func (p *rangeColumnsPruner) partitionRangeForExpr(sctx sessionctx.Context, expr // - EQ operator, consider values 'a','b','ä' where 'ä' would be in the same partition as 'a' if general_ci, but is binary after 'b' // otherwise return all partitions / no pruning _, exprColl := expr.CharsetAndCollation() - colColl := p.partCol.RetType.GetCollate() + colColl := partCol.RetType.GetCollate() if exprColl != colColl && (opName != ast.EQ || !collate.IsBinCollation(exprColl)) { - return 0, len(p.data), true + return 0, len(p.lessThan), true } start, end := p.pruneUseBinarySearch(sctx, opName, con) return start, end, true @@ -1567,22 +1759,30 @@ func (p *rangeColumnsPruner) partitionRangeForExpr(sctx sessionctx.Context, expr func (p *rangeColumnsPruner) pruneUseBinarySearch(sctx sessionctx.Context, op string, data *expression.Constant) (start int, end int) { var err error var isNull bool - charSet, collation := p.partCol.RetType.GetCharset(), p.partCol.RetType.GetCollate() + if len(p.partCols) > 1 { + // Only one constant in the input, this will never be called with + // multi-column RANGE COLUMNS :) + return 0, len(p.lessThan) + } + charSet, collation := p.partCols[0].RetType.GetCharset(), p.partCols[0].RetType.GetCollate() compare := func(ith int, op string, v *expression.Constant) bool { - if ith == len(p.data)-1 { - if p.maxvalue { + for i := range p.partCols { + if p.lessThan[ith][i] == nil { // MAXVALUE + return true + } + var expr expression.Expression + expr, err = expression.NewFunctionBase(sctx, op, types.NewFieldType(mysql.TypeLonglong), *p.lessThan[ith][i], v) + expr.SetCharsetAndCollation(charSet, collation) + var val int64 + val, isNull, err = expr.EvalInt(sctx, chunk.Row{}) + if val > 0 { return true } } - var expr expression.Expression - expr, err = expression.NewFunctionBase(sctx, op, types.NewFieldType(mysql.TypeLonglong), p.data[ith], v) - expr.SetCharsetAndCollation(charSet, collation) - var val int64 - val, isNull, err = expr.EvalInt(sctx, chunk.Row{}) - return val > 0 + return false } - length := len(p.data) + length := len(p.lessThan) switch op { case ast.EQ: pos := sort.Search(length, func(i int) bool { return compare(i, ast.GT, data) }) @@ -1600,9 +1800,9 @@ func (p *rangeColumnsPruner) pruneUseBinarySearch(sctx sessionctx.Context, op st start, end = 0, length } - // Something goes wrong, abort this prunning. + // Something goes wrong, abort this pruning. if err != nil || isNull { - return 0, len(p.data) + return 0, len(p.lessThan) } if end > length { diff --git a/planner/core/testdata/integration_partition_suite_out.json b/planner/core/testdata/integration_partition_suite_out.json index 726b0f6598d6d..e9b75469bc21d 100644 --- a/planner/core/testdata/integration_partition_suite_out.json +++ b/planner/core/testdata/integration_partition_suite_out.json @@ -5,17 +5,17 @@ { "SQL": "explain format = 'brief' select * from tint where a<=1", "Results": [ - "TableReader 3323.33 root partition:p0 data:Selection", - "└─Selection 3323.33 cop[tikv] le(list_col_partition_types.tint.a, 1)", - " └─TableFullScan 10000.00 cop[tikv] table:tint keep order:false, stats:pseudo" + "TableReader 2.00 root partition:p0 data:Selection", + "└─Selection 2.00 cop[tikv] le(list_col_partition_types.tint.a, 1)", + " └─TableFullScan 4.00 cop[tikv] table:tint keep order:false" ] }, { "SQL": "explain format = 'brief' select * from tint where a in (0, 1)", "Results": [ - "TableReader 20.00 root partition:p0 data:Selection", - "└─Selection 20.00 cop[tikv] in(list_col_partition_types.tint.a, 0, 1)", - " └─TableFullScan 10000.00 cop[tikv] table:tint keep order:false, stats:pseudo" + "TableReader 2.00 root partition:p0 data:Selection", + "└─Selection 2.00 cop[tikv] in(list_col_partition_types.tint.a, 0, 1)", + " └─TableFullScan 4.00 cop[tikv] table:tint keep order:false" ] }, { @@ -35,17 +35,17 @@ { "SQL": "explain format = 'brief' select * from tdate where a<='2000-01-01'", "Results": [ - "TableReader 3323.33 root partition:p0 data:Selection", - "└─Selection 3323.33 cop[tikv] le(list_col_partition_types.tdate.a, 2000-01-01 00:00:00.000000)", - " └─TableFullScan 10000.00 cop[tikv] table:tdate keep order:false, stats:pseudo" + "TableReader 1.00 root partition:p0 data:Selection", + "└─Selection 1.00 cop[tikv] le(list_col_partition_types.tdate.a, 2000-01-01 00:00:00.000000)", + " └─TableFullScan 4.00 cop[tikv] table:tdate keep order:false" ] }, { "SQL": "explain format = 'brief' select * from tdate where a in ('2000-01-01', '2000-01-02')", "Results": [ - "TableReader 20.00 root partition:p0 data:Selection", - "└─Selection 20.00 cop[tikv] in(list_col_partition_types.tdate.a, 2000-01-01 00:00:00.000000, 2000-01-02 00:00:00.000000)", - " └─TableFullScan 10000.00 cop[tikv] table:tdate keep order:false, stats:pseudo" + "TableReader 2.00 root partition:p0 data:Selection", + "└─Selection 2.00 cop[tikv] in(list_col_partition_types.tdate.a, 2000-01-01 00:00:00.000000, 2000-01-02 00:00:00.000000)", + " └─TableFullScan 4.00 cop[tikv] table:tdate keep order:false" ] }, { @@ -64,17 +64,17 @@ { "SQL": "explain format = 'brief' select * from tstring where a<='b'", "Results": [ - "TableReader 3323.33 root partition:p0 data:Selection", - "└─Selection 3323.33 cop[tikv] le(list_col_partition_types.tstring.a, \"b\")", - " └─TableFullScan 10000.00 cop[tikv] table:tstring keep order:false, stats:pseudo" + "TableReader 2.00 root partition:p0 data:Selection", + "└─Selection 2.00 cop[tikv] le(list_col_partition_types.tstring.a, \"b\")", + " └─TableFullScan 4.00 cop[tikv] table:tstring keep order:false" ] }, { "SQL": "explain format = 'brief' select * from tstring where a in ('a', 'b')", "Results": [ - "TableReader 20.00 root partition:p0 data:Selection", - "└─Selection 20.00 cop[tikv] in(list_col_partition_types.tstring.a, \"a\", \"b\")", - " └─TableFullScan 10000.00 cop[tikv] table:tstring keep order:false, stats:pseudo" + "TableReader 2.00 root partition:p0 data:Selection", + "└─Selection 2.00 cop[tikv] in(list_col_partition_types.tstring.a, \"a\", \"b\")", + " └─TableFullScan 4.00 cop[tikv] table:tstring keep order:false" ] }, { diff --git a/planner/core/testdata/integration_suite_out.json b/planner/core/testdata/integration_suite_out.json index 0e59adaf03498..e43b448cfde72 100644 --- a/planner/core/testdata/integration_suite_out.json +++ b/planner/core/testdata/integration_suite_out.json @@ -846,7 +846,7 @@ " └─TableFullScan 10000.00 cop[tiflash] table:t, partition:p2 keep order:false, stats:pseudo" ], "Warn": [ - "Warning 1105 Unknown partitions (p0) in optimizer hint /*+ USE_INDEX(t PARTITION(p0, p1) b, c) */" + "Warning 1105 unknown partitions (p0) in optimizer hint /*+ USE_INDEX(t PARTITION(p0, p1) b, c) */" ] }, { @@ -859,7 +859,7 @@ " └─TableFullScan 10000.00 cop[tiflash] table:t, partition:p2 keep order:false, stats:pseudo" ], "Warn": [ - "Warning 1105 Unknown partitions (p_non_exist) in optimizer hint /*+ USE_INDEX(t PARTITION(p_non_exist)) */" + "Warning 1105 unknown partitions (p_non_exist) in optimizer hint /*+ USE_INDEX(t PARTITION(p_non_exist)) */" ] }, { diff --git a/table/tables/partition.go b/table/tables/partition.go index 95ee007754196..6a0b315b856e9 100644 --- a/table/tables/partition.go +++ b/table/tables/partition.go @@ -44,6 +44,7 @@ import ( "github.com/pingcap/tidb/util/logutil" "github.com/pingcap/tidb/util/mock" "github.com/pingcap/tidb/util/ranger" + "github.com/pingcap/tidb/util/stringutil" "go.uber.org/zap" ) @@ -114,6 +115,7 @@ func newPartitionedTable(tbl *TableCommon, tblInfo *model.TableInfo) (table.Tabl } func newPartitionExpr(tblInfo *model.TableInfo) (*PartitionExpr, error) { + // a partitioned table cannot rely on session context/sql modes, so use a default one! ctx := mock.NewContext() dbName := model.NewCIStr(ctx.GetSessionVars().CurrentDB) columns, names, err := expression.ColumnInfos2ColumnsAndNames(ctx, dbName, tblInfo.Name, tblInfo.Cols(), tblInfo) @@ -175,24 +177,30 @@ func initEvalBuffer(t *partitionedTable) *chunk.MutRow { // ForRangeColumnsPruning is used for range partition pruning. type ForRangeColumnsPruning struct { - LessThan []expression.Expression - MaxValue bool + // LessThan contains expressions for [Partition][column]. + // If Maxvalue, then nil + LessThan [][]*expression.Expression } func dataForRangeColumnsPruning(ctx sessionctx.Context, pi *model.PartitionInfo, schema *expression.Schema, names []*types.FieldName, p *parser.Parser) (*ForRangeColumnsPruning, error) { var res ForRangeColumnsPruning - res.LessThan = make([]expression.Expression, len(pi.Definitions)) + res.LessThan = make([][]*expression.Expression, 0, len(pi.Definitions)) for i := 0; i < len(pi.Definitions); i++ { - if strings.EqualFold(pi.Definitions[i].LessThan[0], "MAXVALUE") { - // Use a bool flag instead of math.MaxInt64 to avoid the corner cases. - res.MaxValue = true - } else { - tmp, err := parseSimpleExprWithNames(p, ctx, pi.Definitions[i].LessThan[0], schema, names) + lessThanCols := make([]*expression.Expression, 0, len(pi.Columns)) + for j := range pi.Definitions[i].LessThan { + if strings.EqualFold(pi.Definitions[i].LessThan[j], "MAXVALUE") { + // Use a nil pointer instead of math.MaxInt64 to avoid the corner cases. + lessThanCols = append(lessThanCols, nil) + // No column after MAXVALUE matters + break + } + tmp, err := parseSimpleExprWithNames(p, ctx, pi.Definitions[i].LessThan[j], schema, names) if err != nil { return nil, err } - res.LessThan[i] = tmp + lessThanCols = append(lessThanCols, &tmp) } + res.LessThan = append(res.LessThan, lessThanCols) } return &res, nil } @@ -468,20 +476,17 @@ func fixOldVersionPartitionInfo(sctx sessionctx.Context, str string) (int64, boo return ret, true } -// rangePartitionString returns the partition string for a range typed partition. -func rangePartitionString(pi *model.PartitionInfo) string { - // partition by range expr - if len(pi.Columns) == 0 { - return pi.Expr - } - - // partition by range columns (c1) - if len(pi.Columns) == 1 { - return "`" + pi.Columns[0].L + "`" +func rangePartitionExprStrings(pi *model.PartitionInfo) []string { + var s []string + if len(pi.Columns) > 0 { + s = make([]string, 0, len(pi.Columns)) + for _, col := range pi.Columns { + s = append(s, stringutil.Escape(col.O, mysql.ModeNone)) + } + } else { + s = []string{pi.Expr} } - - // partition by range columns (c1, c2, ...) - panic("create table assert len(columns) = 1") + return s } func generateRangePartitionExpr(ctx sessionctx.Context, pi *model.PartitionInfo, @@ -491,13 +496,24 @@ func generateRangePartitionExpr(ctx sessionctx.Context, pi *model.PartitionInfo, var buf bytes.Buffer p := parser.New() schema := expression.NewSchema(columns...) - partStr := rangePartitionString(pi) + partStrs := rangePartitionExprStrings(pi) for i := 0; i < len(pi.Definitions); i++ { if strings.EqualFold(pi.Definitions[i].LessThan[0], "MAXVALUE") { // Expr less than maxvalue is always true. fmt.Fprintf(&buf, "true") } else { - fmt.Fprintf(&buf, "((%s) < (%s))", partStr, pi.Definitions[i].LessThan[0]) + maxValueFound := false + for j := range partStrs[1:] { + if strings.EqualFold(pi.Definitions[i].LessThan[j+1], "MAXVALUE") { + // if any column will be less than MAXVALUE, so change < to <= of the previous prefix of columns + fmt.Fprintf(&buf, "((%s) <= (%s))", strings.Join(partStrs[:j+1], ","), strings.Join(pi.Definitions[i].LessThan[:j+1], ",")) + maxValueFound = true + break + } + } + if !maxValueFound { + fmt.Fprintf(&buf, "((%s) < (%s))", strings.Join(partStrs, ","), strings.Join(pi.Definitions[i].LessThan, ",")) + } } expr, err := parseSimpleExprWithNames(p, ctx, buf.String(), schema, names) @@ -513,42 +529,25 @@ func generateRangePartitionExpr(ctx sessionctx.Context, pi *model.PartitionInfo, UpperBounds: locateExprs, } - // build column offset. - partExp := pi.Expr - if len(pi.Columns) == 1 { - partExp = "`" + pi.Columns[0].L + "`" - } - exprs, err := parseSimpleExprWithNames(p, ctx, partExp, schema, names) + partExpr, _, offset, err := extractPartitionExprColumns(ctx, pi, columns, names) if err != nil { - return nil, err - } - partitionCols := expression.ExtractColumns(exprs) - offset := make([]int, len(partitionCols)) - for i, col := range columns { - for j, partitionCol := range partitionCols { - if partitionCol.UniqueID == col.UniqueID { - offset[j] = i - } - } + return nil, errors.Trace(err) } ret.ColumnOffset = offset - switch len(pi.Columns) { - case 0: + if len(pi.Columns) < 1 { tmp, err := dataForRangePruning(ctx, pi) if err != nil { return nil, errors.Trace(err) } - ret.Expr = exprs + ret.Expr = partExpr ret.ForRangePruning = tmp - case 1: + } else { tmp, err := dataForRangeColumnsPruning(ctx, pi, schema, names, p) if err != nil { return nil, errors.Trace(err) } ret.ForRangeColumnsPruning = tmp - default: - panic("range column partition currently support only one column") } return ret, nil } @@ -572,7 +571,7 @@ func findIdxByColUniqueID(cols []*expression.Column, col *expression.Column) int return -1 } -func extractListPartitionExprColumns(ctx sessionctx.Context, pi *model.PartitionInfo, columns []*expression.Column, names types.NameSlice) (expression.Expression, []*expression.Column, []int, error) { +func extractPartitionExprColumns(ctx sessionctx.Context, pi *model.PartitionInfo, columns []*expression.Column, names types.NameSlice) (expression.Expression, []*expression.Column, []int, error) { var cols []*expression.Column var partExpr expression.Expression if len(pi.Columns) == 0 { @@ -607,7 +606,7 @@ func generateListPartitionExpr(ctx sessionctx.Context, tblInfo *model.TableInfo, columns []*expression.Column, names types.NameSlice) (*PartitionExpr, error) { // The caller should assure partition info is not nil. pi := tblInfo.GetPartitionInfo() - partExpr, exprCols, offset, err := extractListPartitionExprColumns(ctx, pi, columns, names) + partExpr, exprCols, offset, err := extractPartitionExprColumns(ctx, pi, columns, names) if err != nil { return nil, err } @@ -1003,32 +1002,28 @@ func (t *partitionedTable) locatePartition(ctx sessionctx.Context, pi *model.Par } func (t *partitionedTable) locateRangeColumnPartition(ctx sessionctx.Context, pi *model.PartitionInfo, r []types.Datum) (int, error) { - var err error - var isNull bool + var lastError error partitionExprs := t.partitionExpr.UpperBounds evalBuffer := t.evalBufferPool.Get().(*chunk.MutRow) defer t.evalBufferPool.Put(evalBuffer) - var ret int64 idx := sort.Search(len(partitionExprs), func(i int) bool { evalBuffer.SetDatums(r...) - ret, isNull, err = partitionExprs[i].EvalInt(ctx, evalBuffer.ToRow()) + ret, isNull, err := partitionExprs[i].EvalInt(ctx, evalBuffer.ToRow()) if err != nil { - return true // Break the search. + lastError = err + return true // Does not matter, will propagate the last error anyway. } if isNull { // If the column value used to determine the partition is NULL, the row is inserted into the lowest partition. // See https://dev.mysql.com/doc/mysql-partitioning-excerpt/5.7/en/partitioning-handling-nulls.html - return true // Break the search. + return true // Always less than any other value (NULL cannot be in the partition definition VALUE LESS THAN). } return ret > 0 }) - if err != nil { - return 0, errors.Trace(err) - } - if isNull { - idx = 0 + if lastError != nil { + return 0, errors.Trace(lastError) } - if idx < 0 || idx >= len(partitionExprs) { + if idx >= len(partitionExprs) { // The data does not belong to any of the partition returns `table has no partition for value %s`. var valueMsg string if pi.Expr != "" {