diff --git a/ddl/fktest/foreign_key_test.go b/ddl/fktest/foreign_key_test.go index ba466a8cef07b..df461fa048e5c 100644 --- a/ddl/fktest/foreign_key_test.go +++ b/ddl/fktest/foreign_key_test.go @@ -18,6 +18,7 @@ import ( "bytes" "context" "fmt" + "sync" "testing" "time" @@ -1700,3 +1701,112 @@ func TestForeignKeyWithCacheTable(t *testing.T) { tk.MustExec("alter table t2 nocache;") tk.MustExec("drop table t1,t2;") } + +func TestForeignKeyAndConcurrentDDL(t *testing.T) { + store := testkit.CreateMockStore(t) + tk := testkit.NewTestKit(t, store) + tk.MustExec("set @@foreign_key_checks=1;") + tk.MustExec("use test") + // Test foreign key refer cache table. + tk.MustExec("create table t1 (a int, b int, c int, index(a), index(b), index(c));") + tk.MustExec("create table t2 (a int, b int, c int, index(a), index(b), index(c));") + tk2 := testkit.NewTestKit(t, store) + tk2.MustExec("set @@foreign_key_checks=1;") + tk2.MustExec("use test") + passCases := []struct { + prepare []string + ddl1 string + ddl2 string + }{ + { + ddl1: "alter table t2 add constraint fk_1 foreign key (a) references t1(a)", + ddl2: "alter table t2 add constraint fk_2 foreign key (b) references t1(b)", + }, + { + ddl1: "alter table t2 drop foreign key fk_1", + ddl2: "alter table t2 drop foreign key fk_2", + }, + { + prepare: []string{ + "alter table t2 drop index a", + }, + ddl1: "alter table t2 add index(a)", + ddl2: "alter table t2 add constraint fk_1 foreign key (a) references t1(a)", + }, + { + ddl1: "alter table t2 drop index c", + ddl2: "alter table t2 add constraint fk_2 foreign key (b) references t1(b)", + }, + } + for _, ca := range passCases { + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + tk.MustExec(ca.ddl1) + }() + go func() { + defer wg.Done() + tk2.MustExec(ca.ddl2) + }() + wg.Wait() + } + errorCases := []struct { + prepare []string + ddl1 string + err1 string + ddl2 string + err2 string + }{ + { + ddl1: "alter table t2 add constraint fk foreign key (a) references t1(a)", + err1: "[ddl:1826]Duplicate foreign key constraint name 'fk'", + ddl2: "alter table t2 add constraint fk foreign key (b) references t1(b)", + err2: "[ddl:1826]Duplicate foreign key constraint name 'fk'", + }, + { + prepare: []string{ + "alter table t2 add constraint fk_1 foreign key (a) references t1(a)", + }, + ddl1: "alter table t2 drop foreign key fk_1", + err1: "[schema:1091]Can't DROP 'fk_1'; check that column/key exists", + ddl2: "alter table t2 drop foreign key fk_1", + err2: "[schema:1091]Can't DROP 'fk_1'; check that column/key exists", + }, + { + ddl1: "alter table t2 drop index a", + err1: "[ddl:1553]Cannot drop index 'a': needed in a foreign key constraint", + ddl2: "alter table t2 add constraint fk_1 foreign key (a) references t1(a)", + err2: "[ddl:-1]Failed to add the foreign key constraint. Missing index for 'fk_1' foreign key columns in the table 't2'", + }, + } + tk.MustExec("drop table t1,t2") + tk.MustExec("create table t1 (a int, b int, c int, index(a), index(b), index(c));") + tk.MustExec("create table t2 (a int, b int, c int, index(a), index(b), index(c));") + for i, ca := range errorCases { + for _, sql := range ca.prepare { + tk.MustExec(sql) + } + var wg sync.WaitGroup + var err1, err2 error + wg.Add(2) + go func() { + defer wg.Done() + err1 = tk.ExecToErr(ca.ddl1) + }() + go func() { + defer wg.Done() + err2 = tk2.ExecToErr(ca.ddl2) + }() + wg.Wait() + if (err1 == nil && err2 == nil) || (err1 != nil && err2 != nil) { + require.Failf(t, "both ddl1 and ddl2 execute success, but expect 1 error", fmt.Sprintf("idx: %v, err1: %v, err2: %v", i, err1, err2)) + } + if err1 != nil { + require.Equal(t, ca.err1, err1.Error()) + } + if err2 != nil { + require.Equal(t, ca.err2, err2.Error()) + } + } +} diff --git a/ddl/metadatalocktest/mdl_test.go b/ddl/metadatalocktest/mdl_test.go index 64bdf77d55707..fd307968cad73 100644 --- a/ddl/metadatalocktest/mdl_test.go +++ b/ddl/metadatalocktest/mdl_test.go @@ -257,6 +257,47 @@ func TestMDLBasicBatchPointGet(t *testing.T) { require.Less(t, ts1, ts2) } +func TestMDLAddForeignKey(t *testing.T) { + store, dom := testkit.CreateMockStoreAndDomain(t) + sv := server.CreateMockServer(t, store) + + sv.SetDomain(dom) + dom.InfoSyncer().SetSessionManager(sv) + defer sv.Close() + + conn1 := server.CreateMockConn(t, sv) + tk := testkit.NewTestKitWithSession(t, store, conn1.Context().Session) + conn2 := server.CreateMockConn(t, sv) + tkDDL := testkit.NewTestKitWithSession(t, store, conn2.Context().Session) + tk.MustExec("use test") + tk.MustExec("set global tidb_enable_metadata_lock=1") + tk.MustExec("create table t1(id int key);") + tk.MustExec("create table t2(id int key);") + + tk.MustExec("begin") + tk.MustExec("insert into t2 values(1);") + + var wg sync.WaitGroup + var ddlErr error + wg.Add(1) + var ts2 time.Time + go func() { + defer wg.Done() + ddlErr = tkDDL.ExecToErr("alter table test.t2 add foreign key (id) references t1(id)") + ts2 = time.Now() + }() + + time.Sleep(2 * time.Second) + + ts1 := time.Now() + tk.MustExec("commit") + + wg.Wait() + require.Error(t, ddlErr) + require.Equal(t, "[ddl:1452]Cannot add or update a child row: a foreign key constraint fails (`test`.`t2`, CONSTRAINT `fk_1` FOREIGN KEY (`id`) REFERENCES `t1` (`id`))", ddlErr.Error()) + require.Less(t, ts1, ts2) +} + func TestMDLRRUpdateSchema(t *testing.T) { store, dom := testkit.CreateMockStoreAndDomain(t) sv := server.CreateMockServer(t, store) diff --git a/executor/fktest/BUILD.bazel b/executor/fktest/BUILD.bazel index f245bba152c59..2c9f00dfa0624 100644 --- a/executor/fktest/BUILD.bazel +++ b/executor/fktest/BUILD.bazel @@ -15,6 +15,7 @@ go_test( "//infoschema", "//kv", "//meta/autoid", + "//parser", "//parser/ast", "//parser/auth", "//parser/format", diff --git a/executor/fktest/foreign_key_test.go b/executor/fktest/foreign_key_test.go index 8d6442f39fad4..1dc92d6954a2e 100644 --- a/executor/fktest/foreign_key_test.go +++ b/executor/fktest/foreign_key_test.go @@ -21,6 +21,7 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "testing" "time" @@ -28,6 +29,7 @@ import ( "github.com/pingcap/tidb/executor" "github.com/pingcap/tidb/infoschema" "github.com/pingcap/tidb/kv" + "github.com/pingcap/tidb/parser" "github.com/pingcap/tidb/parser/ast" "github.com/pingcap/tidb/parser/auth" "github.com/pingcap/tidb/parser/format" @@ -2643,3 +2645,91 @@ func TestForeignKeyOnReplaceInto(t *testing.T) { tk.MustExec("replace into t1 values (1, 'new-boss', null)") tk.MustQuery("select id from t1 order by id").Check(testkit.Rows("1")) } + +func TestForeignKeyLargeTxnErr(t *testing.T) { + store := testkit.CreateMockStore(t) + tk := testkit.NewTestKit(t, store) + tk.MustExec("set @@foreign_key_checks=1") + tk.MustExec("use test") + tk.MustExec("create table t1 (id int auto_increment key, pid int, name varchar(200), index(pid));") + tk.MustExec("insert into t1 (name) values ('abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890');") + for i := 0; i < 8; i++ { + tk.MustExec("insert into t1 (name) select name from t1;") + } + tk.MustQuery("select count(*) from t1").Check(testkit.Rows("256")) + tk.MustExec("update t1 set pid=1 where id>1") + tk.MustExec("alter table t1 add foreign key (pid) references t1 (id) on update cascade") + originLimit := atomic.LoadUint64(&kv.TxnTotalSizeLimit) + defer func() { + atomic.StoreUint64(&kv.TxnTotalSizeLimit, originLimit) + }() + // Set the limitation to a small value, make it easier to reach the limitation. + atomic.StoreUint64(&kv.TxnTotalSizeLimit, 10240) + tk.MustQuery("select sum(id) from t1").Check(testkit.Rows("32896")) + // foreign key cascade behaviour will cause ErrTxnTooLarge. + tk.MustGetDBError("update t1 set id=id+100000 where id=1", kv.ErrTxnTooLarge) + tk.MustQuery("select sum(id) from t1").Check(testkit.Rows("32896")) + tk.MustGetDBError("update t1 set id=id+100000 where id=1", kv.ErrTxnTooLarge) + tk.MustQuery("select id,pid from t1 where id<3 order by id").Check(testkit.Rows("1 ", "2 1")) + tk.MustExec("set @@foreign_key_checks=0") + tk.MustExec("update t1 set id=id+100000 where id=1") + tk.MustQuery("select id,pid from t1 where id<3 or pid is null order by id").Check(testkit.Rows("2 1", "100001 ")) +} + +func TestForeignKeyAndLockView(t *testing.T) { + store := testkit.CreateMockStore(t) + tk := testkit.NewTestKit(t, store) + tk.MustExec("use test") + tk.MustExec("create table t1 (id int key)") + tk.MustExec("create table t2 (id int key, foreign key (id) references t1(id) ON DELETE CASCADE ON UPDATE CASCADE)") + tk.MustExec("insert into t1 values (1)") + tk.MustExec("insert into t2 values (1)") + tk.MustExec("begin pessimistic") + tk.MustExec("set @@foreign_key_checks=0") + tk.MustExec("update t2 set id=2") + + tk2 := testkit.NewTestKit(t, store) + tk2.MustExec("set @@foreign_key_checks=1") + tk2.MustExec("use test") + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + tk2.MustExec("begin pessimistic") + tk2.MustExec("update t1 set id=2 where id=1") + tk2.MustExec("commit") + }() + time.Sleep(time.Millisecond * 200) + _, digest := parser.NormalizeDigest("update t1 set id=2 where id=1") + tk.MustQuery("select CURRENT_SQL_DIGEST from information_schema.tidb_trx where state='LockWaiting' and db='test'").Check(testkit.Rows(digest.String())) + tk.MustGetErrMsg("update t1 set id=2", "[executor:1213]Deadlock found when trying to get lock; try restarting transaction") + wg.Wait() +} + +func TestForeignKeyAndMemoryTracker(t *testing.T) { + store := testkit.CreateMockStore(t) + tk := testkit.NewTestKit(t, store) + tk.MustExec("set @@foreign_key_checks=1") + tk.MustExec("use test") + tk.MustExec("create table t1 (id int auto_increment key, pid int, name varchar(200), index(pid));") + tk.MustExec("insert into t1 (name) values ('abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz');") + for i := 0; i < 8; i++ { + tk.MustExec("insert into t1 (name) select name from t1;") + } + tk.MustQuery("select count(*) from t1").Check(testkit.Rows("256")) + tk.MustExec("update t1 set pid=1 where id>1") + tk.MustExec("alter table t1 add foreign key (pid) references t1 (id) on update cascade") + tk.MustQuery("select sum(id) from t1").Check(testkit.Rows("32896")) + defer tk.MustExec("SET GLOBAL tidb_mem_oom_action = DEFAULT") + tk.MustExec("SET GLOBAL tidb_mem_oom_action='CANCEL'") + tk.MustExec("set @@tidb_mem_quota_query=40960;") + // foreign key cascade behaviour will exceed memory quota. + err := tk.ExecToErr("update t1 set id=id+100000 where id=1") + require.Error(t, err) + require.Contains(t, err.Error(), "Out Of Memory Quota!") + tk.MustQuery("select id,pid from t1 where id = 1").Check(testkit.Rows("1 ")) + tk.MustExec("set @@foreign_key_checks=0") + // After disable foreign_key_checks, following DML will execute successful. + tk.MustExec("update t1 set id=id+100000 where id=1") + tk.MustQuery("select id,pid from t1 where id<3 or pid is null order by id").Check(testkit.Rows("2 1", "100001 ")) +} diff --git a/tests/realtikvtest/addindextest/add_index_test.go b/tests/realtikvtest/addindextest/add_index_test.go index 7dd4919570594..1c1403f66a922 100644 --- a/tests/realtikvtest/addindextest/add_index_test.go +++ b/tests/realtikvtest/addindextest/add_index_test.go @@ -100,3 +100,29 @@ func TestCreateMultiColsIndex(t *testing.T) { ctx := initTest(t) testTwoColsFrame(ctx, coliIDs, coljIDs, addIndexMultiCols) } + +func TestAddForeignKeyWithAutoCreateIndex(t *testing.T) { + store := realtikvtest.CreateMockStoreAndSetup(t) + tk := testkit.NewTestKit(t, store) + tk.MustExec("drop database if exists fk_index;") + tk.MustExec("create database fk_index;") + tk.MustExec("use fk_index;") + tk.MustExec(`set global tidb_ddl_enable_fast_reorg=1;`) + tk.MustExec("create table employee (id bigint auto_increment key, pid bigint)") + tk.MustExec("insert into employee (id) values (1),(2),(3),(4),(5),(6),(7),(8)") + for i := 0; i < 14; i++ { + tk.MustExec("insert into employee (pid) select pid from employee") + } + tk.MustExec("update employee set pid=id-1 where id>1") + tk.MustQuery("select count(*) from employee").Check(testkit.Rows("131072")) + tk.MustExec("alter table employee add foreign key fk_1(pid) references employee(id)") + tk.MustExec("alter table employee drop foreign key fk_1") + tk.MustExec("alter table employee drop index fk_1") + tk.MustExec("update employee set pid=0 where id=1") + tk.MustGetErrMsg("alter table employee add foreign key fk_1(pid) references employee(id)", + "[ddl:1452]Cannot add or update a child row: a foreign key constraint fails (`fk_index`.`employee`, CONSTRAINT `fk_1` FOREIGN KEY (`pid`) REFERENCES `employee` (`id`))") + tk.MustExec("update employee set pid=null where id=1") + tk.MustExec("insert into employee (pid) select pid from employee") + tk.MustExec("update employee set pid=id-1 where id>1 and pid is null") + tk.MustExec("alter table employee add foreign key fk_1(pid) references employee(id)") +} diff --git a/tests/realtikvtest/pessimistictest/BUILD.bazel b/tests/realtikvtest/pessimistictest/BUILD.bazel index 97890c8b8b70b..67a01e83cf386 100644 --- a/tests/realtikvtest/pessimistictest/BUILD.bazel +++ b/tests/realtikvtest/pessimistictest/BUILD.bazel @@ -18,6 +18,7 @@ go_test( "//parser/model", "//parser/mysql", "//parser/terror", + "//planner/core", "//session", "//sessionctx/variable", "//sessiontxn", diff --git a/tests/realtikvtest/pessimistictest/pessimistic_test.go b/tests/realtikvtest/pessimistictest/pessimistic_test.go index ae7545e0e91f6..a70b31f0a87b8 100644 --- a/tests/realtikvtest/pessimistictest/pessimistic_test.go +++ b/tests/realtikvtest/pessimistictest/pessimistic_test.go @@ -35,6 +35,7 @@ import ( "github.com/pingcap/tidb/parser/model" "github.com/pingcap/tidb/parser/mysql" "github.com/pingcap/tidb/parser/terror" + plannercore "github.com/pingcap/tidb/planner/core" "github.com/pingcap/tidb/session" "github.com/pingcap/tidb/sessionctx/variable" "github.com/pingcap/tidb/sessiontxn" @@ -2816,6 +2817,66 @@ func TestAsyncCommitCalTSFail(t *testing.T) { tk2.MustExec("commit") } +func TestAsyncCommitAndForeignKey(t *testing.T) { + defer config.RestoreFunc()() + config.UpdateGlobal(func(conf *config.Config) { + conf.TiKVClient.AsyncCommit.SafeWindow = time.Second + conf.TiKVClient.AsyncCommit.AllowedClockDrift = 0 + }) + store := realtikvtest.CreateMockStoreAndSetup(t) + tk := createAsyncCommitTestKit(t, store) + tk.MustExec("drop table if exists t_parent, t_child") + tk.MustExec("create table t_parent (id int primary key)") + tk.MustExec("create table t_child (id int primary key, pid int, foreign key (pid) references t_parent(id) on delete cascade on update cascade)") + tk.MustExec("insert into t_parent values (1),(2),(3),(4)") + tk.MustExec("insert into t_child values (1,1),(2,2),(3,3)") + tk.MustExec("set tidb_enable_1pc = true") + tk.MustExec("begin pessimistic") + tk.MustExec("delete from t_parent where id in (1,4)") + tk.MustExec("update t_parent set id=22 where id=2") + tk.MustExec("commit") + tk.MustQuery("select * from t_parent order by id").Check(testkit.Rows("3", "22")) + tk.MustQuery("select * from t_child order by id").Check(testkit.Rows("2 22", "3 3")) +} + +func TestTransactionIsolationAndForeignKey(t *testing.T) { + if !*realtikvtest.WithRealTiKV { + t.Skip("The test only support test with tikv.") + } + store := realtikvtest.CreateMockStoreAndSetup(t) + tk := testkit.NewTestKit(t, store) + tk2 := testkit.NewTestKit(t, store) + tk.MustExec("use test") + tk2.MustExec("use test") + tk.MustExec("drop table if exists t1,t2") + tk.MustExec("create table t1 (id int primary key)") + tk.MustExec("create table t2 (id int primary key, pid int, foreign key (pid) references t1(id) on delete cascade on update cascade)") + tk.MustExec("insert into t1 values (1)") + tk.MustExec("set tx_isolation = 'READ-COMMITTED'") + tk.MustExec("begin pessimistic") + tk.MustExec("insert into t2 values (1,1)") + tk.MustGetDBError("insert into t2 values (2,2)", plannercore.ErrNoReferencedRow2) + tk2.MustExec("insert into t1 values (2)") + tk.MustQuery("select * from t1").Check(testkit.Rows("1", "2")) + tk.MustExec("insert into t2 values (2,2)") + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + tk2.MustExec("delete from t1 where id=2") + }() + time.Sleep(time.Millisecond * 10) + tk.MustExec("commit") + wg.Wait() + tk.MustQuery("select * from t1").Check(testkit.Rows("1")) + tk.MustQuery("select * from t2").Check(testkit.Rows("1 1")) + tk2.MustExec("delete from t1 where id=1") + tk.MustQuery("select * from t1").Check(testkit.Rows()) + tk.MustQuery("select * from t2").Check(testkit.Rows()) + tk.MustExec("admin check table t1") + tk.MustExec("admin check table t2") +} + func TestChangeLockToPut(t *testing.T) { store := realtikvtest.CreateMockStoreAndSetup(t) diff --git a/tests/realtikvtest/testkit.go b/tests/realtikvtest/testkit.go index b3ae5f3c6a2ac..4b8a749e65c9d 100644 --- a/tests/realtikvtest/testkit.go +++ b/tests/realtikvtest/testkit.go @@ -19,6 +19,7 @@ package realtikvtest import ( "flag" "fmt" + "strings" "sync/atomic" "testing" "time" @@ -110,8 +111,12 @@ func CreateMockStoreAndDomainAndSetup(t *testing.T, opts ...mockstore.MockTiKVSt tk.MustExec(fmt.Sprintf("set global innodb_lock_wait_timeout = %d", variable.DefInnodbLockWaitTimeout)) tk.MustExec("use test") rs := tk.MustQuery("show tables") + tables := []string{} for _, row := range rs.Rows() { - tk.MustExec(fmt.Sprintf("drop table %s", row[0])) + tables = append(tables, fmt.Sprintf("`%v`", row[0])) + } + if len(tables) > 0 { + tk.MustExec(fmt.Sprintf("drop table %s", strings.Join(tables, ","))) } } else { store, err = mockstore.NewMockStore(opts...)