diff --git a/ddl/db_test.go b/ddl/db_test.go index ad4d5069a36a9..60d379bc3715a 100644 --- a/ddl/db_test.go +++ b/ddl/db_test.go @@ -7300,6 +7300,248 @@ func (s *testDBSuite8) TestDdlMaxLimitOfIdentifier(c *C) { } +func testDropIndexes(c *C, store kv.Storage, lease time.Duration, createSQL, dropIdxSQL string, idxNames []string) { + tk := testkit.NewTestKit(c, store) + tk.MustExec("use test_db") + tk.MustExec("drop table if exists test_drop_indexes") + tk.MustExec(createSQL) + done := make(chan error, 1) + + num := 100 + // add some rows + for i := 0; i < num; i++ { + tk.MustExec("insert into test_drop_indexes values (?, ?, ?)", i, i, i) + } + ctx := tk.Se.(sessionctx.Context) + t := testGetTableByName(c, ctx, "test_db", "test_drop_indexes") + var idxs []table.Index + for _, tidx := range t.Indices() { + for _, idxName := range idxNames { + if tidx.Meta().Name.L == idxName { + idxs = append(idxs, tidx) + break + } + } + } + c.Assert(idxs, NotNil) + + testddlutil.SessionExecInGoroutine(c, store, dropIdxSQL, done) + + ticker := time.NewTicker(lease / 2) + defer ticker.Stop() +LOOP: + for { + select { + case err := <-done: + if err == nil { + break LOOP + } + c.Assert(err, IsNil, Commentf("err:%v", errors.ErrorStack(err))) + case <-ticker.C: + step := 5 + // delete some rows, and add some data + for i := num; i < num+step; i++ { + n := rand.Intn(num) + tk.MustExec("update test_drop_indexes set c2 = 1 where c1 = ?", n) + tk.MustExec("insert into test_drop_indexes values (?, ?, ?)", i, i, i) + } + num += step + } + } + + // Check in index, it must be no index in KV. + // Make sure there is no index with name c2_index、c3_index. + t = testGetTableByName(c, ctx, "test_db", "test_drop_indexes") + var nidxs []table.Index + for _, tidx := range t.Indices() { + for _, ids := range idxs { + if tidx.Meta().Name.L == ids.Meta().Name.L { + nidxs = append(nidxs, tidx) + } + } + } + c.Assert(nidxs, IsNil) + + for _, idx := range idxs { + idx := tables.NewIndex(t.Meta().ID, t.Meta(), idx.Meta()) + checkDelRangeDone(c, ctx, idx) + } +} + +func testCancelDropIndexes(c *C, store kv.Storage, d ddl.DDL) { + indexesName := []string{"idx_c1", "idx_c2"} + addIdxesSQL := "alter table t add index idx_c1 (c1);alter table t add index idx_c2 (c2);" + dropIdxesSQL := "alter table t drop index idx_c1;alter table t drop index idx_c2;" + + tk := testkit.NewTestKit(c, store) + tk.MustExec("use test_db") + tk.MustExec("drop table if exists t") + tk.MustExec("create table t(c1 int, c2 int)") + defer tk.MustExec("drop table t;") + for i := 0; i < 5; i++ { + tk.MustExec("insert into t values (?, ?)", i, i) + } + testCases := []struct { + needAddIndex bool + jobState model.JobState + JobSchemaState model.SchemaState + cancelSucc bool + }{ + // model.JobStateNone means the jobs is canceled before the first run. + // if we cancel successfully, we need to set needAddIndex to false in the next test case. Otherwise, set needAddIndex to true. + {true, model.JobStateNone, model.StateNone, true}, + {false, model.JobStateRunning, model.StateWriteOnly, false}, + {true, model.JobStateRunning, model.StateDeleteOnly, false}, + {true, model.JobStateRunning, model.StateDeleteReorganization, false}, + } + var checkErr error + hook := &ddl.TestDDLCallback{} + var jobID int64 + testCase := &testCases[0] + hook.OnJobRunBeforeExported = func(job *model.Job) { + if (job.Type == model.ActionDropIndex || job.Type == model.ActionDropPrimaryKey) && + job.State == testCase.jobState && job.SchemaState == testCase.JobSchemaState { + jobID = job.ID + jobIDs := []int64{job.ID} + hookCtx := mock.NewContext() + hookCtx.Store = store + err := hookCtx.NewTxn(context.TODO()) + if err != nil { + checkErr = errors.Trace(err) + return + } + txn, err := hookCtx.Txn(true) + if err != nil { + checkErr = errors.Trace(err) + return + } + + errs, err := admin.CancelJobs(txn, jobIDs) + if err != nil { + checkErr = errors.Trace(err) + return + } + if errs[0] != nil { + checkErr = errors.Trace(errs[0]) + return + } + checkErr = txn.Commit(context.Background()) + } + } + originalHook := d.GetHook() + d.(ddl.DDLForTest).SetHook(hook) + ctx := tk.Se.(sessionctx.Context) + for i := range testCases { + testCase = &testCases[i] + if testCase.needAddIndex { + tk.MustExec(addIdxesSQL) + } + rs, err := tk.Exec(dropIdxesSQL) + if rs != nil { + rs.Close() + } + t := testGetTableByName(c, ctx, "test_db", "t") + + var indexInfos []*model.IndexInfo + for _, idxName := range indexesName { + indexInfo := t.Meta().FindIndexByName(idxName) + if indexInfo != nil { + indexInfos = append(indexInfos, indexInfo) + } + } + + if testCase.cancelSucc { + c.Assert(checkErr, IsNil) + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "[ddl:8214]Cancelled DDL job") + c.Assert(indexInfos, NotNil) + c.Assert(indexInfos[0].State, Equals, model.StatePublic) + } else { + err1 := admin.ErrCannotCancelDDLJob.GenWithStackByArgs(jobID) + c.Assert(err, IsNil) + c.Assert(checkErr, NotNil) + c.Assert(checkErr.Error(), Equals, err1.Error()) + c.Assert(indexInfos, IsNil) + } + } + d.(ddl.DDLForTest).SetHook(originalHook) + tk.MustExec(addIdxesSQL) + tk.MustExec(dropIdxesSQL) +} + +func testDropIndexesIfExists(c *C, store kv.Storage) { + tk := testkit.NewTestKitWithInit(c, store) + tk.MustExec("use test_db;") + tk.MustExec("drop table if exists test_drop_indexes_if_exists;") + tk.MustExec("create table test_drop_indexes_if_exists (id int, c1 int, c2 int, primary key(id), key i1(c1), key i2(c2));") + + // Drop different indexes. + tk.MustGetErrMsg( + "alter table test_drop_indexes_if_exists drop index i1, drop index i3;", + "[ddl:1091]index i3 doesn't exist", + ) + if _, err := tk.Exec("alter table test_drop_indexes_if_exists drop index i1, drop index if exists i3;"); true { + c.Assert(err, IsNil) + } + tk.MustQuery("show warnings;").Check( + testutil.RowsWithSep("|", "Warning|1091|index i3 doesn't exist"), + ) + + // Verify the impact of deletion order when dropping duplicate indexes. + tk.MustGetErrMsg( + "alter table test_drop_indexes_if_exists drop index i2, drop index i2;", + "[ddl:1091]index i2 doesn't exist", + ) + tk.MustGetErrMsg( + "alter table test_drop_indexes_if_exists drop index if exists i2, drop index i2;", + "[ddl:1091]index i2 doesn't exist", + ) + if _, err := tk.Exec("alter table test_drop_indexes_if_exists drop index i2, drop index if exists i2;"); true { + c.Assert(err, IsNil) + } + tk.MustQuery("show warnings;").Check( + testutil.RowsWithSep("|", "Warning|1091|index i2 doesn't exist"), + ) +} + +func testDropIndexesFromPartitionedTable(c *C, store kv.Storage) { + tk := testkit.NewTestKitWithInit(c, store) + tk.MustExec("use test_db;") + tk.MustExec("drop table if exists test_drop_indexes_from_partitioned_table;") + tk.MustExec(` + create table test_drop_indexes_from_partitioned_table (id int, c1 int, c2 int, primary key(id), key i1(c1), key i2(c2)) + partition by range(id) (partition p0 values less than (6), partition p1 values less than maxvalue); + `) + for i := 0; i < 20; i++ { + tk.MustExec("insert into test_drop_indexes_from_partitioned_table values (?, ?, ?)", i, i, i) + } + if _, err := tk.Exec("alter table test_drop_indexes_from_partitioned_table drop index i1, drop index if exists i2;"); true { + c.Assert(err, IsNil) + } +} + +func (s *testDBSuite5) TestDropIndexes(c *C) { + // drop multiple indexes + createSQL := "create table test_drop_indexes (id int, c1 int, c2 int, primary key(id), key i1(c1), key i2(c2));" + dropIdxSQL := "alter table test_drop_indexes drop index i1, drop index i2;" + idxNames := []string{"i1", "i2"} + testDropIndexes(c, s.store, s.lease, createSQL, dropIdxSQL, idxNames) + + createSQL = "create table test_drop_indexes (id int, c1 int, c2 int, primary key(id) nonclustered, unique key i1(c1), key i2(c2));" + dropIdxSQL = "alter table test_drop_indexes drop primary key, drop index i1;" + idxNames = []string{"primary", "i1"} + testDropIndexes(c, s.store, s.lease, createSQL, dropIdxSQL, idxNames) + + createSQL = "create table test_drop_indexes (uuid varchar(32), c1 int, c2 int, primary key(uuid), unique key i1(c1), key i2(c2));" + dropIdxSQL = "alter table test_drop_indexes drop primary key, drop index i1, drop index i2;" + idxNames = []string{"primary", "i1", "i2"} + testDropIndexes(c, s.store, s.lease, createSQL, dropIdxSQL, idxNames) + + testDropIndexesIfExists(c, s.store) + testDropIndexesFromPartitionedTable(c, s.store) + testCancelDropIndexes(c, s.store, s.dom.DDL()) +} + // Close issue #24580. // See https://github.com/pingcap/tidb/issues/24580 func (s *testDBSuite8) TestIssue24580(c *C) { diff --git a/ddl/ddl.go b/ddl/ddl.go index 7f9f0102120d9..db4a9497d2f5c 100644 --- a/ddl/ddl.go +++ b/ddl/ddl.go @@ -601,6 +601,13 @@ func (d *ddl) doDDLJob(ctx sessionctx.Context, job *model.Job) error { } } } + + if historyJob.MultiSchemaInfo != nil && len(historyJob.MultiSchemaInfo.Warnings) != 0 { + for _, warning := range historyJob.MultiSchemaInfo.Warnings { + ctx.GetSessionVars().StmtCtx.AppendWarning(warning) + } + } + logutil.BgLogger().Info("[ddl] DDL job is finished", zap.Int64("jobID", jobID)) return nil } @@ -609,8 +616,13 @@ func (d *ddl) doDDLJob(ctx sessionctx.Context, job *model.Job) error { logutil.BgLogger().Info("[ddl] DDL job is failed", zap.Int64("jobID", jobID)) return errors.Trace(historyJob.Error) } - // Only for JobStateCancelled job which is adding columns or drop columns. - if historyJob.IsCancelled() && (historyJob.Type == model.ActionAddColumns || historyJob.Type == model.ActionDropColumns) { + // Only for JobStateCancelled job which is adding columns or drop columns or drop indexes. + if historyJob.IsCancelled() && (historyJob.Type == model.ActionAddColumns || historyJob.Type == model.ActionDropColumns || historyJob.Type == model.ActionDropIndexes) { + if historyJob.MultiSchemaInfo != nil && len(historyJob.MultiSchemaInfo.Warnings) != 0 { + for _, warning := range historyJob.MultiSchemaInfo.Warnings { + ctx.GetSessionVars().StmtCtx.AppendWarning(warning) + } + } logutil.BgLogger().Info("[ddl] DDL job is cancelled", zap.Int64("jobID", jobID)) return nil } diff --git a/ddl/ddl_api.go b/ddl/ddl_api.go index 29616dad773dd..ad3360dfe16a0 100644 --- a/ddl/ddl_api.go +++ b/ddl/ddl_api.go @@ -2411,6 +2411,10 @@ func resolveAlterTableSpec(ctx sessionctx.Context, specs []*ast.AlterTableSpec) func isSameTypeMultiSpecs(specs []*ast.AlterTableSpec) bool { specType := specs[0].Tp for _, spec := range specs { + // We think AlterTableDropPrimaryKey and AlterTableDropIndex are the same types. + if spec.Tp == ast.AlterTableDropPrimaryKey || spec.Tp == ast.AlterTableDropIndex { + continue + } if spec.Tp != specType { return false } @@ -2433,12 +2437,15 @@ func (d *ddl) AlterTable(ctx context.Context, sctx sessionctx.Context, ident ast if !sctx.GetSessionVars().EnableChangeMultiSchema { return errRunMultiSchemaChanges } + if isSameTypeMultiSpecs(validSpecs) { switch validSpecs[0].Tp { case ast.AlterTableAddColumns: err = d.AddColumns(sctx, ident, validSpecs) case ast.AlterTableDropColumn: err = d.DropColumns(sctx, ident, validSpecs) + case ast.AlterTableDropPrimaryKey, ast.AlterTableDropIndex: + err = d.DropIndexes(sctx, ident, validSpecs) default: return errRunMultiSchemaChanges } @@ -2447,6 +2454,7 @@ func (d *ddl) AlterTable(ctx context.Context, sctx sessionctx.Context, ident ast } return nil } + return errRunMultiSchemaChanges } @@ -5379,7 +5387,6 @@ func (d *ddl) CreateForeignKey(ctx sessionctx.Context, ti ast.Ident, fkName mode err = d.doDDLJob(ctx, job) err = d.callHookOnChanged(err) return errors.Trace(err) - } func (d *ddl) DropForeignKey(ctx sessionctx.Context, ti ast.Ident, fkName model.CIStr) error { @@ -5420,24 +5427,12 @@ func (d *ddl) DropIndex(ctx sessionctx.Context, ti ast.Ident, indexName model.CI } indexInfo := t.Meta().FindIndexByName(indexName.L) - var isPK bool - if indexName.L == strings.ToLower(mysql.PrimaryKeyName) && - // Before we fixed #14243, there might be a general index named `primary` but not a primary key. - (indexInfo == nil || indexInfo.Primary) { - isPK = true - } - if isPK { - // If the table's PKIsHandle is true, we can't find the index from the table. So we check the value of PKIsHandle. - if indexInfo == nil && !t.Meta().PKIsHandle { - return ErrCantDropFieldOrKey.GenWithStack("Can't DROP 'PRIMARY'; check that column/key exists") - } - if t.Meta().PKIsHandle { - return ErrUnsupportedModifyPrimaryKey.GenWithStack("Unsupported drop primary key when the table's pkIsHandle is true") - } - if t.Meta().IsCommonHandle { - return ErrUnsupportedModifyPrimaryKey.GenWithStack("Unsupported drop primary key when the table is using clustered index") - } + + isPK, err := checkIsDropPrimaryKey(indexName, indexInfo, t) + if err != nil { + return err } + if indexInfo == nil { err = ErrCantDropFieldOrKey.GenWithStack("index %s doesn't exist", indexName) if ifExists { @@ -5477,6 +5472,74 @@ func (d *ddl) DropIndex(ctx sessionctx.Context, ti ast.Ident, indexName model.CI return errors.Trace(err) } +func (d *ddl) DropIndexes(ctx sessionctx.Context, ti ast.Ident, specs []*ast.AlterTableSpec) error { + schema, t, err := d.getSchemaAndTableByIdent(ctx, ti) + if err != nil { + return err + } + + indexNames := make([]model.CIStr, 0, len(specs)) + ifExists := make([]bool, 0, len(specs)) + for _, spec := range specs { + var indexName model.CIStr + if spec.Tp == ast.AlterTableDropPrimaryKey { + indexName = model.NewCIStr(mysql.PrimaryKeyName) + } else { + indexName = model.NewCIStr(spec.Name) + } + + indexInfo := t.Meta().FindIndexByName(indexName.L) + if indexInfo != nil { + _, err := checkIsDropPrimaryKey(indexName, indexInfo, t) + if err != nil { + return err + } + if err := checkDropIndexOnAutoIncrementColumn(t.Meta(), indexInfo); err != nil { + return errors.Trace(err) + } + } + + indexNames = append(indexNames, indexName) + ifExists = append(ifExists, spec.IfExists) + } + + job := &model.Job{ + SchemaID: schema.ID, + TableID: t.Meta().ID, + SchemaName: schema.Name.L, + Type: model.ActionDropIndexes, + BinlogInfo: &model.HistoryInfo{}, + Args: []interface{}{indexNames, ifExists}, + } + + err = d.doDDLJob(ctx, job) + err = d.callHookOnChanged(err) + return errors.Trace(err) +} + +func checkIsDropPrimaryKey(indexName model.CIStr, indexInfo *model.IndexInfo, t table.Table) (bool, error) { + var isPK bool + if indexName.L == strings.ToLower(mysql.PrimaryKeyName) && + // Before we fixed #14243, there might be a general index named `primary` but not a primary key. + (indexInfo == nil || indexInfo.Primary) { + isPK = true + } + if isPK { + // If the table's PKIsHandle is true, we can't find the index from the table. So we check the value of PKIsHandle. + if indexInfo == nil && !t.Meta().PKIsHandle { + return isPK, ErrCantDropFieldOrKey.GenWithStack("Can't DROP 'PRIMARY'; check that column/key exists") + } + if t.Meta().PKIsHandle { + return isPK, ErrUnsupportedModifyPrimaryKey.GenWithStack("Unsupported drop primary key when the table's pkIsHandle is true") + } + if t.Meta().IsCommonHandle { + return isPK, ErrUnsupportedModifyPrimaryKey.GenWithStack("Unsupported drop primary key when the table is using clustered index") + } + } + + return isPK, nil +} + func isDroppableColumn(tblInfo *model.TableInfo, colName model.CIStr) error { if ok, dep, isHidden := hasDependentByGeneratedColumn(tblInfo, colName); ok { if isHidden { diff --git a/ddl/ddl_worker.go b/ddl/ddl_worker.go index d56db10c8cfbe..981ed9095e564 100644 --- a/ddl/ddl_worker.go +++ b/ddl/ddl_worker.go @@ -404,7 +404,7 @@ func (w *worker) finishDDLJob(t *meta.Meta, job *model.Job) (err error) { // After rolling back an AddIndex operation, we need to use delete-range to delete the half-done index data. err = w.deleteRange(w.ddlJobCtx, job) case model.ActionDropSchema, model.ActionDropTable, model.ActionTruncateTable, model.ActionDropIndex, model.ActionDropPrimaryKey, - model.ActionDropTablePartition, model.ActionTruncateTablePartition, model.ActionDropColumn, model.ActionDropColumns, model.ActionModifyColumn: + model.ActionDropTablePartition, model.ActionTruncateTablePartition, model.ActionDropColumn, model.ActionDropColumns, model.ActionModifyColumn, model.ActionDropIndexes: err = w.deleteRange(w.ddlJobCtx, job) } } @@ -786,6 +786,8 @@ func (w *worker) runDDLJob(d *ddlCtx, t *meta.Meta, job *model.Job) (ver int64, ver, err = w.onCreateIndex(d, t, job, true) case model.ActionDropIndex, model.ActionDropPrimaryKey: ver, err = onDropIndex(t, job) + case model.ActionDropIndexes: + ver, err = onDropIndexes(t, job) case model.ActionRenameIndex: ver, err = onRenameIndex(t, job) case model.ActionAddForeignKey: diff --git a/ddl/ddl_worker_test.go b/ddl/ddl_worker_test.go index 1297f15f56a09..0f0fcc69dfe4b 100644 --- a/ddl/ddl_worker_test.go +++ b/ddl/ddl_worker_test.go @@ -656,6 +656,11 @@ func buildCancelJobTests(firstID int64) []testCancelJob { {act: model.ActionModifyColumn, jobIDs: []int64{firstID + 67}, cancelRetErrs: noErrs, cancelState: model.StateWriteOnly}, {act: model.ActionModifyColumn, jobIDs: []int64{firstID + 68}, cancelRetErrs: noErrs, cancelState: model.StateWriteReorganization}, {act: model.ActionModifyColumn, jobIDs: []int64{firstID + 69}, cancelRetErrs: []error{admin.ErrCancelFinishedDDLJob}, cancelState: model.StatePublic}, + + // for drop indexes + {act: model.ActionDropIndexes, jobIDs: []int64{firstID + 72}, cancelRetErrs: []error{admin.ErrCannotCancelDDLJob.GenWithStackByArgs(firstID + 72)}, cancelState: model.StateWriteOnly}, + {act: model.ActionDropIndexes, jobIDs: []int64{firstID + 73}, cancelRetErrs: []error{admin.ErrCannotCancelDDLJob.GenWithStackByArgs(firstID + 73)}, cancelState: model.StateDeleteOnly}, + {act: model.ActionDropIndexes, jobIDs: []int64{firstID + 74}, cancelRetErrs: []error{admin.ErrCannotCancelDDLJob.GenWithStackByArgs(firstID + 74)}, cancelState: model.StateWriteReorganization}, } return tests @@ -1286,6 +1291,27 @@ func (s *testDDLSerialSuite) TestCancelJob(c *C) { c.Assert(baseTable.Meta().Columns[0].FieldType.Tp, Equals, mysql.TypeTiny) c.Assert(baseTable.Meta().Columns[0].FieldType.Flag&mysql.NotNullFlag, Equals, uint(1)) c.Assert(failpoint.Disable("github.com/pingcap/tidb/ddl/skipMockContextDoExec"), IsNil) + + // for drop indexes + updateTest(&tests[54]) + ifExists := make([]bool, 2) + idxNames := []model.CIStr{model.NewCIStr("i1"), model.NewCIStr("i2")} + dropIndexesArgs := []interface{}{idxNames, ifExists} + tableInfo := createTestTableForDropIndexes(c, ctx, d, dbInfo, "test-drop-indexes", 6) + doDDLJobSuccess(ctx, d, c, dbInfo.ID, tableInfo.ID, test.act, dropIndexesArgs) + s.checkDropIndexes(c, d, dbInfo.ID, tableInfo.ID, idxNames, true) + + updateTest(&tests[55]) + idxNames = []model.CIStr{model.NewCIStr("i3"), model.NewCIStr("i4")} + dropIndexesArgs = []interface{}{idxNames, ifExists} + doDDLJobSuccess(ctx, d, c, dbInfo.ID, tableInfo.ID, test.act, dropIndexesArgs) + s.checkDropIndexes(c, d, dbInfo.ID, tableInfo.ID, idxNames, true) + + updateTest(&tests[56]) + idxNames = []model.CIStr{model.NewCIStr("i5"), model.NewCIStr("i6")} + dropIndexesArgs = []interface{}{idxNames, ifExists} + doDDLJobSuccess(ctx, d, c, dbInfo.ID, tableInfo.ID, test.act, dropIndexesArgs) + s.checkDropIndexes(c, d, dbInfo.ID, tableInfo.ID, idxNames, true) } func (s *testDDLSuite) TestIgnorableSpec(c *C) { @@ -1632,3 +1658,9 @@ func (s *testDDLSuite) TestDDLPackageExecuteSQL(c *C) { se := sess.(sqlexec.SQLExecutor) _, _ = se.Execute(context.Background(), "create table t(a int);") } + +func (s *testDDLSerialSuite) checkDropIndexes(c *C, d *ddl, schemaID int64, tableID int64, idxNames []model.CIStr, success bool) { + for _, idxName := range idxNames { + checkIdxExist(c, d, schemaID, tableID, idxName.O, !success) + } +} diff --git a/ddl/delete_range.go b/ddl/delete_range.go index 740f3c86c2f76..6430a8f2a3219 100644 --- a/ddl/delete_range.go +++ b/ddl/delete_range.go @@ -346,6 +346,24 @@ func insertJobIntoDeleteRangeTable(ctx context.Context, sctx sessionctx.Context, endKey := tablecodec.EncodeTableIndexPrefix(tableID, indexID+1) return doInsert(ctx, s, job.ID, indexID, startKey, endKey, now) } + case model.ActionDropIndexes: + var indexIDs []int64 + var partitionIDs []int64 + if err := job.DecodeArgs(&[]model.CIStr{}, &[]bool{}, &indexIDs, &partitionIDs); err != nil { + return errors.Trace(err) + } + // Remove data in TiKV. + if len(indexIDs) == 0 { + return nil + } + if len(partitionIDs) == 0 { + return doBatchDeleteIndiceRange(ctx, s, job.ID, job.TableID, indexIDs, now) + } + for _, pID := range partitionIDs { + if err := doBatchDeleteIndiceRange(ctx, s, job.ID, pID, indexIDs, now); err != nil { + return errors.Trace(err) + } + } case model.ActionDropColumn: var colName model.CIStr var indexIDs []int64 diff --git a/ddl/index.go b/ddl/index.go index 27cfb7ba1e8e7..8585487af10a0 100644 --- a/ddl/index.go +++ b/ddl/index.go @@ -720,21 +720,191 @@ func checkDropIndex(t *meta.Meta, job *model.Job) (*model.TableInfo, *model.Inde } // Check that drop primary index will not cause invisible implicit primary index. + if err := checkInvisibleIndexesOnPK(tblInfo, []*model.IndexInfo{indexInfo}, job); err != nil { + return nil, nil, errors.Trace(err) + } + + return tblInfo, indexInfo, nil +} + +func onDropIndexes(t *meta.Meta, job *model.Job) (ver int64, _ error) { + tblInfo, indexNames, ifExists, err := getSchemaInfos(t, job) + if err != nil { + return ver, errors.Trace(err) + } + + indexInfos, err := checkDropIndexes(tblInfo, job, indexNames, ifExists) + if err != nil { + return ver, errors.Trace(err) + } + + if len(indexInfos) == 0 { + job.State = model.JobStateCancelled + return ver, nil + } + + dependentHiddenCols := make([]*model.ColumnInfo, 0) + for _, indexInfo := range indexInfos { + for _, indexColumn := range indexInfo.Columns { + if tblInfo.Columns[indexColumn.Offset].Hidden { + dependentHiddenCols = append(dependentHiddenCols, tblInfo.Columns[indexColumn.Offset]) + } + } + } + + originalState := indexInfos[0].State + switch indexInfos[0].State { + case model.StatePublic: + // public -> write only + setIndicesState(indexInfos, model.StateWriteOnly) + setColumnsState(dependentHiddenCols, model.StateWriteOnly) + for _, colInfo := range dependentHiddenCols { + adjustColumnInfoInDropColumn(tblInfo, colInfo.Offset) + } + ver, err = updateVersionAndTableInfo(t, job, tblInfo, originalState != indexInfos[0].State) + if err != nil { + return ver, errors.Trace(err) + } + job.SchemaState = model.StateWriteOnly + case model.StateWriteOnly: + // write only -> delete only + setIndicesState(indexInfos, model.StateDeleteOnly) + setColumnsState(dependentHiddenCols, model.StateDeleteOnly) + ver, err = updateVersionAndTableInfo(t, job, tblInfo, originalState != indexInfos[0].State) + if err != nil { + return ver, errors.Trace(err) + } + job.SchemaState = model.StateDeleteOnly + case model.StateDeleteOnly: + // delete only -> reorganization + setIndicesState(indexInfos, model.StateDeleteReorganization) + setColumnsState(dependentHiddenCols, model.StateDeleteReorganization) + ver, err = updateVersionAndTableInfo(t, job, tblInfo, originalState != indexInfos[0].State) + if err != nil { + return ver, errors.Trace(err) + } + job.SchemaState = model.StateDeleteReorganization + case model.StateDeleteReorganization: + // reorganization -> absent + indexIDs := make([]int64, 0, len(indexInfos)) + indexNames := make(map[string]bool, len(indexInfos)) + for _, indexInfo := range indexInfos { + indexNames[indexInfo.Name.L] = true + indexIDs = append(indexIDs, indexInfo.ID) + } + + newIndices := make([]*model.IndexInfo, 0, len(tblInfo.Indices)) + for _, idx := range tblInfo.Indices { + if _, ok := indexNames[idx.Name.L]; !ok { + newIndices = append(newIndices, idx) + } + } + tblInfo.Indices = newIndices + + // Set column index flag. + for _, indexInfo := range indexInfos { + dropIndexColumnFlag(tblInfo, indexInfo) + } + + tblInfo.Columns = tblInfo.Columns[:len(tblInfo.Columns)-len(dependentHiddenCols)] + + ver, err = updateVersionAndTableInfoWithCheck(t, job, tblInfo, originalState != model.StateNone) + if err != nil { + return ver, errors.Trace(err) + } + + job.FinishTableJob(model.JobStateDone, model.StateNone, ver, tblInfo) + job.Args = append(job.Args, indexIDs, getPartitionIDs(tblInfo)) + default: + err = ErrInvalidDDLState.GenWithStackByArgs("index", indexInfos[0].State) + } + + return ver, errors.Trace(err) +} + +func getSchemaInfos(t *meta.Meta, job *model.Job) (*model.TableInfo, []model.CIStr, []bool, error) { + schemaID := job.SchemaID + tblInfo, err := getTableInfoAndCancelFaultJob(t, job, schemaID) + if err != nil { + return nil, nil, nil, errors.Trace(err) + } + + var indexNames []model.CIStr + var ifExists []bool + if err = job.DecodeArgs(&indexNames, &ifExists); err != nil { + return nil, nil, nil, errors.Trace(err) + } + + return tblInfo, indexNames, ifExists, nil +} + +func checkDropIndexes(tblInfo *model.TableInfo, job *model.Job, indexNames []model.CIStr, ifExists []bool) ([]*model.IndexInfo, error) { + var warnings []*errors.Error + indexInfos := make([]*model.IndexInfo, 0, len(indexNames)) + UniqueIndexNames := make(map[model.CIStr]bool, len(indexNames)) + for i, indexName := range indexNames { + // Double check the index is exists. + indexInfo := tblInfo.FindIndexByName(indexName.L) + if indexInfo == nil { + if ifExists[i] { + warnings = append(warnings, toTError(ErrCantDropFieldOrKey.GenWithStack("index %s doesn't exist", indexName))) + continue + } + job.State = model.JobStateCancelled + return nil, ErrCantDropFieldOrKey.GenWithStack("index %s doesn't exist", indexName) + } + + // Double check for drop index on auto_increment column. + if err := checkDropIndexOnAutoIncrementColumn(tblInfo, indexInfo); err != nil { + job.State = model.JobStateCancelled + return nil, autoid.ErrWrongAutoKey + } + + // Check for dropping duplicate indexes. + if UniqueIndexNames[indexName] { + if !ifExists[i] { + job.State = model.JobStateCancelled + return nil, ErrCantDropFieldOrKey.GenWithStack("index %s doesn't exist", indexName) + } + warnings = append(warnings, toTError(ErrCantDropFieldOrKey.GenWithStack("index %s doesn't exist", indexName))) + } + UniqueIndexNames[indexName] = true + + indexInfos = append(indexInfos, indexInfo) + } + + // Check that drop primary index will not cause invisible implicit primary index. + if err := checkInvisibleIndexesOnPK(tblInfo, indexInfos, job); err != nil { + return nil, errors.Trace(err) + } + + job.MultiSchemaInfo = &model.MultiSchemaInfo{Warnings: warnings} + + return indexInfos, nil +} + +func checkInvisibleIndexesOnPK(tblInfo *model.TableInfo, indexInfos []*model.IndexInfo, job *model.Job) error { newIndices := make([]*model.IndexInfo, 0, len(tblInfo.Indices)) - for _, idx := range tblInfo.Indices { - if idx.Name.L != indexInfo.Name.L { - newIndices = append(newIndices, idx) + for _, oidx := range tblInfo.Indices { + needAppend := true + for _, idx := range indexInfos { + if idx.Name.L == oidx.Name.L { + needAppend = false + break + } + } + if needAppend { + newIndices = append(newIndices, oidx) } } newTbl := tblInfo.Clone() newTbl.Indices = newIndices - err = checkInvisibleIndexOnPK(newTbl) - if err != nil { + if err := checkInvisibleIndexOnPK(newTbl); err != nil { job.State = model.JobStateCancelled - return nil, nil, errors.Trace(err) + return err } - return tblInfo, indexInfo, nil + return nil } func checkDropIndexOnAutoIncrementColumn(tblInfo *model.TableInfo, indexInfo *model.IndexInfo) error { diff --git a/ddl/rollingback.go b/ddl/rollingback.go index 884ca8e1f21d4..fbc48f4f69e5f 100644 --- a/ddl/rollingback.go +++ b/ddl/rollingback.go @@ -286,6 +286,43 @@ func rollingbackDropIndex(t *meta.Meta, job *model.Job) (ver int64, err error) { } } +func rollingbackDropIndexes(t *meta.Meta, job *model.Job) (ver int64, err error) { + tblInfo, indexNames, ifExists, err := getSchemaInfos(t, job) + if err != nil { + return ver, errors.Trace(err) + } + + indexInfos, err := checkDropIndexes(tblInfo, job, indexNames, ifExists) + if err != nil { + return ver, errors.Trace(err) + } + + indexInfo := indexInfos[0] + originalState := indexInfo.State + switch indexInfo.State { + case model.StateWriteOnly, model.StateDeleteOnly, model.StateDeleteReorganization, model.StateNone: + // We can not rollback now, so just continue to drop index. + // Normally won't fetch here, because there is a check when canceling DDL jobs. See function: IsJobRollbackable. + job.State = model.JobStateRunning + return ver, nil + case model.StatePublic: + job.State = model.JobStateRollbackDone + for _, indexInfo := range indexInfos { + indexInfo.State = model.StatePublic + } + default: + return ver, ErrInvalidDDLState.GenWithStackByArgs("index", indexInfo.State) + } + + job.SchemaState = indexInfo.State + ver, err = updateVersionAndTableInfo(t, job, tblInfo, originalState != indexInfo.State) + if err != nil { + return ver, errors.Trace(err) + } + job.FinishTableJob(model.JobStateRollbackDone, model.StatePublic, ver, tblInfo) + return ver, errCancelledDDLJob +} + func rollingbackAddIndex(w *worker, d *ddlCtx, t *meta.Meta, job *model.Job, isPK bool) (ver int64, err error) { // If the value of SnapshotVer isn't zero, it means the work is backfilling the indexes. if job.SchemaState == model.StateWriteReorganization && job.SnapshotVer != 0 { @@ -420,6 +457,8 @@ func convertJob2RollbackJob(w *worker, d *ddlCtx, t *meta.Meta, job *model.Job) ver, err = rollingbackDropColumns(t, job) case model.ActionDropIndex, model.ActionDropPrimaryKey: ver, err = rollingbackDropIndex(t, job) + case model.ActionDropIndexes: + ver, err = rollingbackDropIndexes(t, job) case model.ActionDropTable, model.ActionDropView, model.ActionDropSequence: err = rollingbackDropTableOrView(t, job) case model.ActionDropTablePartition: diff --git a/ddl/table_test.go b/ddl/table_test.go index b33feb3d2261a..2b7d263deb777 100644 --- a/ddl/table_test.go +++ b/ddl/table_test.go @@ -413,3 +413,21 @@ func (s *testTableSuite) TestTable(c *C) { testCheckJobDone(c, d, job, true) checkTableLockedTest(c, d, dbInfo1, tblInfo, d.GetID(), ctx.GetSessionVars().ConnectionID, model.TableLockWrite) } + +// for drop indexes +func createTestTableForDropIndexes(c *C, ctx sessionctx.Context, d *ddl, dbInfo *model.DBInfo, name string, num int) *model.TableInfo { + tableInfo := testTableInfo(c, d, name, num) + var idxs []*model.IndexInfo + for i := 0; i < num; i++ { + idxName := model.NewCIStr(fmt.Sprintf("i%d", i+1)) + idx := &model.IndexInfo{ + Name: idxName, + State: model.StatePublic, + Columns: []*model.IndexColumn{{Name: model.NewCIStr(fmt.Sprintf("c%d", i+1))}}, + } + idxs = append(idxs, idx) + } + tableInfo.Indices = idxs + testCreateTable(c, ctx, d, dbInfo, tableInfo) + return tableInfo +} diff --git a/util/admin/admin.go b/util/admin/admin.go index 040cd43601fe2..a8d78fec976e0 100644 --- a/util/admin/admin.go +++ b/util/admin/admin.go @@ -92,7 +92,7 @@ func GetDDLInfo(txn kv.Transaction) (*DDLInfo, error) { // IsJobRollbackable checks whether the job can be rollback. func IsJobRollbackable(job *model.Job) bool { switch job.Type { - case model.ActionDropIndex, model.ActionDropPrimaryKey: + case model.ActionDropIndex, model.ActionDropPrimaryKey, model.ActionDropIndexes: // We can't cancel if index current state is in StateDeleteOnly or StateDeleteReorganization or StateWriteOnly, otherwise there will be an inconsistent issue between record and index. // In WriteOnly state, we can rollback for normal index but can't rollback for expression index(need to drop hidden column). Since we can't // know the type of index here, we consider all indices except primary index as non-rollbackable. diff --git a/util/admin/admin_test.go b/util/admin/admin_test.go index 9e9a40c64609f..be3f12812b0a4 100644 --- a/util/admin/admin_test.go +++ b/util/admin/admin_test.go @@ -352,6 +352,7 @@ func TestIsJobRollbackable(t *testing.T) { {model.ActionDropSchema, model.StateDeleteOnly, false}, {model.ActionDropColumn, model.StateDeleteOnly, false}, {model.ActionDropColumns, model.StateDeleteOnly, false}, + {model.ActionDropIndexes, model.StateDeleteOnly, false}, } job := &model.Job{} for _, ca := range cases {