Add option to detect unique constraint errors
This commit is contained in:
parent
68920449f9
commit
17a4bf87d7
@ -92,12 +92,12 @@ func Create(config *Config) func(db *gorm.DB) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
db.AddError(err)
|
db.AddErrorFromDB(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
db.AddError(err)
|
db.AddErrorFromDB(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -187,14 +187,14 @@ func CreateWithReturning(db *gorm.DB) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
db.AddError(err)
|
db.AddErrorFromDB(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if !db.DryRun && db.Error == nil {
|
} else if !db.DryRun && db.Error == nil {
|
||||||
if result, err := db.Statement.ConnPool.ExecContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...); err == nil {
|
if result, err := db.Statement.ConnPool.ExecContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...); err == nil {
|
||||||
db.RowsAffected, _ = result.RowsAffected()
|
db.RowsAffected, _ = result.RowsAffected()
|
||||||
} else {
|
} else {
|
||||||
db.AddError(err)
|
db.AddErrorFromDB(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ func RawExec(db *gorm.DB) {
|
|||||||
if db.Error == nil && !db.DryRun {
|
if db.Error == nil && !db.DryRun {
|
||||||
result, err := db.Statement.ConnPool.ExecContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...)
|
result, err := db.Statement.ConnPool.ExecContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
db.AddError(err)
|
db.AddErrorFromDB(err)
|
||||||
} else {
|
} else {
|
||||||
db.RowsAffected, _ = result.RowsAffected()
|
db.RowsAffected, _ = result.RowsAffected()
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ func Update(db *gorm.DB) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
db.RowsAffected, _ = result.RowsAffected()
|
db.RowsAffected, _ = result.RowsAffected()
|
||||||
} else {
|
} else {
|
||||||
db.AddError(err)
|
db.AddErrorFromDB(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
20
errors.go
20
errors.go
@ -2,6 +2,8 @@ package gorm
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -32,3 +34,21 @@ var (
|
|||||||
// ErrDryRunModeUnsupported dry run mode unsupported
|
// ErrDryRunModeUnsupported dry run mode unsupported
|
||||||
ErrDryRunModeUnsupported = errors.New("dry run mode unsupported")
|
ErrDryRunModeUnsupported = errors.New("dry run mode unsupported")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrUniqueConstraint unique constraint error
|
||||||
|
type ErrUniqueConstraint struct {
|
||||||
|
ConstraintName string
|
||||||
|
Columns []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrUniqueConstraint) Error() string {
|
||||||
|
if len(e.ConstraintName) > 0 {
|
||||||
|
return fmt.Sprintf("unique constraint '%s' error", e.ConstraintName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.Columns) > 0 {
|
||||||
|
return fmt.Sprintf("unique constraint on columns '%s' error", strings.Join(e.Columns, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unique constraint error"
|
||||||
|
}
|
||||||
|
44
gorm.go
44
gorm.go
@ -5,6 +5,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -252,6 +253,49 @@ func (db *DB) AddError(err error) error {
|
|||||||
return db.Error
|
return db.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddErrorFromDB add error from database
|
||||||
|
func (db *DB) AddErrorFromDB(err error) error {
|
||||||
|
msg := err.Error()
|
||||||
|
// === Unique constraint errors
|
||||||
|
if strings.HasPrefix(msg, "UNIQUE constraint failed:") {
|
||||||
|
// SQLite3
|
||||||
|
cols := strings.Split(msg[25:], ",")
|
||||||
|
for i := range cols {
|
||||||
|
cols[i] = strings.TrimSpace(cols[i])
|
||||||
|
}
|
||||||
|
err = &ErrUniqueConstraint{
|
||||||
|
Columns: cols,
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(msg, "Error 1062: Duplicate entry") {
|
||||||
|
// MySQL
|
||||||
|
constr := strings.Trim(strings.TrimSpace(msg[strings.Index(msg, "for key")+7:]), "'")
|
||||||
|
p := strings.Split(constr, ".")
|
||||||
|
if len(p) == 1 {
|
||||||
|
constr = p[0]
|
||||||
|
} else {
|
||||||
|
constr = p[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
err = &ErrUniqueConstraint{
|
||||||
|
ConstraintName: constr,
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(msg, "ERROR: duplicate key value violates unique constraint") {
|
||||||
|
// PostgreSQL
|
||||||
|
constr := ""
|
||||||
|
i := strings.Index(msg, `"`)
|
||||||
|
j := strings.LastIndex(msg, `"`)
|
||||||
|
if i != -1 && j != i {
|
||||||
|
constr = msg[i+1 : j]
|
||||||
|
}
|
||||||
|
|
||||||
|
err = &ErrUniqueConstraint{
|
||||||
|
ConstraintName: constr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.AddError(err)
|
||||||
|
}
|
||||||
|
|
||||||
// DB returns `*sql.DB`
|
// DB returns `*sql.DB`
|
||||||
func (db *DB) DB() (*sql.DB, error) {
|
func (db *DB) DB() (*sql.DB, error) {
|
||||||
connPool := db.ConnPool
|
connPool := db.ConnPool
|
||||||
|
@ -23,6 +23,7 @@ type User struct {
|
|||||||
Team []*User `gorm:"foreignkey:ManagerID"`
|
Team []*User `gorm:"foreignkey:ManagerID"`
|
||||||
Languages []*tests.Language `gorm:"many2many:UserSpeak"`
|
Languages []*tests.Language `gorm:"many2many:UserSpeak"`
|
||||||
Friends []*User `gorm:"many2many:user_friends"`
|
Friends []*User `gorm:"many2many:user_friends"`
|
||||||
|
Contacts []*tests.Contact
|
||||||
Active *bool
|
Active *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,6 +108,10 @@ func checkUserSchema(t *testing.T, user *schema.Schema) {
|
|||||||
}},
|
}},
|
||||||
References: []Reference{{"ID", "User", "UserID", "user_friends", "", true}, {"ID", "User", "FriendID", "user_friends", "", false}},
|
References: []Reference{{"ID", "User", "UserID", "user_friends", "", true}, {"ID", "User", "FriendID", "user_friends", "", false}},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "Contacts", Type: schema.HasMany, Schema: "User", FieldSchema: "Contact",
|
||||||
|
References: []Reference{{"ID", "User", "UserID", "Contact", "", true}},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, relation := range relations {
|
for _, relation := range relations {
|
||||||
|
43
tests/create_unique_test.go
Normal file
43
tests/create_unique_test.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package tests_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
. "gorm.io/gorm/utils/tests"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateUniqueConstraint(t *testing.T) {
|
||||||
|
user1 := GetUser("create-unique-constraint", Config{})
|
||||||
|
if err := DB.Create(user1).Error; err != nil {
|
||||||
|
t.Fatalf("errors happened when create: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user1Contact := &Contact{UserID: &user1.ID, Email: "create-unique-constraint@email"}
|
||||||
|
if err := DB.Create(user1Contact).Error; err != nil {
|
||||||
|
t.Fatalf("errors happened when create cotract: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user2 := GetUser("create-unique-constraint2", Config{})
|
||||||
|
if err := DB.Create(user2).Error; err != nil {
|
||||||
|
t.Fatalf("errors happened when create: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user2Contact := &Contact{UserID: &user2.ID, Email: "create-unique-constraint@email"}
|
||||||
|
err := DB.Create(user2Contact).Error
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should return unique constraint error")
|
||||||
|
}
|
||||||
|
e, ok := err.(*gorm.ErrUniqueConstraint)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("should return unique constraint error, got err %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.ConstraintName) > 0 {
|
||||||
|
AssertEqual(t, e.ConstraintName, "idx_email")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.Columns) > 0 {
|
||||||
|
AssertEqual(t, e.Columns[0], "contacts.email")
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@ type Config struct {
|
|||||||
Team int
|
Team int
|
||||||
Languages int
|
Languages int
|
||||||
Friends int
|
Friends int
|
||||||
|
Contacts int
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUser(name string, config Config) *User {
|
func GetUser(name string, config Config) *User {
|
||||||
@ -65,6 +66,10 @@ func GetUser(name string, config Config) *User {
|
|||||||
user.Friends = append(user.Friends, GetUser(name+"_friend_"+strconv.Itoa(i+1), Config{}))
|
user.Friends = append(user.Friends, GetUser(name+"_friend_"+strconv.Itoa(i+1), Config{}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := 0; i < config.Contacts; i++ {
|
||||||
|
user.Contacts = append(user.Contacts, &Contact{Email: name + "_" + strconv.Itoa(i+1) + "@email"})
|
||||||
|
}
|
||||||
|
|
||||||
return &user
|
return &user
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,6 +92,19 @@ func CheckPet(t *testing.T, pet Pet, expect Pet) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CheckContact(t *testing.T, contact Contact, expect Contact) {
|
||||||
|
if contact.ID != 0 {
|
||||||
|
var newContact Contact
|
||||||
|
if err := DB.Where("id = ?", contact.ID).First(&newContact).Error; err != nil {
|
||||||
|
t.Fatalf("errors happened when query: %v", err)
|
||||||
|
} else {
|
||||||
|
AssertObjEqual(t, newContact, contact, "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "UserID", "Email")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AssertObjEqual(t, contact, expect, "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "UserID", "Email")
|
||||||
|
}
|
||||||
|
|
||||||
func CheckUser(t *testing.T, user User, expect User) {
|
func CheckUser(t *testing.T, user User, expect User) {
|
||||||
if user.ID != 0 {
|
if user.ID != 0 {
|
||||||
var newUser User
|
var newUser User
|
||||||
@ -227,4 +245,26 @@ func CheckUser(t *testing.T, user User, expect User) {
|
|||||||
AssertObjEqual(t, friend, expect.Friends[idx], "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "Name", "Age", "Birthday", "CompanyID", "ManagerID", "Active")
|
AssertObjEqual(t, friend, expect.Friends[idx], "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "Name", "Age", "Birthday", "CompanyID", "ManagerID", "Active")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Contacts", func(t *testing.T) {
|
||||||
|
if len(user.Contacts) != len(expect.Contacts) {
|
||||||
|
t.Fatalf("contacts should equal, expect: %v, got %v", len(expect.Contacts), len(user.Contacts))
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(user.Contacts, func(i, j int) bool {
|
||||||
|
return user.Contacts[i].ID > user.Contacts[j].ID
|
||||||
|
})
|
||||||
|
|
||||||
|
sort.Slice(expect.Contacts, func(i, j int) bool {
|
||||||
|
return expect.Contacts[i].ID > expect.Contacts[j].ID
|
||||||
|
})
|
||||||
|
|
||||||
|
for idx, contact := range user.Contacts {
|
||||||
|
if contact == nil || expect.Contacts[idx] == nil {
|
||||||
|
t.Errorf("contact#%v should equal, expect: %v, got %v", idx, expect.Contacts[idx], contact)
|
||||||
|
} else {
|
||||||
|
CheckContact(t, *contact, *expect.Contacts[idx])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestMigrate(t *testing.T) {
|
func TestMigrate(t *testing.T) {
|
||||||
allModels := []interface{}{&User{}, &Account{}, &Pet{}, &Company{}, &Toy{}, &Language{}}
|
allModels := []interface{}{&User{}, &Account{}, &Pet{}, &Company{}, &Toy{}, &Language{}, &Contact{}}
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
rand.Shuffle(len(allModels), func(i, j int) { allModels[i], allModels[j] = allModels[j], allModels[i] })
|
rand.Shuffle(len(allModels), func(i, j int) { allModels[i], allModels[j] = allModels[j], allModels[i] })
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ func OpenTestConnection() (db *gorm.DB, err error) {
|
|||||||
|
|
||||||
func RunMigrations() {
|
func RunMigrations() {
|
||||||
var err error
|
var err error
|
||||||
allModels := []interface{}{&User{}, &Account{}, &Pet{}, &Company{}, &Toy{}, &Language{}}
|
allModels := []interface{}{&User{}, &Account{}, &Pet{}, &Company{}, &Toy{}, &Language{}, &Contact{}}
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
rand.Shuffle(len(allModels), func(i, j int) { allModels[i], allModels[j] = allModels[j], allModels[i] })
|
rand.Shuffle(len(allModels), func(i, j int) { allModels[i], allModels[j] = allModels[j], allModels[i] })
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ type User struct {
|
|||||||
Team []User `gorm:"foreignkey:ManagerID"`
|
Team []User `gorm:"foreignkey:ManagerID"`
|
||||||
Languages []Language `gorm:"many2many:UserSpeak;"`
|
Languages []Language `gorm:"many2many:UserSpeak;"`
|
||||||
Friends []*User `gorm:"many2many:user_friends;"`
|
Friends []*User `gorm:"many2many:user_friends;"`
|
||||||
|
Contacts []*Contact
|
||||||
Active bool
|
Active bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,3 +59,9 @@ type Language struct {
|
|||||||
Code string `gorm:"primarykey"`
|
Code string `gorm:"primarykey"`
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Contact struct {
|
||||||
|
gorm.Model
|
||||||
|
UserID *uint
|
||||||
|
Email string `gorm:"index:idx_email,unique"`
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user