diff --git a/ast/ddl.go b/ast/ddl.go index 2be6e1a958..1e40c48b86 100644 --- a/ast/ddl.go +++ b/ast/ddl.go @@ -128,6 +128,8 @@ type ReferenceDef struct { Table *TableName IndexColNames []*IndexColName + OnDelete *OnDeleteOpt + OnUpdate *OnUpdateOpt } // Accept implements Node Accept interface. @@ -149,6 +151,75 @@ func (n *ReferenceDef) Accept(v Visitor) (Node, bool) { } n.IndexColNames[i] = node.(*IndexColName) } + onDelete, ok := n.OnDelete.Accept(v) + if !ok { + return n, false + } + n.OnDelete = onDelete.(*OnDeleteOpt) + onUpdate, ok := n.OnUpdate.Accept(v) + if !ok { + return n, false + } + n.OnUpdate = onUpdate.(*OnUpdateOpt) + return v.Leave(n) +} + +// ReferOptionType is the type for refer options. +type ReferOptionType int + +// Refer option types. +const ( + ReferOptionNoOption ReferOptionType = iota + ReferOptionRestrict + ReferOptionCascade + ReferOptionSetNull + ReferOptionNoAction +) + +// String implements fmt.Stringer interface. +func (r ReferOptionType) String() string { + switch r { + case ReferOptionRestrict: + return "RESTRICT" + case ReferOptionCascade: + return "CASCADE" + case ReferOptionSetNull: + return "SET NULL" + case ReferOptionNoAction: + return "NO ACTION" + } + return "" +} + +// OnDeleteOpt is used for optional on delete clause. +type OnDeleteOpt struct { + node + ReferOpt ReferOptionType +} + +// Accept implements Node Accept interface. +func (n *OnDeleteOpt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*OnDeleteOpt) + return v.Leave(n) +} + +// OnUpdateOpt is used for optional on update clause. +type OnUpdateOpt struct { + node + ReferOpt ReferOptionType +} + +// Accept implements Node Accept interface. +func (n *OnUpdateOpt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*OnUpdateOpt) return v.Leave(n) } diff --git a/ddl/ddl.go b/ddl/ddl.go index 37c00e03a4..46d5be0c7a 100644 --- a/ddl/ddl.go +++ b/ddl/ddl.go @@ -67,6 +67,8 @@ var ( ErrInvalidColumnState = terror.ClassDDL.New(codeInvalidColumnState, "invalid column state") // ErrInvalidIndexState returns for invalid index state. ErrInvalidIndexState = terror.ClassDDL.New(codeInvalidIndexState, "invalid index state") + // ErrInvalidForeignKeyState returns for invalid foreign key state. + ErrInvalidForeignKeyState = terror.ClassDDL.New(codeInvalidForeignKeyState, "invalid foreign key state") // ErrColumnBadNull returns for a bad null value. ErrColumnBadNull = terror.ClassDDL.New(codeBadNull, "column cann't be null") @@ -639,39 +641,68 @@ func checkDuplicateColumn(colDefs []*ast.ColumnDef) error { return nil } -func checkConstraintNames(constraints []*ast.Constraint) error { +func checkDuplicateConstraint(namesMap map[string]bool, name string, foreign bool) error { + if name == "" { + return nil + } + nameLower := strings.ToLower(name) + if namesMap[nameLower] { + if foreign { + return infoschema.ErrForeignKeyExists.Gen("CREATE TABLE: duplicate foreign key %s", name) + } + return infoschema.ErrIndexExists.Gen("CREATE TABLE: duplicate key %s", name) + } + namesMap[nameLower] = true + return nil +} + +func setEmptyConstraintName(namesMap map[string]bool, constr *ast.Constraint, foreign bool) { + if constr.Name == "" && len(constr.Keys) > 0 { + colName := constr.Keys[0].Column.Name.L + constrName := colName + i := 2 + for namesMap[constrName] { + // We loop forever until we find constrName that haven't been used. + if foreign { + constrName = fmt.Sprintf("fk_%s_%d", colName, i) + } else { + constrName = fmt.Sprintf("%s_%d", colName, i) + } + i++ + } + constr.Name = constrName + namesMap[constrName] = true + } +} + +func (d *ddl) checkConstraintNames(constraints []*ast.Constraint) error { constrNames := map[string]bool{} + fkNames := map[string]bool{} // Check not empty constraint name whether is duplicated. for _, constr := range constraints { if constr.Tp == ast.ConstraintForeignKey { - // Ignore foreign key. - continue - } - if constr.Name != "" { - nameLower := strings.ToLower(constr.Name) - if constrNames[nameLower] { - return infoschema.ErrIndexExists.Gen("CREATE TABLE: duplicate key %s", constr.Name) + err := checkDuplicateConstraint(fkNames, constr.Name, true) + if err != nil { + return errors.Trace(err) + } + } else { + err := checkDuplicateConstraint(constrNames, constr.Name, false) + if err != nil { + return errors.Trace(err) } - constrNames[nameLower] = true } } // Set empty constraint names. for _, constr := range constraints { - if constr.Name == "" && len(constr.Keys) > 0 { - colName := constr.Keys[0].Column.Name.O - constrName := colName - i := 2 - for constrNames[strings.ToLower(constrName)] { - // We loop forever until we find constrName that haven't been used. - constrName = fmt.Sprintf("%s_%d", colName, i) - i++ - } - constr.Name = constrName - constrNames[constrName] = true + if constr.Tp == ast.ConstraintForeignKey { + setEmptyConstraintName(fkNames, constr, true) + } else { + setEmptyConstraintName(constrNames, constr, false) } } + return nil } @@ -687,6 +718,33 @@ func (d *ddl) buildTableInfo(tableName model.CIStr, cols []*column.Col, constrai tbInfo.Columns = append(tbInfo.Columns, &v.ColumnInfo) } for _, constr := range constraints { + if constr.Tp == ast.ConstraintForeignKey { + for _, fk := range tbInfo.ForeignKeys { + if fk.Name.L == strings.ToLower(constr.Name) { + return nil, infoschema.ErrForeignKeyExists.Gen("foreign key %s already exists.", constr.Name) + } + } + var fk model.FKInfo + fk.Name = model.NewCIStr(constr.Name) + fk.RefTable = constr.Refer.Table.Name + fk.State = model.StatePublic + for _, key := range constr.Keys { + fk.Cols = append(fk.Cols, key.Column.Name) + } + for _, key := range constr.Refer.IndexColNames { + fk.RefCols = append(fk.RefCols, key.Column.Name) + } + fk.OnDelete = int(constr.Refer.OnDelete.ReferOpt) + fk.OnUpdate = int(constr.Refer.OnUpdate.ReferOpt) + if len(fk.Cols) != len(fk.RefCols) { + return nil, infoschema.ErrForeignKeyNotMatch.Gen("foreign key not match keys len %d, refkeys len %d .", len(fk.Cols), len(fk.RefCols)) + } + if len(fk.Cols) == 0 { + return nil, infoschema.ErrForeignKeyNotMatch.Gen("foreign key should have one key at least.") + } + tbInfo.ForeignKeys = append(tbInfo.ForeignKeys, &fk) + continue + } if constr.Tp == ast.ConstraintPrimaryKey { if len(constr.Keys) == 1 { key := constr.Keys[0] @@ -765,7 +823,7 @@ func (d *ddl) CreateTable(ctx context.Context, ident ast.Ident, colDefs []*ast.C return errors.Trace(err) } - err = checkConstraintNames(newConstraints) + err = d.checkConstraintNames(newConstraints) if err != nil { return errors.Trace(err) } @@ -850,9 +908,13 @@ func (d *ddl) AlterTable(ctx context.Context, ident ast.Ident, specs []*ast.Alte err = d.CreateIndex(ctx, ident, false, model.NewCIStr(constr.Name), spec.Constraint.Keys) case ast.ConstraintUniq, ast.ConstraintUniqIndex, ast.ConstraintUniqKey: err = d.CreateIndex(ctx, ident, true, model.NewCIStr(constr.Name), spec.Constraint.Keys) + case ast.ConstraintForeignKey: + err = d.CreateForeignKey(ctx, ident, model.NewCIStr(constr.Name), spec.Constraint.Keys, spec.Constraint.Refer) default: // nothing to do now. } + case ast.AlterTableDropForeignKey: + err = d.DropForeignKey(ctx, ident, model.NewCIStr(spec.Name)) default: // nothing to do now. } @@ -1005,6 +1067,88 @@ func (d *ddl) CreateIndex(ctx context.Context, ti ast.Ident, unique bool, indexN return errors.Trace(err) } +func (d *ddl) buildFKInfo(fkName model.CIStr, keys []*ast.IndexColName, refer *ast.ReferenceDef) (*model.FKInfo, error) { + fkID, err := d.genGlobalID() + if err != nil { + return nil, errors.Trace(err) + } + + var fkInfo model.FKInfo + fkInfo.ID = fkID + fkInfo.Name = fkName + fkInfo.RefTable = refer.Table.Name + + fkInfo.Cols = make([]model.CIStr, len(keys)) + for i, key := range keys { + fkInfo.Cols[i] = key.Column.Name + } + + fkInfo.RefCols = make([]model.CIStr, len(refer.IndexColNames)) + for i, key := range refer.IndexColNames { + fkInfo.RefCols[i] = key.Column.Name + } + + fkInfo.OnDelete = int(refer.OnDelete.ReferOpt) + fkInfo.OnUpdate = int(refer.OnUpdate.ReferOpt) + + return &fkInfo, nil + +} + +func (d *ddl) CreateForeignKey(ctx context.Context, ti ast.Ident, fkName model.CIStr, keys []*ast.IndexColName, refer *ast.ReferenceDef) error { + is := d.infoHandle.Get() + schema, ok := is.SchemaByName(ti.Schema) + if !ok { + return infoschema.ErrDatabaseNotExists.Gen("database %s not exists", ti.Schema) + } + + t, err := is.TableByName(ti.Schema, ti.Name) + if err != nil { + return errors.Trace(infoschema.ErrTableNotExists) + } + + fkInfo, err := d.buildFKInfo(fkName, keys, refer) + if err != nil { + return errors.Trace(err) + } + + job := &model.Job{ + SchemaID: schema.ID, + TableID: t.Meta().ID, + Type: model.ActionAddForeignKey, + Args: []interface{}{fkInfo}, + } + + err = d.doDDLJob(ctx, job) + err = d.hook.OnChanged(err) + return errors.Trace(err) + +} + +func (d *ddl) DropForeignKey(ctx context.Context, ti ast.Ident, fkName model.CIStr) error { + is := d.infoHandle.Get() + schema, ok := is.SchemaByName(ti.Schema) + if !ok { + return infoschema.ErrDatabaseNotExists.Gen("database %s not exists", ti.Schema) + } + + t, err := is.TableByName(ti.Schema, ti.Name) + if err != nil { + return errors.Trace(infoschema.ErrTableNotExists) + } + + job := &model.Job{ + SchemaID: schema.ID, + TableID: t.Meta().ID, + Type: model.ActionDropForeignKey, + Args: []interface{}{fkName}, + } + + err = d.doDDLJob(ctx, job) + err = d.hook.OnChanged(err) + return errors.Trace(err) +} + func (d *ddl) DropIndex(ctx context.Context, ti ast.Ident, indexName model.CIStr) error { is := d.infoHandle.Get() schema, ok := is.SchemaByName(ti.Schema) @@ -1052,10 +1196,11 @@ const ( codeWaitReorgTimeout = 7 codeInvalidStoreVer = 8 - codeInvalidDBState = 100 - codeInvalidTableState = 101 - codeInvalidColumnState = 102 - codeInvalidIndexState = 103 + codeInvalidDBState = 100 + codeInvalidTableState = 101 + codeInvalidColumnState = 102 + codeInvalidIndexState = 103 + codeInvalidForeignKeyState = 104 codeCantDropColWithIndex = 201 codeUnsupportedAddColumn = 202 diff --git a/ddl/ddl_worker.go b/ddl/ddl_worker.go index 102ee83435..ca4312e9db 100644 --- a/ddl/ddl_worker.go +++ b/ddl/ddl_worker.go @@ -352,6 +352,10 @@ func (d *ddl) runDDLJob(t *meta.Meta, job *model.Job) { err = d.onCreateIndex(t, job) case model.ActionDropIndex: err = d.onDropIndex(t, job) + case model.ActionAddForeignKey: + err = d.onCreateForeignKey(t, job) + case model.ActionDropForeignKey: + err = d.onDropForeignKey(t, job) default: // invalid job, cancel it. job.State = model.JobCancelled diff --git a/ddl/foreign_key.go b/ddl/foreign_key.go new file mode 100644 index 0000000000..355e55d839 --- /dev/null +++ b/ddl/foreign_key.go @@ -0,0 +1,120 @@ +// Copyright 2016 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package ddl + +import ( + "github.com/juju/errors" + "github.com/pingcap/tidb/infoschema" + "github.com/pingcap/tidb/meta" + "github.com/pingcap/tidb/model" +) + +func (d *ddl) onCreateForeignKey(t *meta.Meta, job *model.Job) error { + schemaID := job.SchemaID + tblInfo, err := d.getTableInfo(t, job) + if err != nil { + return errors.Trace(err) + } + + var fkInfo model.FKInfo + err = job.DecodeArgs(&fkInfo) + if err != nil { + job.State = model.JobCancelled + return errors.Trace(err) + } + tblInfo.ForeignKeys = append(tblInfo.ForeignKeys, &fkInfo) + + _, err = t.GenSchemaVersion() + if err != nil { + return errors.Trace(err) + } + + switch fkInfo.State { + case model.StateNone: + // We just support record the foreign key, so we just make it public. + // none -> public + job.SchemaState = model.StatePublic + fkInfo.State = model.StatePublic + err = t.UpdateTable(schemaID, tblInfo) + if err != nil { + return errors.Trace(err) + } + // finish this job + job.State = model.JobDone + return nil + default: + return ErrInvalidForeignKeyState.Gen("invalid fk state %v", fkInfo.State) + } +} + +func (d *ddl) onDropForeignKey(t *meta.Meta, job *model.Job) error { + schemaID := job.SchemaID + tblInfo, err := d.getTableInfo(t, job) + if err != nil { + return errors.Trace(err) + } + + var ( + fkName model.CIStr + found bool + fkInfo model.FKInfo + ) + err = job.DecodeArgs(&fkName) + if err != nil { + job.State = model.JobCancelled + return errors.Trace(err) + } + + for _, fk := range tblInfo.ForeignKeys { + if fk.Name.L == fkName.L { + found = true + fkInfo = *fk + } + } + + if !found { + return infoschema.ErrForeignKeyNotExists.Gen("foreign key doesn't exist", fkName) + } + + nfks := tblInfo.ForeignKeys[:0] + for _, fk := range tblInfo.ForeignKeys { + if fk.Name.L != fkName.L { + nfks = append(nfks, fk) + } + } + tblInfo.ForeignKeys = nfks + + _, err = t.GenSchemaVersion() + if err != nil { + return errors.Trace(err) + } + + switch fkInfo.State { + case model.StatePublic: + // We just support record the foreign key, so we just make it none. + // public -> none + job.SchemaState = model.StateNone + fkInfo.State = model.StateNone + err = t.UpdateTable(schemaID, tblInfo) + if err != nil { + return errors.Trace(err) + } + // finish this job + job.State = model.JobDone + return nil + default: + return ErrInvalidForeignKeyState.Gen("invalid fk state %v", fkInfo.State) + } + +} diff --git a/ddl/foreign_key_test.go b/ddl/foreign_key_test.go new file mode 100644 index 0000000000..77251ef753 --- /dev/null +++ b/ddl/foreign_key_test.go @@ -0,0 +1,177 @@ +// Copyright 2016 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package ddl + +import ( + "strings" + "time" + + . "github.com/pingcap/check" + "github.com/pingcap/tidb/ast" + "github.com/pingcap/tidb/context" + "github.com/pingcap/tidb/kv" + "github.com/pingcap/tidb/model" + "github.com/pingcap/tidb/table" + "github.com/pingcap/tidb/util/testleak" +) + +var _ = Suite(&testForeighKeySuite{}) + +type testForeighKeySuite struct { + store kv.Storage + dbInfo *model.DBInfo +} + +func (s *testForeighKeySuite) SetUpSuite(c *C) { + trySkipTest(c) + + s.store = testCreateStore(c, "test_foreign") +} + +func (s *testForeighKeySuite) TearDownSuite(c *C) { + trySkipTest(c) + + err := s.store.Close() + c.Assert(err, IsNil) +} + +func testCreateForeignKey(c *C, ctx context.Context, d *ddl, dbInfo *model.DBInfo, tblInfo *model.TableInfo, fkName string, keys []string, refTable string, refKeys []string, onDelete ast.ReferOptionType, onUpdate ast.ReferOptionType) *model.Job { + FKName := model.NewCIStr(fkName) + Keys := make([]*ast.IndexColName, len(keys)) + for i, key := range keys { + col := &ast.ColumnName{Name: model.NewCIStr(key)} + ic := &ast.IndexColName{Column: col} + Keys[i] = ic + } + RefTable := &ast.TableName{Name: model.NewCIStr(refTable)} + RefKeys := make([]*ast.IndexColName, len(refKeys)) + for i, key := range refKeys { + col := &ast.ColumnName{Name: model.NewCIStr(key)} + ic := &ast.IndexColName{Column: col} + RefKeys[i] = ic + } + fkID, err := d.genGlobalID() + c.Assert(err, IsNil) + job := &model.Job{ + SchemaID: dbInfo.ID, + TableID: tblInfo.ID, + Type: model.ActionAddForeignKey, + Args: []interface{}{FKName, fkID, Keys, RefTable, RefKeys, onDelete, onUpdate}, + } + err = d.doDDLJob(ctx, job) + c.Assert(err, IsNil) + return job +} + +func testDropForeignKey(c *C, ctx context.Context, d *ddl, dbInfo *model.DBInfo, tblInfo *model.TableInfo, foreignKeyName string) *model.Job { + job := &model.Job{ + SchemaID: dbInfo.ID, + TableID: tblInfo.ID, + Type: model.ActionDropForeignKey, + Args: []interface{}{model.NewCIStr(foreignKeyName)}, + } + err := d.doDDLJob(ctx, job) + c.Assert(err, IsNil) + return job +} + +func getForeignKey(t table.Table, name string) *model.FKInfo { + for _, fk := range t.Meta().ForeignKeys { + // only public foreign key can be read. + if fk.State != model.StatePublic { + continue + } + if fk.Name.L == strings.ToLower(name) { + return fk + } + } + return nil +} + +func (s *testForeighKeySuite) testForeignKeyExist(c *C, t table.Table, name string, isExist bool) { + fk := getForeignKey(t, name) + if isExist { + c.Assert(fk, NotNil) + } else { + c.Assert(fk, IsNil) + } +} + +func (s *testForeighKeySuite) TestForeignKey(c *C) { + defer testleak.AfterTest(c)() + d := newDDL(s.store, nil, nil, 100*time.Millisecond) + tblInfo := testTableInfo(c, d, "t", 3) + ctx := testNewContext(c, d) + + _, err := ctx.GetTxn(true) + c.Assert(err, IsNil) + + testCreateTable(c, ctx, d, s.dbInfo, tblInfo) + + err = ctx.FinishTxn(false) + c.Assert(err, IsNil) + + checkOK := false + tc := &testDDLCallback{} + tc.onJobUpdated = func(job *model.Job) { + if job.State != model.JobDone { + return + } + + t := testGetTable(c, d, s.dbInfo.ID, tblInfo.ID) + s.testForeignKeyExist(c, t, "c1", true) + checkOK = true + } + + d.hook = tc + + d.close() + d.start() + + job := testCreateForeignKey(c, ctx, d, s.dbInfo, tblInfo, "c1_fk", []string{"c1"}, "t2", []string{"c1"}, ast.ReferOptionCascade, ast.ReferOptionSetNull) + + testCheckJobDone(c, d, job, true) + + err = ctx.FinishTxn(false) + c.Assert(err, IsNil) + + tc.onJobUpdated = func(job *model.Job) { + if job.State != model.JobDone { + return + } + + t := testGetTable(c, d, s.dbInfo.ID, tblInfo.ID) + s.testForeignKeyExist(c, t, "c1", true) + checkOK = true + } + + d.hook = tc + + d.close() + d.start() + + job = testDropForeignKey(c, ctx, d, s.dbInfo, tblInfo, "c1_fk") + testCheckJobDone(c, d, job, false) + + _, err = ctx.GetTxn(true) + c.Assert(err, IsNil) + + job = testDropTable(c, ctx, d, s.dbInfo, tblInfo) + testCheckJobDone(c, d, job, false) + + err = ctx.FinishTxn(false) + c.Assert(err, IsNil) + + d.close() +} diff --git a/executor/show.go b/executor/show.go index c18fd7a8de..7170f9a872 100644 --- a/executor/show.go +++ b/executor/show.go @@ -403,6 +403,34 @@ func (e *ShowExec) fetchShowCreateTable() error { buf.WriteString(",\n") } } + + for _, fk := range tb.Meta().ForeignKeys { + if fk.State != model.StatePublic { + continue + } + + buf.WriteString("\n") + cols := make([]string, 0, len(fk.Cols)) + for _, c := range fk.Cols { + cols = append(cols, c.L) + } + + refCols := make([]string, 0, len(fk.RefCols)) + for _, c := range fk.Cols { + refCols = append(refCols, c.L) + } + + buf.WriteString(fmt.Sprintf(" CONSTRAINT `%s` FOREIGN KEY (`%s`)", fk.Name.L, strings.Join(cols, "`,`"))) + buf.WriteString(fmt.Sprintf(" REFERENCES `%s` (`%s`)", fk.RefTable.L, strings.Join(refCols, "`,`"))) + + if ast.ReferOptionType(fk.OnDelete) != ast.ReferOptionNoOption { + buf.WriteString(fmt.Sprintf(" ON DELETE %s", ast.ReferOptionType(fk.OnDelete))) + } + + if ast.ReferOptionType(fk.OnUpdate) != ast.ReferOptionNoOption { + buf.WriteString(fmt.Sprintf(" ON UPDATE %s", ast.ReferOptionType(fk.OnUpdate))) + } + } buf.WriteString("\n") buf.WriteString(") ENGINE=InnoDB") diff --git a/executor/show_test.go b/executor/show_test.go index 8b7ed4c2e4..fa89701847 100644 --- a/executor/show_test.go +++ b/executor/show_test.go @@ -88,3 +88,51 @@ func (s statistics) Stats() (map[string]interface{}, error) { m["test_interface_slice"] = []interface{}{"a", "b", "c"} return m, nil } + +func (s *testSuite) TestForeignKeyInShowCreateTable(c *C) { + tk := testkit.NewTestKit(c, s.store) + tk.MustExec("use test") + testSQL := `drop table if exists show_test` + tk.MustExec(testSQL) + testSQL = `drop table if exists t1` + tk.MustExec(testSQL) + testSQL = `CREATE TABLE t1 (id int PRIMARY KEY AUTO_INCREMENT)` + tk.MustExec(testSQL) + + testSQL = "create table show_test (`id` int PRIMARY KEY AUTO_INCREMENT, FOREIGN KEY `fk` (`id`) REFERENCES `t1` (`a`) ON DELETE CASCADE ON UPDATE CASCADE) ENGINE=InnoDB" + tk.MustExec(testSQL) + testSQL = "show create table show_test;" + result := tk.MustQuery(testSQL) + c.Check(result.Rows(), HasLen, 1) + row := result.Rows()[0] + expectedRow := []interface{}{ + "show_test", "CREATE TABLE `show_test` (\n `id` int(11) NOT NULL AUTO_INCREMENT,\n PRIMARY KEY (`id`) \n CONSTRAINT `fk` FOREIGN KEY (`id`) REFERENCES `t1` (`id`) ON DELETE CASCADE ON UPDATE CASCADE\n) ENGINE=InnoDB"} + for i, r := range row { + c.Check(r, Equals, expectedRow[i]) + } + + testSQL = "alter table show_test drop foreign key `fk`" + tk.MustExec(testSQL) + testSQL = "show create table show_test;" + result = tk.MustQuery(testSQL) + c.Check(result.Rows(), HasLen, 1) + row = result.Rows()[0] + expectedRow = []interface{}{ + "show_test", "CREATE TABLE `show_test` (\n `id` int(11) NOT NULL AUTO_INCREMENT,\n PRIMARY KEY (`id`) \n) ENGINE=InnoDB"} + for i, r := range row { + c.Check(r, Equals, expectedRow[i]) + } + + testSQL = "ALTER TABLE SHOW_TEST ADD CONSTRAINT `fk` FOREIGN KEY (`id`) REFERENCES `t1` (`id`) ON DELETE CASCADE ON UPDATE CASCADE\n " + tk.MustExec(testSQL) + testSQL = "show create table show_test;" + result = tk.MustQuery(testSQL) + c.Check(result.Rows(), HasLen, 1) + row = result.Rows()[0] + expectedRow = []interface{}{ + "show_test", "CREATE TABLE `show_test` (\n `id` int(11) NOT NULL AUTO_INCREMENT,\n PRIMARY KEY (`id`) \n CONSTRAINT `fk` FOREIGN KEY (`id`) REFERENCES `t1` (`id`) ON DELETE CASCADE ON UPDATE CASCADE\n) ENGINE=InnoDB"} + for i, r := range row { + c.Check(r, Equals, expectedRow[i]) + } + +} diff --git a/infoschema/infoschema.go b/infoschema/infoschema.go index b657c10332..c26c307e2f 100644 --- a/infoschema/infoschema.go +++ b/infoschema/infoschema.go @@ -39,7 +39,12 @@ var ( ErrTableNotExists = terror.ClassSchema.New(codeTableNotExists, "table not exists") // ErrColumnNotExists returns for column not exists. ErrColumnNotExists = terror.ClassSchema.New(codeColumnNotExists, "field not exists") - + // ErrForeignKeyNotMatch returns for foreign key not match. + ErrForeignKeyNotMatch = terror.ClassSchema.New(codeCannotAddForeign, "foreign key not match") + // ErrForeignKeyExists returns for foreign key exists. + ErrForeignKeyExists = terror.ClassSchema.New(codeCannotAddForeign, "foreign key already exists") + // ErrForeignKeyNotExists returns for foreign key not exists. + ErrForeignKeyNotExists = terror.ClassSchema.New(codeForeignKeyNotExists, "foreign key not exists") // ErrDatabaseExists returns for database already exists. ErrDatabaseExists = terror.ClassSchema.New(codeDatabaseExists, "database already exists") // ErrTableExists returns for table already exists. @@ -456,6 +461,9 @@ const ( codeTableNotExists = 1146 codeColumnNotExists = 1054 + codeCannotAddForeign = 1215 + codeForeignKeyNotExists = 1091 + codeDatabaseExists = 1007 codeTableExists = 1050 codeBadTable = 1051 @@ -465,15 +473,17 @@ const ( func init() { schemaMySQLErrCodes := map[terror.ErrCode]uint16{ - codeDBDropExists: mysql.ErrDBDropExists, - codeDatabaseNotExists: mysql.ErrBadDB, - codeTableNotExists: mysql.ErrNoSuchTable, - codeColumnNotExists: mysql.ErrBadField, - codeDatabaseExists: mysql.ErrDBCreateExists, - codeTableExists: mysql.ErrTableExists, - codeBadTable: mysql.ErrBadTable, - codeColumnExists: mysql.ErrDupFieldName, - codeIndexExists: mysql.ErrDupIndex, + codeDBDropExists: mysql.ErrDBDropExists, + codeDatabaseNotExists: mysql.ErrBadDB, + codeTableNotExists: mysql.ErrNoSuchTable, + codeColumnNotExists: mysql.ErrBadField, + codeCannotAddForeign: mysql.ErrCannotAddForeign, + codeForeignKeyNotExists: mysql.ErrCantDropFieldOrKey, + codeDatabaseExists: mysql.ErrDBCreateExists, + codeTableExists: mysql.ErrTableExists, + codeBadTable: mysql.ErrBadTable, + codeColumnExists: mysql.ErrDupFieldName, + codeIndexExists: mysql.ErrDupIndex, } terror.ErrClassToMySQLCodes[terror.ClassSchema] = schemaMySQLErrCodes } diff --git a/model/ddl.go b/model/ddl.go index d8809dac14..72db198972 100644 --- a/model/ddl.go +++ b/model/ddl.go @@ -34,6 +34,8 @@ const ( ActionDropColumn ActionAddIndex ActionDropIndex + ActionAddForeignKey + ActionDropForeignKey ) func (action ActionType) String() string { @@ -54,6 +56,10 @@ func (action ActionType) String() string { return "add index" case ActionDropIndex: return "drop index" + case ActionAddForeignKey: + return "add foreign key" + case ActionDropForeignKey: + return "drop foreign key" default: return "none" } diff --git a/model/model.go b/model/model.go index a90863151a..c586e759d5 100644 --- a/model/model.go +++ b/model/model.go @@ -80,12 +80,13 @@ type TableInfo struct { Charset string `json:"charset"` Collate string `json:"collate"` // Columns are listed in the order in which they appear in the schema. - Columns []*ColumnInfo `json:"cols"` - Indices []*IndexInfo `json:"index_info"` - State SchemaState `json:"state"` - PKIsHandle bool `json:"pk_is_handle"` - Comment string `json:"comment"` - AutoIncID int64 `json:"auto_inc_id"` + Columns []*ColumnInfo `json:"cols"` + Indices []*IndexInfo `json:"index_info"` + ForeignKeys []*FKInfo `json:"fk_info"` + State SchemaState `json:"state"` + PKIsHandle bool `json:"pk_is_handle"` + Comment string `json:"comment"` + AutoIncID int64 `json:"auto_inc_id"` } // Clone clones TableInfo. @@ -93,6 +94,7 @@ func (t *TableInfo) Clone() *TableInfo { nt := *t nt.Columns = make([]*ColumnInfo, len(t.Columns)) nt.Indices = make([]*IndexInfo, len(t.Indices)) + nt.ForeignKeys = make([]*FKInfo, len(t.ForeignKeys)) for i := range t.Columns { nt.Columns[i] = t.Columns[i].Clone() @@ -101,6 +103,11 @@ func (t *TableInfo) Clone() *TableInfo { for i := range t.Indices { nt.Indices[i] = t.Indices[i].Clone() } + + for i := range t.ForeignKeys { + nt.ForeignKeys[i] = t.ForeignKeys[i].Clone() + } + return &nt } @@ -162,6 +169,30 @@ func (index *IndexInfo) Clone() *IndexInfo { return &ni } +// FKInfo provides meta data describing a foreign key constraint. +type FKInfo struct { + ID int64 `json:"id"` + Name CIStr `json:"fk_name"` + RefTable CIStr `json:"ref_table"` + RefCols []CIStr `json:"ref_cols"` + Cols []CIStr `json:"cols"` + OnDelete int `json:"on_delete"` + OnUpdate int `json:"on_update"` + State SchemaState `json:"state"` +} + +// Clone clones FKInfo. +func (fk *FKInfo) Clone() *FKInfo { + nfk := *fk + + nfk.RefCols = make([]CIStr, len(fk.RefCols)) + nfk.Cols = make([]CIStr, len(fk.Cols)) + copy(nfk.RefCols, fk.RefCols) + copy(nfk.Cols, fk.Cols) + + return &nfk +} + // DBInfo provides meta data describing a DB. type DBInfo struct { ID int64 `json:"id"` // Database ID diff --git a/model/model_test.go b/model/model_test.go index 3c6a8ebf36..32ec639e6d 100644 --- a/model/model_test.go +++ b/model/model_test.go @@ -59,12 +59,13 @@ func (*testSuite) TestClone(c *C) { } table := &TableInfo{ - ID: 1, - Name: NewCIStr("t"), - Charset: "utf8", - Collate: "utf8", - Columns: []*ColumnInfo{column}, - Indices: []*IndexInfo{index}, + ID: 1, + Name: NewCIStr("t"), + Charset: "utf8", + Collate: "utf8", + Columns: []*ColumnInfo{column}, + Indices: []*IndexInfo{index}, + ForeignKeys: []*FKInfo{}, } dbInfo := &DBInfo{ diff --git a/parser/parser.y b/parser/parser.y index 9e4f38e371..f9ae6a96a2 100644 --- a/parser/parser.y +++ b/parser/parser.y @@ -360,7 +360,7 @@ import ( uint16Type "uint16" uint32Type "uint32" uint64Type "uint64" - uint8Type "uint8", + uint8Type "uint8" float32Type "float32" float64Type "float64" boolType "BOOL" @@ -380,6 +380,12 @@ import ( dayHour "DAY_HOUR" yearMonth "YEAR_MONTH" + restrict "RESTRICT" + cascade "CASCADE" + no "NO" + action "ACTION" + + %type AdminStmt "Check table statement or show ddl statement" AlterTableStmt "Alter table statement" @@ -525,6 +531,9 @@ import ( PrivLevel "Privilege scope" PrivType "Privilege type" ReferDef "Reference definition" + OnDeleteOpt "optional ON DELETE clause" + OnUpdateOpt "optional ON UPDATE clause" + ReferOpt "reference option" RegexpSym "REGEXP or RLIKE" ReplaceIntoStmt "REPLACE INTO statement" ReplacePriority "replace statement priority" @@ -648,6 +657,7 @@ import ( %left join inner cross left right full /* A dummy token to force the priority of TableRef production in a join. */ %left tableRefPriority +%precedence lowerThanOn %precedence on %left oror or %left xor @@ -1077,9 +1087,58 @@ ConstraintElem: } ReferDef: - "REFERENCES" TableName '(' IndexColNameList ')' + "REFERENCES" TableName '(' IndexColNameList ')' OnDeleteOpt OnUpdateOpt { - $$ = &ast.ReferenceDef{Table: $2.(*ast.TableName), IndexColNames: $4.([]*ast.IndexColName)} + var onDeleteOpt *ast.OnDeleteOpt + if $6 != nil { + onDeleteOpt = $6.(*ast.OnDeleteOpt) + } + var onUpdateOpt *ast.OnUpdateOpt + if $7 != nil { + onUpdateOpt = $7.(*ast.OnUpdateOpt) + } + $$ = &ast.ReferenceDef{ + Table: $2.(*ast.TableName), + IndexColNames: $4.([]*ast.IndexColName), + OnDelete: onDeleteOpt, + OnUpdate: onUpdateOpt, + } + } + +OnDeleteOpt: + { + $$ = &ast.OnDeleteOpt{} + } %prec lowerThanOn +| "ON" "DELETE" ReferOpt + { + $$ = &ast.OnDeleteOpt{ReferOpt: $3.(ast.ReferOptionType)} + } + +OnUpdateOpt: + { + $$ = &ast.OnUpdateOpt{} + } %prec lowerThanOn +| "ON" "UPDATE" ReferOpt + { + $$ = &ast.OnUpdateOpt{ReferOpt: $3.(ast.ReferOptionType)} + } + +ReferOpt: + "RESTRICT" + { + $$ = ast.ReferOptionRestrict + } +| "CASCADE" + { + $$ = ast.ReferOptionCascade + } +| "SET" "NULL" + { + $$ = ast.ReferOptionSetNull + } +| "NO" "ACTION" + { + $$ = ast.ReferOptionNoAction } /* diff --git a/parser/parser_test.go b/parser/parser_test.go index 6ed194a22b..aaac4e8a05 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -755,6 +755,24 @@ func (s *testParserSuite) TestDDL(c *C) { INDEX FK_a3t0m9apja9jmrn60uab30pqd USING BTREE (user_id) comment '' ) ENGINE=InnoDB AUTO_INCREMENT=95 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci ROW_FORMAT=COMPACT COMMENT='' CHECKSUM=0 DELAY_KEY_WRITE=0;`, true}, {`create table t (c int KEY);`, true}, + {`CREATE TABLE address ( + id bigint(20) NOT NULL AUTO_INCREMENT, + create_at datetime NOT NULL, + deleted tinyint(1) NOT NULL, + update_at datetime NOT NULL, + version bigint(20) DEFAULT NULL, + address varchar(128) NOT NULL, + address_detail varchar(128) NOT NULL, + cellphone varchar(16) NOT NULL, + latitude double NOT NULL, + longitude double NOT NULL, + name varchar(16) NOT NULL, + sex tinyint(1) NOT NULL, + user_id bigint(20) NOT NULL, + PRIMARY KEY (id), + CONSTRAINT FK_7rod8a71yep5vxasb0ms3osbg FOREIGN KEY (user_id) REFERENCES waimaiqa.user (id) ON DELETE CASCADE ON UPDATE NO ACTION, + INDEX FK_7rod8a71yep5vxasb0ms3osbg (user_id) comment '' + ) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci ROW_FORMAT=COMPACT COMMENT='' CHECKSUM=0 DELAY_KEY_WRITE=0;`, true}, } s.RunTest(c, table) } diff --git a/parser/scanner.l b/parser/scanner.l index 176508c0ad..4d6f385f32 100644 --- a/parser/scanner.l +++ b/parser/scanner.l @@ -585,6 +585,11 @@ day_minute {d}{a}{y}_{m}{i}{n}{u}{t}{e} day_hour {d}{a}{y}_{h}{o}{u}{r} year_month {y}{e}{a}{r}_{m}{o}{n}{t}{h} +restrict {r}{e}{s}{t}{r}{i}{c}{t} +cascade {c}{a}{s}{c}{a}{d}{e} +no {n}{o} +action {a}{c}{t}{i}{o}{n} + %yyc c %yyn c = l.next() %yyt l.sc @@ -1049,7 +1054,16 @@ redundant lval.item = string(l.val) return yearweek {year_month} lval.item = string(l.val) return yearMonth - + +{restrict} lval.item = string(l.val) + return restrict +{cascade} lval.item = string(l.val) + return cascade +{no} lval.item = string(l.val) + return no +{action} lval.item = string(l.val) + return action + {signed} lval.item = string(l.val) return signed {unsigned} return unsigned diff --git a/table/tables/tables.go b/table/tables/tables.go index 5d1e50fb2b..7e2c04b58a 100644 --- a/table/tables/tables.go +++ b/table/tables/tables.go @@ -87,6 +87,7 @@ func TableFromMeta(alloc autoid.Allocator, tblInfo *model.TableInfo) (table.Tabl t.indices = append(t.indices, idx) } + t.meta = tblInfo return t, nil }