gorm/migration_test.go

580 lines
16 KiB
Go
Raw Normal View History

2014-07-29 06:59:13 +04:00
package gorm_test
import (
2016-03-08 16:45:20 +03:00
"database/sql"
"database/sql/driver"
"errors"
2014-07-29 06:59:13 +04:00
"fmt"
"os"
2016-03-08 16:45:20 +03:00
"reflect"
2018-02-11 18:52:38 +03:00
"strconv"
2014-07-29 06:59:13 +04:00
"testing"
2014-07-29 08:32:58 +04:00
"time"
2016-03-08 16:45:20 +03:00
"github.com/jinzhu/gorm"
2014-07-29 06:59:13 +04:00
)
2016-03-08 16:45:20 +03:00
type User struct {
Id int64
Age int64
UserNum Num
2016-03-10 12:35:19 +03:00
Name string `sql:"size:255"`
Email string
Birthday *time.Time // Time
2016-03-08 16:45:20 +03:00
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
2017-03-22 17:57:13 +03:00
Role Role
Password EncryptedData
2016-03-08 16:45:20 +03:00
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
2016-03-08 16:45:20 +03:00
}
type ReallyLongThingThatReferencesShort struct {
Id int64
ShortID int64
Short Short
}
type Short struct {
Id int64
}
2016-03-08 16:45:20 +03:00
type CreditCard struct {
ID int8
Number string
UserId sql.NullInt64
CreatedAt time.Time `sql:"not null"`
UpdatedAt time.Time
2017-02-01 16:33:36 +03:00
DeletedAt *time.Time `sql:"column:deleted_time"`
2016-03-08 16:45:20 +03:00
}
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 Place struct {
Id int64
PlaceAddressID int
PlaceAddress *Address `gorm:"save_associations:false"`
OwnerAddressID int
OwnerAddress *Address `gorm:"save_associations:true"`
}
type EncryptedData []byte
func (data *EncryptedData) Scan(value interface{}) error {
if b, ok := value.([]byte); ok {
2017-08-31 10:30:48 +03:00
if len(b) < 3 || b[0] != '*' || b[1] != '*' || b[2] != '*' {
return errors.New("Too short")
}
*data = b[3:]
return nil
}
2017-08-31 10:30:48 +03:00
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
}
2016-03-08 16:45:20 +03:00
type Role struct {
Name string `gorm:"size:256"`
2016-03-08 16:45:20 +03:00
}
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:
2018-02-11 18:52:38 +03:00
n, _ := strconv.Atoi(string(s))
*i = Num(n)
2016-03-08 16:45:20 +03:00
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
2016-10-20 08:27:26 +03:00
Categories []Category
CategoryID *uint
2016-03-08 16:45:20 +03:00
}
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)},
},
}
}
2014-07-29 06:59:13 +04:00
func runMigration() {
2015-02-28 12:01:27 +03:00
if err := DB.DropTableIfExists(&User{}).Error; err != nil {
2014-07-29 06:59:13 +04:00
fmt.Printf("Got error when try to delete table users, %+v\n", err)
}
2014-08-28 14:53:27 +04:00
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{}, &Place{}}
2014-08-28 14:53:27 +04:00
for _, value := range values {
DB.DropTable(value)
}
if err := DB.AutoMigrate(values...).Error; err != nil {
2014-07-29 13:28:10 +04:00
panic(fmt.Sprintf("No error should happen when create table, but got %+v", err))
}
2014-07-29 06:59:13 +04:00
}
func TestIndexes(t *testing.T) {
2014-08-28 11:33:43 +04:00
if err := DB.Model(&Email{}).AddIndex("idx_email_email", "email").Error; err != nil {
2014-07-29 06:59:13 +04:00
t.Errorf("Got error when tried to create index: %+v", err)
}
scope := DB.NewScope(&Email{})
2016-02-15 09:09:24 +03:00
if !scope.Dialect().HasIndex(scope.TableName(), "idx_email_email") {
t.Errorf("Email should have index idx_email_email")
}
2014-08-28 11:33:43 +04:00
if err := DB.Model(&Email{}).RemoveIndex("idx_email_email").Error; err != nil {
2014-07-29 06:59:13 +04:00
t.Errorf("Got error when tried to remove index: %+v", err)
}
2016-02-15 09:09:24 +03:00
if scope.Dialect().HasIndex(scope.TableName(), "idx_email_email") {
t.Errorf("Email's index idx_email_email should be deleted")
}
2014-08-28 11:33:43 +04:00
if err := DB.Model(&Email{}).AddIndex("idx_email_email_and_user_id", "user_id", "email").Error; err != nil {
2014-07-29 06:59:13 +04:00
t.Errorf("Got error when tried to create index: %+v", err)
}
2016-02-15 09:09:24 +03:00
if !scope.Dialect().HasIndex(scope.TableName(), "idx_email_email_and_user_id") {
t.Errorf("Email should have index idx_email_email_and_user_id")
}
2014-08-28 11:33:43 +04:00
if err := DB.Model(&Email{}).RemoveIndex("idx_email_email_and_user_id").Error; err != nil {
2014-07-29 06:59:13 +04:00
t.Errorf("Got error when tried to remove index: %+v", err)
}
2016-02-15 09:09:24 +03:00
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")
}
2014-08-28 11:33:43 +04:00
if err := DB.Model(&Email{}).AddUniqueIndex("idx_email_email_and_user_id", "user_id", "email").Error; err != nil {
2014-07-29 06:59:13 +04:00
t.Errorf("Got error when tried to create index: %+v", err)
}
2016-02-15 09:09:24 +03:00
if !scope.Dialect().HasIndex(scope.TableName(), "idx_email_email_and_user_id") {
t.Errorf("Email should have index idx_email_email_and_user_id")
}
2014-08-28 11:33:43 +04:00
if DB.Save(&User{Name: "unique_indexes", Emails: []Email{{Email: "user1@example.comiii"}, {Email: "user1@example.com"}, {Email: "user1@example.com"}}}).Error == nil {
2014-07-29 06:59:13 +04:00
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")
}
2014-08-28 11:33:43 +04:00
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)
}
2016-02-15 09:09:24 +03:00
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")
}
2014-08-28 11:33:43 +04:00
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")
}
2014-07-29 06:59:13 +04:00
}
2014-07-29 08:32:58 +04:00
2017-03-22 17:57:13 +03:00
type EmailWithIdx struct {
2014-07-29 08:32:58 +04:00
Id int64
UserId int64
Email string `sql:"index:idx_email_agent"`
UserAgent string `sql:"index:idx_email_agent"`
RegisteredAt *time.Time `sql:"unique_index"`
2014-07-29 08:32:58 +04:00
CreatedAt time.Time
UpdatedAt time.Time
}
func TestAutoMigration(t *testing.T) {
2014-11-25 09:41:09 +03:00
DB.AutoMigrate(&Address{})
2017-03-22 17:57:13 +03:00
DB.DropTable(&EmailWithIdx{})
if err := DB.AutoMigrate(&EmailWithIdx{}).Error; err != nil {
2014-07-29 08:32:58 +04:00
t.Errorf("Auto Migrate should not raise any error")
}
now := time.Now()
2017-03-22 17:57:13 +03:00
DB.Save(&EmailWithIdx{Email: "jinzhu@example.org", UserAgent: "pc", RegisteredAt: &now})
2014-07-29 08:32:58 +04:00
2017-03-22 17:57:13 +03:00
scope := DB.NewScope(&EmailWithIdx{})
2016-02-15 09:09:24 +03:00
if !scope.Dialect().HasIndex(scope.TableName(), "idx_email_agent") {
2015-03-09 12:22:16 +03:00
t.Errorf("Failed to create index")
}
2017-03-22 17:57:13 +03:00
if !scope.Dialect().HasIndex(scope.TableName(), "uix_email_with_idxes_registered_at") {
2015-03-09 12:22:16 +03:00
t.Errorf("Failed to create index")
}
2017-03-22 17:57:13 +03:00
var bigemail EmailWithIdx
2014-08-28 11:33:43 +04:00
DB.First(&bigemail, "user_agent = ?", "pc")
2014-07-29 08:32:58 +04:00
if bigemail.Email != "jinzhu@example.org" || bigemail.UserAgent != "pc" || bigemail.RegisteredAt.IsZero() {
t.Error("Big Emails should be saved and fetched correctly")
}
}
func TestCreateAndAutomigrateTransaction(t *testing.T) {
tx := DB.Begin()
func() {
type Bar struct {
ID uint
}
DB.DropTableIfExists(&Bar{})
if ok := DB.HasTable("bars"); ok {
t.Errorf("Table should not exist, but does")
}
if ok := tx.HasTable("bars"); ok {
t.Errorf("Table should not exist, but does")
}
}()
func() {
type Bar struct {
Name string
}
err := tx.CreateTable(&Bar{}).Error
if err != nil {
t.Errorf("Should have been able to create the table, but couldn't: %s", err)
}
if ok := tx.HasTable(&Bar{}); !ok {
t.Errorf("The transaction should be able to see the table")
}
}()
func() {
type Bar struct {
Stuff string
}
err := tx.AutoMigrate(&Bar{}).Error
if err != nil {
t.Errorf("Should have been able to alter the table, but couldn't")
}
}()
tx.Rollback()
}
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)
}
2016-06-16 12:58:25 +03:00
DB.AutoMigrate(&MultipleIndexes{})
2017-03-22 17:57:13 +03:00
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)
}
}
func TestIndexWithPrefixLength(t *testing.T) {
if dialect := os.Getenv("GORM_DIALECT"); dialect != "mysql" {
t.Skip("Skipping this because only mysql support setting an index prefix length")
}
type IndexWithPrefix struct {
gorm.Model
Name string
Description string `gorm:"type:text;index:idx_index_with_prefixes_length(100)"`
}
type IndexesWithPrefix struct {
gorm.Model
Name string
Description1 string `gorm:"type:text;index:idx_index_with_prefixes_length(100)"`
Description2 string `gorm:"type:text;index:idx_index_with_prefixes_length(100)"`
}
type IndexesWithPrefixAndWithoutPrefix struct {
gorm.Model
Name string `gorm:"index:idx_index_with_prefixes_length"`
Description string `gorm:"type:text;index:idx_index_with_prefixes_length(100)"`
}
tables := []interface{}{&IndexWithPrefix{}, &IndexesWithPrefix{}, &IndexesWithPrefixAndWithoutPrefix{}}
for _, table := range tables {
scope := DB.NewScope(table)
tableName := scope.TableName()
t.Run(fmt.Sprintf("Create index with prefix length: %s", tableName), func(t *testing.T) {
if err := DB.DropTableIfExists(table).Error; err != nil {
t.Errorf("Failed to drop %s table: %v", tableName, err)
}
if err := DB.CreateTable(table).Error; err != nil {
t.Errorf("Failed to create %s table: %v", tableName, err)
}
if !scope.Dialect().HasIndex(tableName, "idx_index_with_prefixes_length") {
t.Errorf("Failed to create %s table index:", tableName)
}
})
}
}