Skip to content

Commit 5820f65

Browse files
mjonssti-chi-bot
authored andcommitted
This is an automated cherry-pick of pingcap#59612
Signed-off-by: ti-chi-bot <[email protected]>
1 parent 0cadc8e commit 5820f65

File tree

5 files changed

+1282
-0
lines changed

5 files changed

+1282
-0
lines changed

pkg/ddl/partition.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3644,6 +3644,7 @@ func buildCheckSQLConditionForRangeExprPartition(pi *model.PartitionInfo, index
36443644
// Since the pi.Expr string may contain the identifier, which couldn't be escaped in our ParseWithParams(...)
36453645
// So we write it to the origin sql string here.
36463646
if index == 0 {
3647+
// TODO: Handle MAXVALUE in first partition
36473648
buf.WriteString(pi.Expr)
36483649
buf.WriteString(" >= %?")
36493650
paramList = append(paramList, driver.UnwrapFromSingleQuotes(pi.Definitions[index].LessThan[0]))
@@ -3661,6 +3662,7 @@ func buildCheckSQLConditionForRangeExprPartition(pi *model.PartitionInfo, index
36613662
return buf.String(), paramList
36623663
}
36633664

3665+
<<<<<<< HEAD
36643666
func buildCheckSQLConditionForRangeColumnsPartition(pi *model.PartitionInfo, index int) (string, []interface{}) {
36653667
paramList := make([]interface{}, 0, 2)
36663668
colName := pi.Columns[0].L
@@ -3674,10 +3676,94 @@ func buildCheckSQLConditionForRangeColumnsPartition(pi *model.PartitionInfo, ind
36743676
paramList = append(paramList, colName, driver.UnwrapFromSingleQuotes(pi.Definitions[index-1].LessThan[0]), colName, driver.UnwrapFromSingleQuotes(pi.Definitions[index].LessThan[0]))
36753677
return "%n < %? or %n >= %?", paramList
36763678
}
3679+
=======
3680+
func buildCheckSQLConditionForRangeColumnsPartition(pi *model.PartitionInfo, index int) (string, []any) {
3681+
var buf strings.Builder
3682+
paramList := make([]any, 0, len(pi.Columns)*2)
3683+
3684+
hasLowerBound := index > 0
3685+
needOR := false
3686+
3687+
// Lower bound check (for all partitions except first)
3688+
if hasLowerBound {
3689+
currVals := pi.Definitions[index-1].LessThan
3690+
for i := 0; i < len(pi.Columns); i++ {
3691+
nextIsMax := false
3692+
if i < (len(pi.Columns)-1) && strings.EqualFold(currVals[i+1], partitionMaxValue) {
3693+
nextIsMax = true
3694+
}
3695+
if needOR {
3696+
buf.WriteString(" OR ")
3697+
}
3698+
if i > 0 {
3699+
buf.WriteString("(")
3700+
// All previous columns must be equal and non-NULL
3701+
for j := 0; j < i; j++ {
3702+
if j > 0 {
3703+
buf.WriteString(" AND ")
3704+
}
3705+
buf.WriteString("(%n = %?)")
3706+
paramList = append(paramList, pi.Columns[j].L, driver.UnwrapFromSingleQuotes(currVals[j]))
3707+
}
3708+
buf.WriteString(" AND ")
3709+
}
3710+
paramList = append(paramList, pi.Columns[i].L, driver.UnwrapFromSingleQuotes(currVals[i]), pi.Columns[i].L)
3711+
if nextIsMax {
3712+
buf.WriteString("(%n <= %? OR %n IS NULL)")
3713+
} else {
3714+
buf.WriteString("(%n < %? OR %n IS NULL)")
3715+
}
3716+
if i > 0 {
3717+
buf.WriteString(")")
3718+
}
3719+
needOR = true
3720+
if nextIsMax {
3721+
break
3722+
}
3723+
}
3724+
}
3725+
3726+
currVals := pi.Definitions[index].LessThan
3727+
// Upper bound check (for all partitions)
3728+
for i := 0; i < len(pi.Columns); i++ {
3729+
if strings.EqualFold(currVals[i], partitionMaxValue) {
3730+
break
3731+
}
3732+
if needOR {
3733+
buf.WriteString(" OR ")
3734+
}
3735+
if i > 0 {
3736+
buf.WriteString("(")
3737+
// All previous columns must be equal
3738+
for j := 0; j < i; j++ {
3739+
if j > 0 {
3740+
buf.WriteString(" AND ")
3741+
}
3742+
paramList = append(paramList, pi.Columns[j].L, driver.UnwrapFromSingleQuotes(currVals[j]))
3743+
buf.WriteString("(%n = %?)")
3744+
}
3745+
buf.WriteString(" AND ")
3746+
}
3747+
isLast := i == len(pi.Columns)-1
3748+
if isLast {
3749+
buf.WriteString("(%n >= %?)")
3750+
} else {
3751+
buf.WriteString("(%n > %?)")
3752+
}
3753+
paramList = append(paramList, pi.Columns[i].L, driver.UnwrapFromSingleQuotes(currVals[i]))
3754+
if i > 0 {
3755+
buf.WriteString(")")
3756+
}
3757+
needOR = true
3758+
}
3759+
3760+
return buf.String(), paramList
3761+
>>>>>>> d39268519f7 (ddl: Add checks for all partitioning columns during EXCHANGE PARTITION for RANGE COLUMNS (#59612))
36773762
}
36783763

36793764
func buildCheckSQLConditionForListPartition(pi *model.PartitionInfo, index int) string {
36803765
var buf strings.Builder
3766+
// TODO: Handle DEFAULT partition
36813767
buf.WriteString("not (")
36823768
for i, inValue := range pi.Definitions[index].InValues {
36833769
if i != 0 {
@@ -3699,14 +3785,19 @@ func buildCheckSQLConditionForListPartition(pi *model.PartitionInfo, index int)
36993785

37003786
func buildCheckSQLConditionForListColumnsPartition(pi *model.PartitionInfo, index int) string {
37013787
var buf strings.Builder
3788+
// TODO: Verify if this is correct!!!
3789+
// TODO: Handle DEFAULT partition!
3790+
// TODO: use paramList with column names, instead of quoting.
37023791
// How to find a match?
37033792
// (row <=> vals1) OR (row <=> vals2)
37043793
// How to find a non-matching row:
37053794
// NOT ( (row <=> vals1) OR (row <=> vals2) ... )
37063795
buf.WriteString("not (")
37073796
colNames := make([]string, 0, len(pi.Columns))
37083797
for i := range pi.Columns {
3798+
// TODO: Add test for this!
37093799
// TODO: check if there are no proper quoting function for this?
3800+
// TODO: Maybe Sprintf("%#q", str) ?
37103801
n := "`" + strings.ReplaceAll(pi.Columns[i].O, "`", "``") + "`"
37113802
colNames = append(colNames, n)
37123803
}

pkg/ddl/partition_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,41 @@ func TestReorganizePartitionRollback(t *testing.T) {
246246
// test then add index should success
247247
tk.MustExec("alter table t1 add index idx_kc (k, c)")
248248
}
249+
<<<<<<< HEAD
250+
=======
251+
252+
func TestUpdateDuringAddColumn(t *testing.T) {
253+
store := testkit.CreateMockStore(t)
254+
tk := testkit.NewTestKit(t, store)
255+
tk.MustExec("use test")
256+
tk.MustExec("create table t1 (c1 int, c2 int) partition by hash (c1) partitions 16")
257+
tk.MustExec("insert t1 values (1, 1), (2, 2)")
258+
tk.MustExec("create table t2 (c1 int, c2 int) partition by hash (c1) partitions 16")
259+
tk.MustExec("insert t2 values (1, 3), (2, 5)")
260+
tk2 := testkit.NewTestKit(t, store)
261+
tk2.MustExec("use test")
262+
263+
testfailpoint.EnableCall(t, "github.com/pingcap/tidb/pkg/ddl/afterWaitSchemaSynced", func(job *model.Job) {
264+
if job.SchemaState == model.StateWriteOnly {
265+
tk2.MustExec("update t1, t2 set t1.c1 = 8, t2.c2 = 10 where t1.c2 = t2.c1")
266+
tk2.MustQuery("select * from t1").Sort().Check(testkit.Rows("8 1", "8 2"))
267+
tk2.MustQuery("select * from t2").Sort().Check(testkit.Rows("1 10", "2 10"))
268+
}
269+
})
270+
271+
tk.MustExec("alter table t1 add column c3 bigint default 9")
272+
273+
tk.MustQuery("select * from t1").Sort().Check(testkit.Rows("8 1 9", "8 2 9"))
274+
}
275+
276+
func TestExchangePartitionMultiColumn(t *testing.T) {
277+
store := testkit.CreateMockStore(t)
278+
tk := testkit.NewTestKit(t, store)
279+
tk.MustExec("use test")
280+
tk.MustExec("CREATE TABLE t (a1 int(11) not null,a2 int(11) not null,a3 date default null, primary key (`a1`,`a2`)) partition by range columns(`a1`,`a2`)(partition `p10` values less than (10,10),partition `p20` values less than (20,20),partition `pmax` values less than (maxvalue,maxvalue))")
281+
tk.MustExec(`insert into t values(5,10,null),(10,4,null)`)
282+
tk.MustExec("CREATE TABLE t_np (a1 int(11) not null,a2 int(11) not null,a3 date default null, primary key (`a1`,`a2`))")
283+
tk.MustExec(`insert into t_np values(10,4,null),(4,10,null)`)
284+
tk.MustExec(`alter table t exchange partition p10 with table t_np`)
285+
}
286+
>>>>>>> d39268519f7 (ddl: Add checks for all partitioning columns during EXCHANGE PARTITION for RANGE COLUMNS (#59612))

pkg/ddl/tests/partition/BUILD.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ go_test(
55
timeout = "short",
66
srcs = [
77
"db_partition_test.go",
8+
<<<<<<< HEAD
9+
=======
10+
"error_injection_test.go",
11+
"exchange_partition_test.go",
12+
>>>>>>> d39268519f7 (ddl: Add checks for all partitioning columns during EXCHANGE PARTITION for RANGE COLUMNS (#59612))
813
"main_test.go",
914
"multi_domain_test.go",
1015
],
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Copyright 2025 PingCAP, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package partition
16+
17+
import (
18+
"fmt"
19+
"strings"
20+
"testing"
21+
22+
"github.com/pingcap/tidb/pkg/testkit"
23+
)
24+
25+
func TestExchangeRangeColumnsPartition(t *testing.T) {
26+
store := testkit.CreateMockStore(t)
27+
28+
tk := testkit.NewTestKit(t, store)
29+
tk.MustExec("use test")
30+
tk.MustExec("set @@tidb_enable_exchange_partition=1")
31+
32+
// Create a table partitioned by range columns with multiple column types
33+
tk.MustExec(`CREATE TABLE t1 (
34+
id INT NOT NULL,
35+
age INT,
36+
name VARCHAR(50)
37+
) PARTITION BY RANGE COLUMNS(age, name) (
38+
PARTITION p0 VALUES LESS THAN (20, 'm'),
39+
PARTITION p1 VALUES LESS THAN (30, 'm'),
40+
PARTITION p2 VALUES LESS THAN (30, MAXVALUE),
41+
PARTITION p3 VALUES LESS THAN (40, 'm'),
42+
PARTITION p4 VALUES LESS THAN (MAXVALUE, MAXVALUE)
43+
)`)
44+
45+
// Define test values for each column type
46+
ageValues := []any{
47+
nil, // NULL
48+
-2147483648, // min int
49+
2147483647, // max int
50+
0,
51+
19, // boundary-1
52+
20, // boundary 1
53+
29, // boundary-1
54+
30, // boundary 2
55+
39, // boundary-1
56+
40, // boundary 3
57+
}
58+
59+
nameValues := []any{
60+
nil, // NULL
61+
"", // empty string
62+
"l", // boundary-1
63+
"m", // boundary
64+
"n", // boundary+1
65+
}
66+
67+
// Generate all combinations
68+
id := 0
69+
addComma := false
70+
query := "INSERT INTO t1 VALUES "
71+
for _, age := range ageValues {
72+
for _, name := range nameValues {
73+
id++
74+
// if id != 26 {
75+
// continue
76+
// }
77+
if addComma {
78+
query += ","
79+
}
80+
if age == nil && name == nil {
81+
query += fmt.Sprintf("(%d, NULL, NULL)", id)
82+
} else if age == nil {
83+
query += fmt.Sprintf("(%d, NULL, %q)", id, name)
84+
} else if name == nil {
85+
query += fmt.Sprintf("(%d, %d, NULL)", id, age)
86+
} else {
87+
query += fmt.Sprintf("(%d, %d, %q)", id, age, name)
88+
}
89+
addComma = true
90+
}
91+
}
92+
tk.MustExec(query)
93+
94+
// Save initial counts per partition
95+
initialResults := make(map[string]*testkit.Result)
96+
for _, p := range []string{"p0", "p1", "p2", "p3", "p4"} {
97+
result := tk.MustQuery(fmt.Sprintf("SELECT * FROM t1 PARTITION(%s)", p)).Sort()
98+
initialResults[p] = result
99+
}
100+
101+
// Create empty exchange table
102+
tk.MustExec(`CREATE TABLE t2 (
103+
id INT NOT NULL,
104+
age INT,
105+
name VARCHAR(50)
106+
)`)
107+
// Test each partition
108+
partitionNames := []string{"p0", "p1", "p2", "p3", "p4"}
109+
for i, p := range partitionNames {
110+
// Exchange partition out
111+
tk.MustExec(fmt.Sprintf("ALTER TABLE t1 EXCHANGE PARTITION %s WITH TABLE t2", p))
112+
113+
// Verify partition is now empty
114+
tk.MustQuery(fmt.Sprintf("SELECT COUNT(*) FROM t1 PARTITION(%s)", p)).Check(testkit.Rows("0"))
115+
116+
// Verify all rows moved to t2
117+
tk.MustQuery("SELECT * FROM t2").Sort().Check(initialResults[p].Rows())
118+
119+
// Exchange partition back
120+
tk.MustExec(fmt.Sprintf("ALTER TABLE t1 EXCHANGE PARTITION %s WITH TABLE t2", p))
121+
122+
// Verify results are back to initial state
123+
tk.MustQuery(fmt.Sprintf("SELECT * FROM t1 PARTITION(%s)", p)).Sort().Check(initialResults[p].Rows())
124+
125+
// Check that no non-matching rows will be allowed to be exchanged
126+
otherPartitions := strings.Join(append(append([]string{}, partitionNames[:i]...), partitionNames[i+1:]...), ",")
127+
for j := 1; j <= id; j++ {
128+
res := tk.MustQuery(fmt.Sprintf("select * from t1 partition (%s) where id = %d", p, j))
129+
if len(res.Rows()) > 0 {
130+
// Skip rows from current partition, since already tested above.
131+
continue
132+
}
133+
tk.MustExec(fmt.Sprintf("insert into t2 select * from t1 partition (%s) where id = %d", otherPartitions, j))
134+
tk.MustContainErrMsg(fmt.Sprintf("ALTER TABLE t1 EXCHANGE PARTITION %s WITH TABLE t2 /* j = %d */", p, j), "[ddl:1737]Found a row that does not match the partition")
135+
tk.MustExec(`truncate table t2`)
136+
}
137+
}
138+
// Cleanup exchange table
139+
tk.MustExec("DROP TABLE t2")
140+
141+
// Clean up
142+
tk.MustExec("DROP TABLE t1")
143+
}

0 commit comments

Comments
 (0)