Skip to content

Commit 95532bb

Browse files
authored
ddl: fix the batch check for unique index (#40672)
ref #40464, close #40592
1 parent f268e29 commit 95532bb

File tree

3 files changed

+120
-18
lines changed

3 files changed

+120
-18
lines changed

ddl/index.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -1669,10 +1669,18 @@ func (w *addIndexWorker) BackfillDataInTxn(handleRange reorgBackfillTask) (taskC
16691669
return nil
16701670
})
16711671
logSlowOperations(time.Since(oprStartTime), "AddIndexBackfillDataInTxn", 3000)
1672-
1672+
failpoint.Inject("mockDMLExecution", func(val failpoint.Value) {
1673+
//nolint:forcetypeassert
1674+
if val.(bool) && MockDMLExecution != nil {
1675+
MockDMLExecution()
1676+
}
1677+
})
16731678
return
16741679
}
16751680

1681+
// MockDMLExecution is only used for test.
1682+
var MockDMLExecution func()
1683+
16761684
func (w *worker) addPhysicalTableIndex(t table.PhysicalTable, reorgInfo *reorgInfo) error {
16771685
if reorgInfo.mergingTmpIdx {
16781686
logutil.BgLogger().Info("[ddl] start to merge temp index", zap.String("job", reorgInfo.Job.String()), zap.String("reorgInfo", reorgInfo.String()))

ddl/index_merge_tmp.go

+46-17
Original file line numberDiff line numberDiff line change
@@ -48,25 +48,12 @@ func (w *mergeIndexWorker) batchCheckTemporaryUniqueKey(txn kv.Transaction, idxR
4848
return errors.Trace(err)
4949
}
5050

51-
// 1. unique-key/primary-key is duplicate and the handle is equal, skip it.
52-
// 2. unique-key/primary-key is duplicate and the handle is not equal, return duplicate error.
53-
// 3. non-unique-key is duplicate, skip it.
5451
for i, key := range w.originIdxKeys {
5552
if val, found := batchVals[string(key)]; found {
56-
if idxRecords[i].distinct && !bytes.Equal(val, idxRecords[i].vals) {
57-
return kv.ErrKeyExists
58-
}
59-
if !idxRecords[i].delete {
60-
idxRecords[i].skip = true
61-
} else {
62-
// Prevent deleting an unexpected index KV.
63-
hdInVal, err := tablecodec.DecodeHandleInUniqueIndexValue(val, w.table.Meta().IsCommonHandle)
64-
if err != nil {
65-
return errors.Trace(err)
66-
}
67-
if !idxRecords[i].handle.Equal(hdInVal) {
68-
idxRecords[i].skip = true
69-
}
53+
// Found a value in the original index key.
54+
err := checkTempIndexKey(txn, idxRecords[i], val, w.table)
55+
if err != nil {
56+
return errors.Trace(err)
7057
}
7158
} else if idxRecords[i].distinct {
7259
// The keys in w.batchCheckKeys also maybe duplicate,
@@ -77,6 +64,48 @@ func (w *mergeIndexWorker) batchCheckTemporaryUniqueKey(txn kv.Transaction, idxR
7764
return nil
7865
}
7966

67+
func checkTempIndexKey(txn kv.Transaction, tmpRec *temporaryIndexRecord, originIdxVal []byte, tblInfo table.Table) error {
68+
if !tmpRec.delete {
69+
if tmpRec.distinct && !bytes.Equal(originIdxVal, tmpRec.vals) {
70+
return kv.ErrKeyExists
71+
}
72+
// The key has been found in the original index, skip merging it.
73+
tmpRec.skip = true
74+
return nil
75+
}
76+
// Delete operation.
77+
distinct := tablecodec.IndexKVIsUnique(originIdxVal)
78+
if !distinct {
79+
// For non-distinct key, it is consist of a null value and the handle.
80+
// Same as the non-unique indexes, replay the delete operation on non-distinct keys.
81+
return nil
82+
}
83+
// For distinct index key values, prevent deleting an unexpected index KV in original index.
84+
hdInVal, err := tablecodec.DecodeHandleInUniqueIndexValue(originIdxVal, tblInfo.Meta().IsCommonHandle)
85+
if err != nil {
86+
return errors.Trace(err)
87+
}
88+
if !tmpRec.handle.Equal(hdInVal) {
89+
// The inequality means multiple modifications happened in the same key.
90+
// We use the handle in origin index value to check if the row exists.
91+
rowKey := tablecodec.EncodeRecordKey(tblInfo.RecordPrefix(), hdInVal)
92+
_, err := txn.Get(context.Background(), rowKey)
93+
if err != nil {
94+
if kv.IsErrNotFound(err) {
95+
// The row is deleted, so we can merge the delete operation to the origin index.
96+
tmpRec.skip = false
97+
return nil
98+
}
99+
// Unexpected errors.
100+
return errors.Trace(err)
101+
}
102+
// Don't delete the index key if the row exists.
103+
tmpRec.skip = true
104+
return nil
105+
}
106+
return nil
107+
}
108+
80109
// temporaryIndexRecord is the record information of an index.
81110
type temporaryIndexRecord struct {
82111
vals []byte

ddl/index_merge_tmp_test.go

+65
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"testing"
1919
"time"
2020

21+
"github.com/pingcap/failpoint"
2122
"github.com/pingcap/tidb/ddl"
2223
"github.com/pingcap/tidb/ddl/ingest"
2324
"github.com/pingcap/tidb/domain"
@@ -405,6 +406,70 @@ func TestAddIndexMergeDeleteUniqueOnWriteOnly(t *testing.T) {
405406
tk.MustExec("admin check table t;")
406407
}
407408

409+
func TestAddIndexMergeDeleteNullUnique(t *testing.T) {
410+
store := testkit.CreateMockStore(t)
411+
412+
tk := testkit.NewTestKit(t, store)
413+
tk.MustExec("use test")
414+
tk.MustExec("create table t(id int primary key, a int default 0);")
415+
tk.MustExec("insert into t values (1, 1), (2, null);")
416+
417+
tk1 := testkit.NewTestKit(t, store)
418+
tk1.MustExec("use test")
419+
420+
ddl.MockDMLExecution = func() {
421+
_, err := tk1.Exec("delete from t where id = 2;")
422+
assert.NoError(t, err)
423+
}
424+
require.NoError(t, failpoint.Enable("github.com/pingcap/tidb/ddl/mockDMLExecution", "1*return(true)->return(false)"))
425+
tk.MustExec("alter table t add unique index idx(a);")
426+
tk.MustQuery("select count(1) from t;").Check(testkit.Rows("1"))
427+
tk.MustExec("admin check table t;")
428+
require.NoError(t, failpoint.Disable("github.com/pingcap/tidb/ddl/mockDMLExecution"))
429+
}
430+
431+
func TestAddIndexMergeDoubleDelete(t *testing.T) {
432+
store, dom := testkit.CreateMockStoreAndDomain(t)
433+
434+
tk := testkit.NewTestKit(t, store)
435+
tk.MustExec("use test")
436+
tk.MustExec("create table t(id int primary key, a int default 0);")
437+
438+
tk1 := testkit.NewTestKit(t, store)
439+
tk1.MustExec("use test")
440+
441+
d := dom.DDL()
442+
originalCallback := d.GetHook()
443+
defer d.SetHook(originalCallback)
444+
callback := &ddl.TestDDLCallback{}
445+
onJobUpdatedExportedFunc := func(job *model.Job) {
446+
if t.Failed() {
447+
return
448+
}
449+
switch job.SchemaState {
450+
case model.StateWriteOnly:
451+
_, err := tk1.Exec("insert into t values (1, 1);")
452+
assert.NoError(t, err)
453+
}
454+
}
455+
callback.OnJobUpdatedExported.Store(&onJobUpdatedExportedFunc)
456+
d.SetHook(callback)
457+
458+
ddl.MockDMLExecution = func() {
459+
_, err := tk1.Exec("delete from t where id = 1;")
460+
assert.NoError(t, err)
461+
_, err = tk1.Exec("insert into t values (2, 1);")
462+
assert.NoError(t, err)
463+
_, err = tk1.Exec("delete from t where id = 2;")
464+
assert.NoError(t, err)
465+
}
466+
require.NoError(t, failpoint.Enable("github.com/pingcap/tidb/ddl/mockDMLExecution", "1*return(true)->return(false)"))
467+
tk.MustExec("alter table t add unique index idx(a);")
468+
tk.MustQuery("select count(1) from t;").Check(testkit.Rows("0"))
469+
tk.MustExec("admin check table t;")
470+
require.NoError(t, failpoint.Disable("github.com/pingcap/tidb/ddl/mockDMLExecution"))
471+
}
472+
408473
func TestAddIndexMergeConflictWithPessimistic(t *testing.T) {
409474
store, dom := testkit.CreateMockStoreAndDomain(t)
410475
tk := testkit.NewTestKit(t, store)

0 commit comments

Comments
 (0)