
* Correct ModifyColumn SQL syntax. The generated SQL for ModifyColumn was: `ALTER TABLE "tablename" MODIFY "columname" type` But should have been: `ALTER TABLE "tablename" ALTER COLUMN "columname" TYPE type` since Modify does not seem to be entirely compatible with all Engines * Test ModifyColumn * Skip ModifyColumnType test on incompatible DBs Some DB Engines don't fully support alter table so we skip when the dialect does not correspond to one of the ones that are known to support it.
460 lines
13 KiB
Go
460 lines
13 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
|
|
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 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) {
|
|
dialect := os.Getenv("GORM_DIALECT")
|
|
if 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)
|
|
}
|
|
}
|