Repository: go-gorm/gorm
Pull request #4888 (2021-12-01 09:19:44)

fix type alias AutoMigrate bug（Add Test Case）
<!--
Make sure these boxes checked before submitting your pull request.

For significant changes, please open an issue to make an agreement on an implementation design/plan first before starting it.
-->

- [x] Do only one thing
- [x] Non breaking API changes
- [x] Tested

### What did this pull request do?

At some times, we need to define alias types for native data types, such as easy reuse for some common interfaces.
but, when you denfine alias types, the AutoMigrate will crash!

<!--
provide a general description of the code changes in your pull request
-->

### User Case Description

<!-- Your use case -->
eg: 
1. defing `IDer` interface.
2. define `int64` alias type as `ID`.
3. define a `Test` struct contian column `ID`.
4. call `AutoMigrate`, and gorm return error `invalid embedded struct for Test's field ID, should be struct, but got main.ID`

bellow is the full demo code.

```go
package main

import (
	"log"

	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

type IDer interface{ GetID() int64 }

// ID will add some method to implement some interface eg: GetID
type ID int64

func (z ID) GetID() int64 { return int64(z) }

type Test struct {
	ID
	Code string `gorm:"size:50"`
	Name string `gorm:"size:50"`
}

func main() {
	db, err := gorm.Open(postgres.New(postgres.Config{
		DSN:                  `dsn`,
		PreferSimpleProtocol: false,
	}), &gorm.Config{
		Logger:                 logger.Default.LogMode(logger.Info),
		SkipDefaultTransaction: true,
	})
	if err != nil {
		log.Fatal(err)
	}

	if err = db.AutoMigrate(&Test{}); err != nil {
		// invalid embedded struct for Test's field ID, should be struct, but got main.ID
		log.Fatal(err)
	}
}
```


jinzhu: Can you please create a test for this?
piyongcai: > Can you please create a test for this?

Test case Added.
`gorm/schema/field_test.go`
piyongcai: > Can you please create a test for this?

but I don't understand the error of `reviewdog / runner / golangci-lint (pull_request) `. 
and I don't know how to resolve it.

Patch:
diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml
index abfd57f..95b6fb0 100644
--- a/.github/workflows/reviewdog.yml
+++ b/.github/workflows/reviewdog.yml
@@ -9,5 +9,3 @@ jobs:
         uses: actions/checkout@v2
       - name: golangci-lint
         uses: reviewdog/action-golangci-lint@v2
-        with:
-          golangci_lint_flags: '-E cyclop,unconvert,misspell,unparam,ineffassign,gocritic,prealloc,exportloopref,gosec'
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..16903ed
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,11 @@
+linters:
+  enable:
+    - cyclop
+    - exportloopref
+    - gocritic
+    - gosec
+    - ineffassign
+    - misspell
+    - prealloc
+    - unconvert
+    - unparam
diff --git a/callbacks/create.go b/callbacks/create.go
index 9dc5b8b..2911312 100644
--- a/callbacks/create.go
+++ b/callbacks/create.go
@@ -57,7 +57,7 @@ func Create(config *Config) func(db *gorm.DB) {
 			}
 		}
 
-		if db.Statement.SQL.String() == "" {
+		if db.Statement.SQL.Len() == 0 {
 			db.Statement.SQL.Grow(180)
 			db.Statement.AddClauseIfNotExists(clause.Insert{})
 			db.Statement.AddClause(ConvertToCreateValues(db.Statement))
diff --git a/callbacks/delete.go b/callbacks/delete.go
index b05a9d0..7f1e09c 100644
--- a/callbacks/delete.go
+++ b/callbacks/delete.go
@@ -118,13 +118,7 @@ func Delete(config *Config) func(db *gorm.DB) {
 			return
 		}
 
-		if db.Statement.Schema != nil && !db.Statement.Unscoped {
-			for _, c := range db.Statement.Schema.DeleteClauses {
-				db.Statement.AddClause(c)
-			}
-		}
-
-		if db.Statement.SQL.String() == "" {
+		if db.Statement.SQL.Len() == 0 {
 			db.Statement.SQL.Grow(100)
 			db.Statement.AddClauseIfNotExists(clause.Delete{})
 
@@ -147,6 +141,15 @@ func Delete(config *Config) func(db *gorm.DB) {
 			}
 
 			db.Statement.AddClauseIfNotExists(clause.From{})
+		}
+
+		if db.Statement.Schema != nil {
+			for _, c := range db.Statement.Schema.DeleteClauses {
+				db.Statement.AddClause(c)
+			}
+		}
+
+		if db.Statement.SQL.Len() == 0 {
 			db.Statement.Build(db.Statement.BuildClauses...)
 		}
 
diff --git a/callbacks/query.go b/callbacks/query.go
index 2f98a4b..efb0860 100644
--- a/callbacks/query.go
+++ b/callbacks/query.go
@@ -33,7 +33,7 @@ func BuildQuerySQL(db *gorm.DB) {
 		}
 	}
 
-	if db.Statement.SQL.String() == "" {
+	if db.Statement.SQL.Len() == 0 {
 		db.Statement.SQL.Grow(100)
 		clauseSelect := clause.Select{Distinct: db.Statement.Distinct}
 
diff --git a/callbacks/update.go b/callbacks/update.go
index fa7640d..b3eaaf1 100644
--- a/callbacks/update.go
+++ b/callbacks/update.go
@@ -59,13 +59,7 @@ func Update(config *Config) func(db *gorm.DB) {
 			return
 		}
 
-		if db.Statement.Schema != nil && !db.Statement.Unscoped {
-			for _, c := range db.Statement.Schema.UpdateClauses {
-				db.Statement.AddClause(c)
-			}
-		}
-
-		if db.Statement.SQL.String() == "" {
+		if db.Statement.SQL.Len() == 0 {
 			db.Statement.SQL.Grow(180)
 			db.Statement.AddClauseIfNotExists(clause.Update{})
 			if set := ConvertToAssignments(db.Statement); len(set) != 0 {
@@ -73,6 +67,16 @@ func Update(config *Config) func(db *gorm.DB) {
 			} else if _, ok := db.Statement.Clauses["SET"]; !ok {
 				return
 			}
+
+		}
+
+		if db.Statement.Schema != nil {
+			for _, c := range db.Statement.Schema.UpdateClauses {
+				db.Statement.AddClause(c)
+			}
+		}
+
+		if db.Statement.SQL.Len() == 0 {
 			db.Statement.Build(db.Statement.BuildClauses...)
 		}
 
diff --git a/schema/field.go b/schema/field.go
index f3189c7..c6c89cc 100644
--- a/schema/field.go
+++ b/schema/field.go
@@ -347,7 +347,9 @@ func (schema *Schema) ParseField(fieldStruct reflect.StructField) *Field {
 	}
 
 	if _, ok := field.TagSettings["EMBEDDED"]; ok || (fieldStruct.Anonymous && !isValuer && (field.Creatable || field.Updatable || field.Readable)) {
-		if reflect.Indirect(fieldValue).Kind() == reflect.Struct {
+		kind := reflect.Indirect(fieldValue).Kind()
+		switch kind {
+		case reflect.Struct:
 			var err error
 			field.Creatable = false
 			field.Updatable = false
@@ -396,7 +398,8 @@ func (schema *Schema) ParseField(fieldStruct reflect.StructField) *Field {
 					ef.TagSettings[k] = v
 				}
 			}
-		} else {
+		case reflect.Invalid, reflect.Uintptr, reflect.Array, reflect.Chan,          reflect.Func,      reflect.Interface,
+		     reflect.Map,     reflect.Ptr,     reflect.Slice, reflect.UnsafePointer, reflect.Complex64, reflect.Complex128:
 			schema.err = fmt.Errorf("invalid embedded struct for %s's field %s, should be struct, but got %v", field.Schema.Name, field.Name, field.FieldType)
 		}
 	}
diff --git a/soft_delete.go b/soft_delete.go
index 11c4faf..4e236fc 100644
--- a/soft_delete.go
+++ b/soft_delete.go
@@ -103,7 +103,7 @@ func (sd SoftDeleteUpdateClause) MergeClause(*clause.Clause) {
 }
 
 func (sd SoftDeleteUpdateClause) ModifyStatement(stmt *Statement) {
-	if stmt.SQL.String() == "" {
+	if stmt.SQL.Len() == 0 && !stmt.Statement.Unscoped {
 		if _, ok := stmt.Clauses["WHERE"]; stmt.DB.AllowGlobalUpdate || ok {
 			SoftDeleteQueryClause(sd).ModifyStatement(stmt)
 		}
@@ -129,7 +129,7 @@ func (sd SoftDeleteDeleteClause) MergeClause(*clause.Clause) {
 }
 
 func (sd SoftDeleteDeleteClause) ModifyStatement(stmt *Statement) {
-	if stmt.SQL.String() == "" {
+	if stmt.SQL.Len() == 0 && !stmt.Statement.Unscoped {
 		curTime := stmt.DB.NowFunc()
 		stmt.AddClause(clause.Set{{Column: clause.Column{Name: sd.Field.DBName}, Value: curTime}})
 		stmt.SetColumn(sd.Field.DBName, curTime, true)


Test patch:
diff --git a/schema/field_test.go b/schema/field_test.go
index 4be3e5a..8768a4c 100644
--- a/schema/field_test.go
+++ b/schema/field_test.go
@@ -244,7 +244,7 @@ func TestParseFieldWithPermission(t *testing.T) {
 		t.Fatalf("Failed to parse user with permission, got error %v", err)
 	}
 
-	fields := []schema.Field{
+	fields := []*schema.Field{
 		{Name: "ID", DBName: "id", BindNames: []string{"ID"}, DataType: schema.Uint, PrimaryKey: true, Size: 64, Creatable: true, Updatable: true, Readable: true, HasDefaultValue: true, AutoIncrement: true},
 		{Name: "Name", DBName: "", BindNames: []string{"Name"}, DataType: "", Tag: `gorm:"-"`, Creatable: false, Updatable: false, Readable: false},
 		{Name: "Name2", DBName: "name2", BindNames: []string{"Name2"}, DataType: schema.String, Tag: `gorm:"->"`, Creatable: false, Updatable: false, Readable: true},
@@ -257,6 +257,68 @@ func TestParseFieldWithPermission(t *testing.T) {
 	}
 
 	for _, f := range fields {
-		checkSchemaField(t, user, &f, func(f *schema.Field) {})
+		checkSchemaField(t, user, f, func(f *schema.Field) {})
 	}
 }
+
+type ID int64
+type INT int
+type INT8 int8
+type INT16 int16
+type INT32 int32
+type INT64 int64
+type UINT uint
+type UINT8 uint8
+type UINT16 uint16
+type UINT32 uint32
+type UINT64 uint64
+type FLOAT32 float32
+type FLOAT64 float64
+type BOOL bool
+type STRING string
+type TypeAlias struct {
+	ID
+	INT     `gorm:"column:fint"`
+	INT8    `gorm:"column:fint8"`
+	INT16   `gorm:"column:fint16"`
+	INT32   `gorm:"column:fint32"`
+	INT64   `gorm:"column:fint64"`
+	UINT    `gorm:"column:fuint"`
+	UINT8   `gorm:"column:fuint8"`
+	UINT16  `gorm:"column:fuint16"`
+	UINT32  `gorm:"column:fuint32"`
+	UINT64  `gorm:"column:fuint64"`
+	FLOAT32 `gorm:"column:ffloat32"`
+	FLOAT64 `gorm:"column:ffloat64"`
+	BOOL    `gorm:"column:fbool"`
+	STRING  `gorm:"column:fstring"`
+}
+
+func TestTypeAliasField(t *testing.T){
+	alias, err := schema.Parse(&TypeAlias{}, &sync.Map{}, schema.NamingStrategy{})
+	if err != nil {
+		t.Fatalf("Failed to parse TypeAlias with permission, got error %v", err)
+	}
+
+	fields := []*schema.Field{
+		{Name: "ID",      DBName: "id",       BindNames: []string{"ID"},      DataType: schema.Int   , Creatable: true, Updatable: true, Readable: true, Size: 64, PrimaryKey: true, HasDefaultValue: true, AutoIncrement: true },
+		{Name: "INT",     DBName: "fint",     BindNames: []string{"INT"},     DataType: schema.Int   , Creatable: true, Updatable: true, Readable: true, Size: 64, Tag: `gorm:"column:fint"`},
+		{Name: "INT8",    DBName: "fint8",    BindNames: []string{"INT8"},    DataType: schema.Int   , Creatable: true, Updatable: true, Readable: true, Size: 8,  Tag: `gorm:"column:fint8"`},
+		{Name: "INT16",   DBName: "fint16",   BindNames: []string{"INT16"},   DataType: schema.Int   , Creatable: true, Updatable: true, Readable: true, Size: 16, Tag: `gorm:"column:fint16"`},
+		{Name: "INT32",   DBName: "fint32",   BindNames: []string{"INT32"},   DataType: schema.Int   , Creatable: true, Updatable: true, Readable: true, Size: 32, Tag: `gorm:"column:fint32"`},
+		{Name: "INT64",   DBName: "fint64",   BindNames: []string{"INT64"},   DataType: schema.Int   , Creatable: true, Updatable: true, Readable: true, Size: 64, Tag: `gorm:"column:fint64"`},
+		{Name: "UINT",    DBName: "fuint",    BindNames: []string{"UINT"},    DataType: schema.Uint  , Creatable: true, Updatable: true, Readable: true, Size: 64, Tag: `gorm:"column:fuint"`},
+		{Name: "UINT8",   DBName: "fuint8",   BindNames: []string{"UINT8"},   DataType: schema.Uint  , Creatable: true, Updatable: true, Readable: true, Size: 8,  Tag: `gorm:"column:fuint8"`},
+		{Name: "UINT16",  DBName: "fuint16",  BindNames: []string{"UINT16"},  DataType: schema.Uint  , Creatable: true, Updatable: true, Readable: true, Size: 16, Tag: `gorm:"column:fuint16"`},
+		{Name: "UINT32",  DBName: "fuint32",  BindNames: []string{"UINT32"},  DataType: schema.Uint  , Creatable: true, Updatable: true, Readable: true, Size: 32, Tag: `gorm:"column:fuint32"`},
+		{Name: "UINT64",  DBName: "fuint64",  BindNames: []string{"UINT64"},  DataType: schema.Uint  , Creatable: true, Updatable: true, Readable: true, Size: 64, Tag: `gorm:"column:fuint64"`},
+		{Name: "FLOAT32", DBName: "ffloat32", BindNames: []string{"FLOAT32"}, DataType: schema.Float , Creatable: true, Updatable: true, Readable: true, Size: 32, Tag: `gorm:"column:ffloat32"`},
+		{Name: "FLOAT64", DBName: "ffloat64", BindNames: []string{"FLOAT64"}, DataType: schema.Float , Creatable: true, Updatable: true, Readable: true, Size: 64, Tag: `gorm:"column:ffloat64"`},
+		{Name: "BOOL",    DBName: "fbool",    BindNames: []string{"BOOL"},    DataType: schema.Bool  , Creatable: true, Updatable: true, Readable: true, Tag: `gorm:"column:fbool"`},
+		{Name: "STRING",  DBName: "fstring",  BindNames: []string{"STRING"},  DataType: schema.String, Creatable: true, Updatable: true, Readable: true, Tag: `gorm:"column:fstring"`},
+	}
+
+	for _, f := range fields {
+		checkSchemaField(t, alias, f, func(f *schema.Field) {})
+	}
+}
\ No newline at end of file
diff --git a/tests/update_test.go b/tests/update_test.go
index 14ed982..abe520d 100644
--- a/tests/update_test.go
+++ b/tests/update_test.go
@@ -645,7 +645,13 @@ func TestSave(t *testing.T) {
 
 	dryDB := DB.Session(&gorm.Session{DryRun: true})
 	stmt := dryDB.Save(&user).Statement
-	if !regexp.MustCompile("WHERE .id. = [^ ]+$").MatchString(stmt.SQL.String()) {
+	if !regexp.MustCompile(`.id. = .* AND .users.\..deleted_at. IS NULL`).MatchString(stmt.SQL.String()) {
+		t.Fatalf("invalid updating SQL, got %v", stmt.SQL.String())
+	}
+
+	dryDB = DB.Session(&gorm.Session{DryRun: true})
+	stmt = dryDB.Unscoped().Save(&user).Statement
+	if !regexp.MustCompile(`WHERE .id. = [^ ]+$`).MatchString(stmt.SQL.String()) {
 		t.Fatalf("invalid updating SQL, got %v", stmt.SQL.String())
 	}