mirror of https://github.com/go-gorm/gorm.git
483 lines
14 KiB
Go
483 lines
14 KiB
Go
package gorm_test
|
|
|
|
import (
|
|
"database/sql"
|
|
"database/sql/driver"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jinzhu/gorm"
|
|
)
|
|
|
|
type User struct {
|
|
Id int64
|
|
Age int64
|
|
UserNum Num
|
|
Name string `sql:"size:255"`
|
|
Email string
|
|
Birthday *time.Time // Time
|
|
CreatedAt time.Time // CreatedAt: Time of record is created, will be insert automatically
|
|
UpdatedAt time.Time // UpdatedAt: Time of record is updated, will be updated automatically
|
|
Emails []Email // Embedded structs
|
|
BillingAddress Address // Embedded struct
|
|
BillingAddressID sql.NullInt64 // Embedded struct's foreign key
|
|
ShippingAddress Address // Embedded struct
|
|
ShippingAddressId int64 // Embedded struct's foreign key
|
|
CreditCard CreditCard
|
|
Latitude float64
|
|
Languages []Language `gorm:"many2many:user_languages;"`
|
|
CompanyID *int
|
|
Company Company
|
|
Role Role
|
|
Password EncryptedData
|
|
PasswordHash []byte
|
|
IgnoreMe int64 `sql:"-"`
|
|
IgnoreStringSlice []string `sql:"-"`
|
|
Ignored struct{ Name string } `sql:"-"`
|
|
IgnoredPointer *User `sql:"-"`
|
|
}
|
|
|
|
type NotSoLongTableName struct {
|
|
Id int64
|
|
ReallyLongThingID int64
|
|
ReallyLongThing ReallyLongTableNameToTestMySQLNameLengthLimit
|
|
}
|
|
|
|
type ReallyLongTableNameToTestMySQLNameLengthLimit struct {
|
|
Id int64
|
|
}
|
|
|
|
type ReallyLongThingThatReferencesShort struct {
|
|
Id int64
|
|
ShortID int64
|
|
Short Short
|
|
}
|
|
|
|
type Short struct {
|
|
Id int64
|
|
}
|
|
|
|
type CreditCard struct {
|
|
ID int8
|
|
Number string
|
|
UserId sql.NullInt64
|
|
CreatedAt time.Time `sql:"not null"`
|
|
UpdatedAt time.Time
|
|
DeletedAt *time.Time `sql:"column:deleted_time"`
|
|
}
|
|
|
|
type Email struct {
|
|
Id int16
|
|
UserId int
|
|
Email string `sql:"type:varchar(100);"`
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
type Address struct {
|
|
ID int
|
|
Address1 string
|
|
Address2 string
|
|
Post string
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
DeletedAt *time.Time
|
|
}
|
|
|
|
type Language struct {
|
|
gorm.Model
|
|
Name string
|
|
Users []User `gorm:"many2many:user_languages;"`
|
|
}
|
|
|
|
type Product struct {
|
|
Id int64
|
|
Code string
|
|
Price int64
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
AfterFindCallTimes int64
|
|
BeforeCreateCallTimes int64
|
|
AfterCreateCallTimes int64
|
|
BeforeUpdateCallTimes int64
|
|
AfterUpdateCallTimes int64
|
|
BeforeSaveCallTimes int64
|
|
AfterSaveCallTimes int64
|
|
BeforeDeleteCallTimes int64
|
|
AfterDeleteCallTimes int64
|
|
}
|
|
|
|
type Company struct {
|
|
Id int64
|
|
Name string
|
|
Owner *User `sql:"-"`
|
|
}
|
|
|
|
type EncryptedData []byte
|
|
|
|
func (data *EncryptedData) Scan(value interface{}) error {
|
|
if b, ok := value.([]byte); ok {
|
|
if len(b) < 3 || b[0] != '*' || b[1] != '*' || b[2] != '*' {
|
|
return errors.New("Too short")
|
|
}
|
|
|
|
*data = b[3:]
|
|
return nil
|
|
}
|
|
|
|
return errors.New("Bytes expected")
|
|
}
|
|
|
|
func (data EncryptedData) Value() (driver.Value, error) {
|
|
if len(data) > 0 && data[0] == 'x' {
|
|
//needed to test failures
|
|
return nil, errors.New("Should not start with 'x'")
|
|
}
|
|
|
|
//prepend asterisks
|
|
return append([]byte("***"), data...), nil
|
|
}
|
|
|
|
type Role struct {
|
|
Name string `gorm:"size:256"`
|
|
}
|
|
|
|
func (role *Role) Scan(value interface{}) error {
|
|
if b, ok := value.([]uint8); ok {
|
|
role.Name = string(b)
|
|
} else {
|
|
role.Name = value.(string)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (role Role) Value() (driver.Value, error) {
|
|
return role.Name, nil
|
|
}
|
|
|
|
func (role Role) IsAdmin() bool {
|
|
return role.Name == "admin"
|
|
}
|
|
|
|
type Num int64
|
|
|
|
func (i *Num) Scan(src interface{}) error {
|
|
switch s := src.(type) {
|
|
case []byte:
|
|
case int64:
|
|
*i = Num(s)
|
|
default:
|
|
return errors.New("Cannot scan NamedInt from " + reflect.ValueOf(src).String())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type Animal struct {
|
|
Counter uint64 `gorm:"primary_key:yes"`
|
|
Name string `sql:"DEFAULT:'galeone'"`
|
|
From string //test reserved sql keyword as field name
|
|
Age time.Time `sql:"DEFAULT:current_timestamp"`
|
|
unexported string // unexported value
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
type JoinTable struct {
|
|
From uint64
|
|
To uint64
|
|
Time time.Time `sql:"default: null"`
|
|
}
|
|
|
|
type Post struct {
|
|
Id int64
|
|
CategoryId sql.NullInt64
|
|
MainCategoryId int64
|
|
Title string
|
|
Body string
|
|
Comments []*Comment
|
|
Category Category
|
|
MainCategory Category
|
|
}
|
|
|
|
type Category struct {
|
|
gorm.Model
|
|
Name string
|
|
|
|
Categories []Category
|
|
CategoryID *uint
|
|
}
|
|
|
|
type Comment struct {
|
|
gorm.Model
|
|
PostId int64
|
|
Content string
|
|
Post Post
|
|
}
|
|
|
|
// Scanner
|
|
type NullValue struct {
|
|
Id int64
|
|
Name sql.NullString `sql:"not null"`
|
|
Gender *sql.NullString `sql:"not null"`
|
|
Age sql.NullInt64
|
|
Male sql.NullBool
|
|
Height sql.NullFloat64
|
|
AddedAt NullTime
|
|
}
|
|
|
|
type NullTime struct {
|
|
Time time.Time
|
|
Valid bool
|
|
}
|
|
|
|
func (nt *NullTime) Scan(value interface{}) error {
|
|
if value == nil {
|
|
nt.Valid = false
|
|
return nil
|
|
}
|
|
nt.Time, nt.Valid = value.(time.Time), true
|
|
return nil
|
|
}
|
|
|
|
func (nt NullTime) Value() (driver.Value, error) {
|
|
if !nt.Valid {
|
|
return nil, nil
|
|
}
|
|
return nt.Time, nil
|
|
}
|
|
|
|
func getPreparedUser(name string, role string) *User {
|
|
var company Company
|
|
DB.Where(Company{Name: role}).FirstOrCreate(&company)
|
|
|
|
return &User{
|
|
Name: name,
|
|
Age: 20,
|
|
Role: Role{role},
|
|
BillingAddress: Address{Address1: fmt.Sprintf("Billing Address %v", name)},
|
|
ShippingAddress: Address{Address1: fmt.Sprintf("Shipping Address %v", name)},
|
|
CreditCard: CreditCard{Number: fmt.Sprintf("123456%v", name)},
|
|
Emails: []Email{
|
|
{Email: fmt.Sprintf("user_%v@example1.com", name)}, {Email: fmt.Sprintf("user_%v@example2.com", name)},
|
|
},
|
|
Company: company,
|
|
Languages: []Language{
|
|
{Name: fmt.Sprintf("lang_1_%v", name)},
|
|
{Name: fmt.Sprintf("lang_2_%v", name)},
|
|
},
|
|
}
|
|
}
|
|
|
|
func runMigration() {
|
|
if err := DB.DropTableIfExists(&User{}).Error; err != nil {
|
|
fmt.Printf("Got error when try to delete table users, %+v\n", err)
|
|
}
|
|
|
|
for _, table := range []string{"animals", "user_languages"} {
|
|
DB.Exec(fmt.Sprintf("drop table %v;", table))
|
|
}
|
|
|
|
values := []interface{}{&Short{}, &ReallyLongThingThatReferencesShort{}, &ReallyLongTableNameToTestMySQLNameLengthLimit{}, &NotSoLongTableName{}, &Product{}, &Email{}, &Address{}, &CreditCard{}, &Company{}, &Role{}, &Language{}, &HNPost{}, &EngadgetPost{}, &Animal{}, &User{}, &JoinTable{}, &Post{}, &Category{}, &Comment{}, &Cat{}, &Dog{}, &Hamster{}, &Toy{}, &ElementWithIgnoredField{}}
|
|
for _, value := range values {
|
|
DB.DropTable(value)
|
|
}
|
|
if err := DB.AutoMigrate(values...).Error; err != nil {
|
|
panic(fmt.Sprintf("No error should happen when create table, but got %+v", err))
|
|
}
|
|
}
|
|
|
|
func TestIndexes(t *testing.T) {
|
|
if err := DB.Model(&Email{}).AddIndex("idx_email_email", "email").Error; err != nil {
|
|
t.Errorf("Got error when tried to create index: %+v", err)
|
|
}
|
|
|
|
scope := DB.NewScope(&Email{})
|
|
if !scope.Dialect().HasIndex(scope.TableName(), "idx_email_email") {
|
|
t.Errorf("Email should have index idx_email_email")
|
|
}
|
|
|
|
if err := DB.Model(&Email{}).RemoveIndex("idx_email_email").Error; err != nil {
|
|
t.Errorf("Got error when tried to remove index: %+v", err)
|
|
}
|
|
|
|
if scope.Dialect().HasIndex(scope.TableName(), "idx_email_email") {
|
|
t.Errorf("Email's index idx_email_email should be deleted")
|
|
}
|
|
|
|
if err := DB.Model(&Email{}).AddIndex("idx_email_email_and_user_id", "user_id", "email").Error; err != nil {
|
|
t.Errorf("Got error when tried to create index: %+v", err)
|
|
}
|
|
|
|
if !scope.Dialect().HasIndex(scope.TableName(), "idx_email_email_and_user_id") {
|
|
t.Errorf("Email should have index idx_email_email_and_user_id")
|
|
}
|
|
|
|
if err := DB.Model(&Email{}).RemoveIndex("idx_email_email_and_user_id").Error; err != nil {
|
|
t.Errorf("Got error when tried to remove index: %+v", err)
|
|
}
|
|
|
|
if scope.Dialect().HasIndex(scope.TableName(), "idx_email_email_and_user_id") {
|
|
t.Errorf("Email's index idx_email_email_and_user_id should be deleted")
|
|
}
|
|
|
|
if err := DB.Model(&Email{}).AddUniqueIndex("idx_email_email_and_user_id", "user_id", "email").Error; err != nil {
|
|
t.Errorf("Got error when tried to create index: %+v", err)
|
|
}
|
|
|
|
if !scope.Dialect().HasIndex(scope.TableName(), "idx_email_email_and_user_id") {
|
|
t.Errorf("Email should have index idx_email_email_and_user_id")
|
|
}
|
|
|
|
if DB.Save(&User{Name: "unique_indexes", Emails: []Email{{Email: "user1@example.comiii"}, {Email: "user1@example.com"}, {Email: "user1@example.com"}}}).Error == nil {
|
|
t.Errorf("Should get to create duplicate record when having unique index")
|
|
}
|
|
|
|
var user = User{Name: "sample_user"}
|
|
DB.Save(&user)
|
|
if DB.Model(&user).Association("Emails").Append(Email{Email: "not-1duplicated@gmail.com"}, Email{Email: "not-duplicated2@gmail.com"}).Error != nil {
|
|
t.Errorf("Should get no error when append two emails for user")
|
|
}
|
|
|
|
if DB.Model(&user).Association("Emails").Append(Email{Email: "duplicated@gmail.com"}, Email{Email: "duplicated@gmail.com"}).Error == nil {
|
|
t.Errorf("Should get no duplicated email error when insert duplicated emails for a user")
|
|
}
|
|
|
|
if err := DB.Model(&Email{}).RemoveIndex("idx_email_email_and_user_id").Error; err != nil {
|
|
t.Errorf("Got error when tried to remove index: %+v", err)
|
|
}
|
|
|
|
if scope.Dialect().HasIndex(scope.TableName(), "idx_email_email_and_user_id") {
|
|
t.Errorf("Email's index idx_email_email_and_user_id should be deleted")
|
|
}
|
|
|
|
if DB.Save(&User{Name: "unique_indexes", Emails: []Email{{Email: "user1@example.com"}, {Email: "user1@example.com"}}}).Error != nil {
|
|
t.Errorf("Should be able to create duplicated emails after remove unique index")
|
|
}
|
|
}
|
|
|
|
type EmailWithIdx struct {
|
|
Id int64
|
|
UserId int64
|
|
Email string `sql:"index:idx_email_agent"`
|
|
UserAgent string `sql:"index:idx_email_agent"`
|
|
RegisteredAt *time.Time `sql:"unique_index"`
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
func TestAutoMigration(t *testing.T) {
|
|
DB.AutoMigrate(&Address{})
|
|
DB.DropTable(&EmailWithIdx{})
|
|
if err := DB.AutoMigrate(&EmailWithIdx{}).Error; err != nil {
|
|
t.Errorf("Auto Migrate should not raise any error")
|
|
}
|
|
|
|
now := time.Now()
|
|
DB.Save(&EmailWithIdx{Email: "jinzhu@example.org", UserAgent: "pc", RegisteredAt: &now})
|
|
|
|
scope := DB.NewScope(&EmailWithIdx{})
|
|
if !scope.Dialect().HasIndex(scope.TableName(), "idx_email_agent") {
|
|
t.Errorf("Failed to create index")
|
|
}
|
|
|
|
if !scope.Dialect().HasIndex(scope.TableName(), "uix_email_with_idxes_registered_at") {
|
|
t.Errorf("Failed to create index")
|
|
}
|
|
|
|
var bigemail EmailWithIdx
|
|
DB.First(&bigemail, "user_agent = ?", "pc")
|
|
if bigemail.Email != "jinzhu@example.org" || bigemail.UserAgent != "pc" || bigemail.RegisteredAt.IsZero() {
|
|
t.Error("Big Emails should be saved and fetched correctly")
|
|
}
|
|
}
|
|
|
|
type MultipleIndexes struct {
|
|
ID int64
|
|
UserID int64 `sql:"unique_index:uix_multipleindexes_user_name,uix_multipleindexes_user_email;index:idx_multipleindexes_user_other"`
|
|
Name string `sql:"unique_index:uix_multipleindexes_user_name"`
|
|
Email string `sql:"unique_index:,uix_multipleindexes_user_email"`
|
|
Other string `sql:"index:,idx_multipleindexes_user_other"`
|
|
}
|
|
|
|
func TestMultipleIndexes(t *testing.T) {
|
|
if err := DB.DropTableIfExists(&MultipleIndexes{}).Error; err != nil {
|
|
fmt.Printf("Got error when try to delete table multiple_indexes, %+v\n", err)
|
|
}
|
|
|
|
DB.AutoMigrate(&MultipleIndexes{})
|
|
if err := DB.AutoMigrate(&EmailWithIdx{}).Error; err != nil {
|
|
t.Errorf("Auto Migrate should not raise any error")
|
|
}
|
|
|
|
DB.Save(&MultipleIndexes{UserID: 1, Name: "jinzhu", Email: "jinzhu@example.org", Other: "foo"})
|
|
|
|
scope := DB.NewScope(&MultipleIndexes{})
|
|
if !scope.Dialect().HasIndex(scope.TableName(), "uix_multipleindexes_user_name") {
|
|
t.Errorf("Failed to create index")
|
|
}
|
|
|
|
if !scope.Dialect().HasIndex(scope.TableName(), "uix_multipleindexes_user_email") {
|
|
t.Errorf("Failed to create index")
|
|
}
|
|
|
|
if !scope.Dialect().HasIndex(scope.TableName(), "uix_multiple_indexes_email") {
|
|
t.Errorf("Failed to create index")
|
|
}
|
|
|
|
if !scope.Dialect().HasIndex(scope.TableName(), "idx_multipleindexes_user_other") {
|
|
t.Errorf("Failed to create index")
|
|
}
|
|
|
|
if !scope.Dialect().HasIndex(scope.TableName(), "idx_multiple_indexes_other") {
|
|
t.Errorf("Failed to create index")
|
|
}
|
|
|
|
var mutipleIndexes MultipleIndexes
|
|
DB.First(&mutipleIndexes, "name = ?", "jinzhu")
|
|
if mutipleIndexes.Email != "jinzhu@example.org" || mutipleIndexes.Name != "jinzhu" {
|
|
t.Error("MutipleIndexes should be saved and fetched correctly")
|
|
}
|
|
|
|
// Check unique constraints
|
|
if err := DB.Save(&MultipleIndexes{UserID: 1, Name: "name1", Email: "jinzhu@example.org", Other: "foo"}).Error; err == nil {
|
|
t.Error("MultipleIndexes unique index failed")
|
|
}
|
|
|
|
if err := DB.Save(&MultipleIndexes{UserID: 1, Name: "name1", Email: "foo@example.org", Other: "foo"}).Error; err != nil {
|
|
t.Error("MultipleIndexes unique index failed")
|
|
}
|
|
|
|
if err := DB.Save(&MultipleIndexes{UserID: 2, Name: "name1", Email: "jinzhu@example.org", Other: "foo"}).Error; err == nil {
|
|
t.Error("MultipleIndexes unique index failed")
|
|
}
|
|
|
|
if err := DB.Save(&MultipleIndexes{UserID: 2, Name: "name1", Email: "foo2@example.org", Other: "foo"}).Error; err != nil {
|
|
t.Error("MultipleIndexes unique index failed")
|
|
}
|
|
}
|
|
|
|
func TestModifyColumnType(t *testing.T) {
|
|
if dialect := os.Getenv("GORM_DIALECT"); dialect != "postgres" && dialect != "mysql" && dialect != "mssql" {
|
|
t.Skip("Skipping this because only postgres, mysql and mssql support altering a column type")
|
|
}
|
|
|
|
type ModifyColumnType struct {
|
|
gorm.Model
|
|
Name1 string `gorm:"length:100"`
|
|
Name2 string `gorm:"length:200"`
|
|
}
|
|
DB.DropTable(&ModifyColumnType{})
|
|
DB.CreateTable(&ModifyColumnType{})
|
|
|
|
name2Field, _ := DB.NewScope(&ModifyColumnType{}).FieldByName("Name2")
|
|
name2Type := DB.Dialect().DataTypeOf(name2Field.StructField)
|
|
|
|
if err := DB.Model(&ModifyColumnType{}).ModifyColumn("name1", name2Type).Error; err != nil {
|
|
t.Errorf("No error should happen when ModifyColumn, but got %v", err)
|
|
}
|
|
}
|