add foreign key on update and on delete support (#1152)

* add foreign key on update and on delete support
This commit is contained in:
zxylvlp
2016-04-27 19:44:00 +08:00
committed by zimulala
parent 42e4dfd428
commit e742def024
15 changed files with 784 additions and 51 deletions

View File

@ -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)
}

View File

@ -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

View File

@ -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

120
ddl/foreign_key.go Normal file
View File

@ -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)
}
}

177
ddl/foreign_key_test.go Normal file
View File

@ -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()
}

View File

@ -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")

View File

@ -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])
}
}

View File

@ -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
}

View File

@ -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"
}

View File

@ -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

View File

@ -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{

View File

@ -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 <item>
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
}
/*

View File

@ -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)
}

View File

@ -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

View File

@ -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
}