Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,3 @@ require (
github.com/jinzhu/now v1.1.5
golang.org/x/text v0.20.0
)

require (
github.com/mattn/go-sqlite3 v1.14.22 // indirect
gorm.io/driver/sqlite v1.6.0 // indirect
)
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,5 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
32 changes: 23 additions & 9 deletions migrator/migrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ func (m Migrator) AutoMigrate(values ...interface{}) error {
}
} else {
if err := m.RunWithValue(value, func(stmt *gorm.Statement) error {

if stmt.Schema == nil {
return errors.New("failed to get schema")
}
Expand Down Expand Up @@ -216,7 +215,6 @@ func (m Migrator) CreateTable(values ...interface{}) error {
for _, value := range m.ReorderModels(values, false) {
tx := m.DB.Session(&gorm.Session{})
if err := m.RunWithValue(value, func(stmt *gorm.Statement) (err error) {

if stmt.Schema == nil {
return errors.New("failed to get schema")
}
Expand Down Expand Up @@ -472,22 +470,38 @@ func (m Migrator) MigrateColumn(value interface{}, field *schema.Field, columnTy
}

// found, smart migrate
fullDataType := strings.TrimSpace(strings.ToLower(m.DB.Migrator().FullDataTypeOf(field).SQL))
realDataType := strings.ToLower(columnType.DatabaseTypeName())
fullDataType := strings.TrimSpace(strings.ToLower(m.DB.Migrator().FullDataTypeOf(field).SQL)) // from struct
realDataType := strings.ToLower(columnType.DatabaseTypeName()) // from db
var (
alterColumn bool
isSameType = fullDataType == realDataType
)

if !field.PrimaryKey {
// check type

if !strings.HasPrefix(fullDataType, realDataType) {
// check type aliases
aliases := m.DB.Migrator().GetTypeAliases(realDataType)
for _, alias := range aliases {
if strings.HasPrefix(fullDataType, alias) {
isSameType = true
break
// we must compare without any brackets or length specifiers
// also we compare in both directions in case the mapping misses one of both ways for a type

rdt := realDataType
if p := strings.IndexAny(realDataType, "(["); p >= 0 {
rdt = realDataType[:p]
}
fdt := fullDataType
if p := strings.IndexAny(fullDataType, "(["); p >= 0 {
fdt = fullDataType[:p]
}

types := []string{rdt, fdt}
for i := 0; !isSameType && i < len(types); i++ {
aliases := m.DB.Migrator().GetTypeAliases(types[i])
for _, alias := range aliases {
if strings.HasPrefix(types[1-i], alias) {
isSameType = true
break
}
}
}

Expand Down
211 changes: 211 additions & 0 deletions tests/migrate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,56 @@ import (

"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/logger"
"gorm.io/gorm/migrator"
"gorm.io/gorm/schema"
"gorm.io/gorm/utils"
. "gorm.io/gorm/utils/tests"
)

// testWriter captures log output for testing
type testWriter struct {
logs *[]string
}

func (w *testWriter) Write(p []byte) (n int, err error) {
*w.logs = append(*w.logs, string(p))
return len(p), nil
}

// testLogger captures SQL logs for testing
type testLogger struct {
logs *[]string
}

func (l *testLogger) LogMode(level logger.LogLevel) logger.Interface {
return l
}

func (l *testLogger) Info(ctx context.Context, msg string, data ...interface{}) {
log := fmt.Sprintf(msg, data...)
*l.logs = append(*l.logs, log)
}

func (l *testLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
log := fmt.Sprintf(msg, data...)
*l.logs = append(*l.logs, log)
}

func (l *testLogger) Error(ctx context.Context, msg string, data ...interface{}) {
log := fmt.Sprintf(msg, data...)
*l.logs = append(*l.logs, log)
}

func (l *testLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
sql, _ := fc()
log := sql
if err != nil {
log += " " + err.Error()
}
*l.logs = append(*l.logs, log)
}

func TestMigrate(t *testing.T) {
allModels := []interface{}{&User{}, &Account{}, &Pet{}, &Company{}, &Toy{}, &Language{}, &Tools{}, &Man{}}
rand.Seed(time.Now().UnixNano())
Expand Down Expand Up @@ -356,6 +400,173 @@ func TestSmartMigrateColumnGaussDB(t *testing.T) {
}
}

func TestMigrateColumnTypeAliases(t *testing.T) {
t.Run("length_specifiers", func(t *testing.T) {
type UserLength struct {
ID uint
Name string `gorm:"type:varchar(100)"`
}

DB.Migrator().DropTable(&UserLength{})

if err := DB.AutoMigrate(&UserLength{}); err != nil {
t.Fatalf("failed to auto migrate, got error: %v", err)
}

columnTypes, err := DB.Migrator().ColumnTypes(&UserLength{})
if err != nil {
t.Fatalf("failed to get column types, got error: %v", err)
}

var nameLength int64
for _, columnType := range columnTypes {
if columnType.Name() == "name" {
nameLength, _ = columnType.Length()
}
}

// Change to varchar without size and migrate again
type UserLength2 struct {
ID uint
Name string `gorm:"type:varchar"`
}

if err := DB.Table("user_lengths").AutoMigrate(&UserLength2{}); err != nil {
t.Fatalf("failed to auto migrate with varchar, got error: %v", err)
}

columnTypes2, err := DB.Table("user_lengths").Migrator().ColumnTypes(&UserLength{})
if err != nil {
t.Fatalf("failed to get column types after migrate, got error: %v", err)
}

for _, columnType := range columnTypes2 {
if columnType.Name() == "name" {
length2, _ := columnType.Length()
// The new logic should prevent altering the type/length if they are considered the same
// Without the new logic, if DatabaseTypeName includes (100), it might alter
if length2 != nameLength {
t.Fatalf("length changed unexpectedly, was %v, now %v", nameLength, length2)
}
}
}
})

t.Run("array_types_postgres", func(t *testing.T) {
if DB.Dialector.Name() != "postgres" {
t.Skip("Array types test is for postgres")
}

// Set up test logger to capture SQL statements
var sqlLogs []string
testLog := &testLogger{logs: &sqlLogs}
oldLogger := DB.Logger
DB.Logger = testLog

defer func() {
DB.Logger = oldLogger
}()

type UserArray struct {
ID uint
Names []string `gorm:"type:varchar[]"`
}

if err := DB.AutoMigrate(&UserArray{}); err != nil {
t.Fatalf("failed to auto migrate with varchar[], got error: %v", err)
}

// Clear logs before migration
sqlLogs = nil

// Now migrate with struct that has varchar[] - should NOT generate ALTER statements
if err := DB.AutoMigrate(&UserArray{}); err != nil {
t.Fatalf("failed to auto migrate with varchar[], got error: %v", err)
}

// Check that no ALTER statements were executed
for _, log := range sqlLogs {
if strings.Contains(strings.ToUpper(log), "ALTER TABLE") {
t.Fatalf("Unexpected ALTER TABLE statement found in logs: %v - this indicates the alias fix is not working", log)
}
}

columnTypes, err := DB.Migrator().ColumnTypes(&UserArray{})
if err != nil {
t.Fatalf("failed to get column types, got error: %v", err)
}

var namesType string
for _, columnType := range columnTypes {
if columnType.Name() == "names" {
namesType = columnType.DatabaseTypeName()
// Should be character varying[] (postgres native) or varchar[]
if !strings.Contains(namesType, "character varying") && !strings.Contains(namesType, "varchar") {
t.Fatalf("expected array type containing 'character varying' or 'varchar', got %v", namesType)
}
}
}

// Clear logs again
sqlLogs = nil

// Migrate again with same struct - should not generate any ALTER statements
if err := DB.AutoMigrate(&UserArray{}); err != nil {
t.Fatalf("failed to auto migrate again, got error: %v", err)
}

// Check again that no ALTER statements were executed
for _, log := range sqlLogs {
if strings.Contains(strings.ToUpper(log), "ALTER TABLE") {
t.Fatalf("Unexpected ALTER TABLE statement found in logs after second migration: %v", log)
}
}

// Clean up
DB.Migrator().DropTable(&UserArray{})
})

t.Run("bidirectional_aliases", func(t *testing.T) {
// This subtest assumes some common aliases; adjust based on database
type UserAlias struct {
ID uint
Data string `gorm:"type:text"`
}

DB.Migrator().DropTable(&UserAlias{})

if err := DB.AutoMigrate(&UserAlias{}); err != nil {
t.Fatalf("failed to auto migrate, got error: %v", err)
}

// Change to varchar and migrate
type UserAlias2 struct {
ID uint
Data string `gorm:"type:varchar"`
}

if err := DB.Table("user_aliases").AutoMigrate(&UserAlias2{}); err != nil {
t.Fatalf("failed to auto migrate with varchar, got error: %v", err)
}

// Should not alter if aliases are properly mapped bidirectionally
columnTypes, err := DB.Table("user_aliases").Migrator().ColumnTypes(&UserAlias{})
if err != nil {
t.Fatalf("failed to get column types, got error: %v", err)
}

for _, columnType := range columnTypes {
if columnType.Name() == "data" {
dbType := columnType.DatabaseTypeName()
// Just check it's a string type
if !strings.Contains(dbType, "text") && !strings.Contains(dbType, "varchar") && !strings.Contains(dbType, "character") {
t.Fatalf("unexpected type %v", dbType)
}
}
}
})
}

func TestMigrateWithColumnComment(t *testing.T) {
type UserWithColumnComment struct {
gorm.Model
Expand Down
20 changes: 10 additions & 10 deletions tests/upsert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ func TestFindOrInitialize(t *testing.T) {
}

func TestFindOrCreate(t *testing.T) {
var user1, user2, user3, user4, user5, user6, user7, user8 User
var user1, user2, user3, user4, user5, user6, user7, user8, user9 User
if err := DB.Where(&User{Name: "find or create", Age: 33}).FirstOrCreate(&user1).Error; err != nil {
t.Errorf("no error should happen when FirstOrInit, but got %v", err)
}
Expand Down Expand Up @@ -327,27 +327,27 @@ func TestFindOrCreate(t *testing.T) {
t.Errorf("UpdateAt should be changed when update values with assign")
}

DB.Where(&User{Name: "find or create 4"}).Assign(User{Age: 44}).FirstOrCreate(&user4)
if user4.Name != "find or create 4" || user4.ID == 0 || user4.Age != 44 {
DB.Where(&User{Name: "find or create 4"}).Assign(User{Age: 44}).FirstOrCreate(&user5)
if user5.Name != "find or create 4" || user5.ID == 0 || user5.Age != 44 {
t.Errorf("user should be created with search value and assigned attrs")
}

DB.Where(&User{Name: "find or create"}).Attrs("age", 44).FirstOrInit(&user5)
if user5.Name != "find or create" || user5.ID == 0 || user5.Age != 33 {
DB.Where(&User{Name: "find or create"}).Attrs("age", 44).FirstOrInit(&user6)
if user6.Name != "find or create" || user6.ID == 0 || user6.Age != 33 {
t.Errorf("user should be found and not initialized by Attrs")
}

DB.Where(&User{Name: "find or create"}).Assign(User{Age: 44}).FirstOrCreate(&user6)
if user6.Name != "find or create" || user6.ID == 0 || user6.Age != 44 {
DB.Where(&User{Name: "find or create"}).Assign(User{Age: 44}).FirstOrCreate(&user7)
if user7.Name != "find or create" || user7.ID == 0 || user7.Age != 44 {
t.Errorf("user should be found and updated with assigned attrs")
}

DB.Where(&User{Name: "find or create"}).Find(&user7)
if user7.Name != "find or create" || user7.ID == 0 || user7.Age != 44 {
DB.Where(&User{Name: "find or create"}).Find(&user8)
if user8.Name != "find or create" || user8.ID == 0 || user8.Age != 44 {
t.Errorf("user should be found and updated with assigned attrs")
}

DB.Where(&User{Name: "find or create embedded struct"}).Assign(User{Age: 44, Account: Account{Number: "1231231231"}, Pets: []*Pet{{Name: "first_or_create_pet1"}, {Name: "first_or_create_pet2"}}}).FirstOrCreate(&user8)
DB.Where(&User{Name: "find or create embedded struct"}).Assign(User{Age: 44, Account: Account{Number: "1231231231"}, Pets: []*Pet{{Name: "first_or_create_pet1"}, {Name: "first_or_create_pet2"}}}).FirstOrCreate(&user9)
if err := DB.Where("name = ?", "first_or_create_pet1").First(&Pet{}).Error; err != nil {
t.Errorf("has many association should be saved")
}
Expand Down
Loading