From 699cb8fc5ba2c81d6a46826e361518a763aa00f2 Mon Sep 17 00:00:00 2001 From: Francesco Cartier Date: Fri, 13 May 2022 11:34:01 +0200 Subject: [PATCH 1/6] feat: add ON UPDATE and ON DELETE rules to belongs-to struct tag --- internal/dbtest/db_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/dbtest/db_test.go b/internal/dbtest/db_test.go index 404e8cdf2..343d0f1cb 100644 --- a/internal/dbtest/db_test.go +++ b/internal/dbtest/db_test.go @@ -879,7 +879,7 @@ func testWithForeignKeys(t *testing.T, db *bun.DB) { ID int `bun:",pk,autoincrement"` UserID int UserType string - User *User `bun:"rel:belongs-to,join:user_id=id,join:user_type=type"` + User *User `bun:"rel:belongs-to,join:user_id=id,join:user_type=type,on_update:cascade,on_delete:set null"` } if db.Dialect().Name() == dialect.SQLite { From 24981866ab47616fe124e91b3341505c96953712 Mon Sep 17 00:00:00 2001 From: Francesco Cartier Date: Fri, 13 May 2022 11:34:43 +0200 Subject: [PATCH 2/6] feat: add ON UPDATE and ON DELETE rules to belongs-to struct tag --- query_table_create.go | 9 ++++++--- schema/relation.go | 2 ++ schema/table.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/query_table_create.go b/query_table_create.go index 4aad10070..daa1ccca9 100644 --- a/query_table_create.go +++ b/query_table_create.go @@ -105,13 +105,16 @@ func (q *CreateTableQuery) TableSpace(tablespace string) *CreateTableQuery { func (q *CreateTableQuery) WithForeignKeys() *CreateTableQuery { for _, relation := range q.tableModel.Table().Relations { if relation.Type == schema.ManyToManyRelation || - relation.Type == schema.HasManyRelation { + relation.Type == schema.HasManyRelation { continue - } - q = q.ForeignKey("(?) REFERENCES ? (?)", + } + + q = q.ForeignKey("(?) REFERENCES ? (?) ? ?", Safe(appendColumns(nil, "", relation.BaseFields)), relation.JoinTable.SQLName, Safe(appendColumns(nil, "", relation.JoinFields)), + Safe(relation.OnUpdate), + Safe(relation.OnDelete), ) } return q diff --git a/schema/relation.go b/schema/relation.go index 8d1baeb3f..06ef8c05c 100644 --- a/schema/relation.go +++ b/schema/relation.go @@ -18,6 +18,8 @@ type Relation struct { JoinTable *Table BaseFields []*Field JoinFields []*Field + OnUpdate string + OnDelete string PolymorphicField *Field PolymorphicValue string diff --git a/schema/table.go b/schema/table.go index e39cefb30..627421274 100644 --- a/schema/table.go +++ b/schema/table.go @@ -479,6 +479,35 @@ func (t *Table) belongsToRelation(field *Field) *Relation { JoinTable: joinTable, } + rel.OnUpdate = "ON DELETE NO ACTION" + if onUpdate, ok := field.Tag.Options["on_update"]; ok { + if len(onUpdate) > 1 { + panic(fmt.Errorf("bun: %s belongs-to %s: on_update option must be a single field", t.TypeName, field.GoName)) + } + + rule := strings.ToUpper(onUpdate[0]) + if !isKnownFKRule(rule) { + internal.Warn.Printf("bun: %s belongs-to %s: unknown on_update rule %s", t.TypeName, field.GoName, rule) + } + + s := fmt.Sprintf("ON UPDATE %s", rule) + rel.OnUpdate = s + } + + rel.OnDelete = "ON DELETE NO ACTION" + if onDelete, ok := field.Tag.Options["on_delete"]; ok { + if len(onDelete) > 1 { + panic(fmt.Errorf("bun: %s belongs-to %s: on_delete option must be a single field", t.TypeName, field.GoName)) + } + + rule := strings.ToUpper(onDelete[0]) + if !isKnownFKRule(rule) { + internal.Warn.Printf("bun: %s belongs-to %s: unknown on_delete rule %s", t.TypeName, field.GoName, rule) + } + s := fmt.Sprintf("ON DELETE %s", rule) + rel.OnDelete = s + } + if join, ok := field.Tag.Options["join"]; ok { baseColumns, joinColumns := parseRelationJoin(join) for i, baseColumn := range baseColumns { @@ -859,6 +888,8 @@ func isKnownFieldOption(name string) bool { "autoincrement", "rel", "join", + "on_update", + "on_delete", "m2m", "polymorphic": return true @@ -866,6 +897,17 @@ func isKnownFieldOption(name string) bool { return false } +func isKnownFKRule(name string) bool { + switch name { + case "CASCADE", + "RESTRICT", + "SET NULL", + "SET DEFAULT": + return true + } + return false +} + func removeField(fields []*Field, field *Field) []*Field { for i, f := range fields { if f == field { From 15454387f9423a5c1512910f209c308c35bc9426 Mon Sep 17 00:00:00 2001 From: Francesco Cartier Date: Fri, 13 May 2022 13:34:43 +0200 Subject: [PATCH 3/6] fix OnUpdate default string --- schema/table.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema/table.go b/schema/table.go index 627421274..3abb354be 100644 --- a/schema/table.go +++ b/schema/table.go @@ -479,7 +479,7 @@ func (t *Table) belongsToRelation(field *Field) *Relation { JoinTable: joinTable, } - rel.OnUpdate = "ON DELETE NO ACTION" + rel.OnUpdate = "ON UPDATE NO ACTION" if onUpdate, ok := field.Tag.Options["on_update"]; ok { if len(onUpdate) > 1 { panic(fmt.Errorf("bun: %s belongs-to %s: on_update option must be a single field", t.TypeName, field.GoName)) From 0c20c0ee07a97f7ea4b41f8719db51cde753e12d Mon Sep 17 00:00:00 2001 From: Francesco Cartier Date: Fri, 13 May 2022 13:34:43 +0200 Subject: [PATCH 4/6] fix: OnUpdate default string --- schema/table.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema/table.go b/schema/table.go index 627421274..3abb354be 100644 --- a/schema/table.go +++ b/schema/table.go @@ -479,7 +479,7 @@ func (t *Table) belongsToRelation(field *Field) *Relation { JoinTable: joinTable, } - rel.OnUpdate = "ON DELETE NO ACTION" + rel.OnUpdate = "ON UPDATE NO ACTION" if onUpdate, ok := field.Tag.Options["on_update"]; ok { if len(onUpdate) > 1 { panic(fmt.Errorf("bun: %s belongs-to %s: on_update option must be a single field", t.TypeName, field.GoName)) From 2d4c0dae186a3a8eecabe4db6c8f79618956cfd3 Mon Sep 17 00:00:00 2001 From: Francesco Cartier Date: Fri, 13 May 2022 13:34:43 +0200 Subject: [PATCH 5/6] fix: change onUpdate default string --- schema/table.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema/table.go b/schema/table.go index 627421274..3abb354be 100644 --- a/schema/table.go +++ b/schema/table.go @@ -479,7 +479,7 @@ func (t *Table) belongsToRelation(field *Field) *Relation { JoinTable: joinTable, } - rel.OnUpdate = "ON DELETE NO ACTION" + rel.OnUpdate = "ON UPDATE NO ACTION" if onUpdate, ok := field.Tag.Options["on_update"]; ok { if len(onUpdate) > 1 { panic(fmt.Errorf("bun: %s belongs-to %s: on_update option must be a single field", t.TypeName, field.GoName)) From 9a3feb117e06de54d78e921f8bece756f54a40d8 Mon Sep 17 00:00:00 2001 From: Francesco Cartier Date: Sat, 14 May 2022 14:56:22 +0200 Subject: [PATCH 6/6] fix: added required test --- internal/dbtest/db_test.go | 91 +++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/internal/dbtest/db_test.go b/internal/dbtest/db_test.go index 343d0f1cb..4e620f119 100644 --- a/internal/dbtest/db_test.go +++ b/internal/dbtest/db_test.go @@ -254,6 +254,7 @@ func TestDB(t *testing.T) { {testJSONValuer}, {testSelectBool}, {testFKViolation}, + {testWithForeignKeysAndRules}, {testWithForeignKeys}, {testInterfaceAny}, {testInterfaceJSON}, @@ -869,6 +870,94 @@ func testFKViolation(t *testing.T, db *bun.DB) { require.Equal(t, 0, n) } +func testWithForeignKeysAndRules(t *testing.T, db *bun.DB) { + type User struct { + ID int `bun:",pk"` + Type string `bun:",pk"` + Name string + } + type Deck struct { + ID int `bun:",pk"` + UserID int + UserType string + User *User `bun:"rel:belongs-to,join:user_id=id,join:user_type=type,on_update:cascade,on_delete:set null"` + } + + if db.Dialect().Name() == dialect.SQLite { + _, err := db.Exec("PRAGMA foreign_keys = ON;") + require.NoError(t, err) + } + + for _, model := range []interface{}{(*Deck)(nil), (*User)(nil)} { + _, err := db.NewDropTable().Model(model).IfExists().Exec(ctx) + require.NoError(t, err) + } + + _, err := db.NewCreateTable(). + Model((*User)(nil)). + IfNotExists(). + Exec(ctx) + require.NoError(t, err) + + _, err = db.NewCreateTable(). + Model((*Deck)(nil)). + IfNotExists(). + WithForeignKeys(). + Exec(ctx) + require.NoError(t, err) + + // Empty deck should violate FK constraint. + _, err = db.NewInsert().Model(new(Deck)).Exec(ctx) + require.Error(t, err) + + // Create a deck that violates the user_id FK contraint + deck := &Deck{UserID: 42} + + _, err = db.NewInsert().Model(deck).Exec(ctx) + require.Error(t, err) + + decks := []*Deck{deck} + _, err = db.NewInsert().Model(&decks).Exec(ctx) + require.Error(t, err) + + n, err := db.NewSelect().Model((*Deck)(nil)).Count(ctx) + require.NoError(t, err) + require.Equal(t, 0, n) + + _, err = db.NewInsert().Model(&User{ID: 1, Type: "admin", Name: "root"}).Exec(ctx) + require.NoError(t, err) + res, err := db.NewInsert().Model(&Deck{UserID: 1, UserType: "admin"}).Exec(ctx) + require.NoError(t, err) + + affected, err := res.RowsAffected() + require.NoError(t, err) + require.Equal(t, int64(1), affected) + + // Update User ID and check for FK update + res, err = db.NewUpdate().Model(&User{}).Where("id = ?", 1).Where("type = ?", "admin").Set("id = ?", 2).Exec(ctx) + require.NoError(t, err) + + affected, err = res.RowsAffected() + require.NoError(t, err) + require.Equal(t, int64(1), affected) + + n, err = db.NewSelect().Model(&Deck{}).Where("user_id = 1").Count(ctx) + require.NoError(t, err) + require.Equal(t, 0, n) + + n, err = db.NewSelect().Model(&Deck{}).Where("user_id = 2").Count(ctx) + require.NoError(t, err) + require.Equal(t, 1, n) + + // Delete user and check for FK delete + _, err = db.NewDelete().Model(&User{}).Where("id = ?", 2).Exec(ctx) + require.NoError(t, err) + + n, err = db.NewSelect().Model(&Deck{}).Where("user_id = 2").Count(ctx) + require.NoError(t, err) + require.Equal(t, 0, n) +} + func testWithForeignKeys(t *testing.T, db *bun.DB) { type User struct { ID int `bun:",pk,autoincrement"` @@ -879,7 +968,7 @@ func testWithForeignKeys(t *testing.T, db *bun.DB) { ID int `bun:",pk,autoincrement"` UserID int UserType string - User *User `bun:"rel:belongs-to,join:user_id=id,join:user_type=type,on_update:cascade,on_delete:set null"` + User *User `bun:"rel:belongs-to,join:user_id=id,join:user_type=type"` } if db.Dialect().Name() == dialect.SQLite {