Repository: go-gorm/gorm
Pull request #5269 (2022-04-18 04:32:47)

index: add composite id
- [x] Do only one thing
- [x] Non breaking API changes
- [x] Tested

### What did this pull request do?
In the issue #3973, if a struct with any composite indexes, these indexes will be invalid when the struct is embedded more than once. Because a composite index **must** specify its name, embedding more than once results in the duplicated name in db. Also if the name of index is specified manually, `NamingStrategy` will not be applied. Take the example of #3973:
```go
type Bar0 struct {
  Foo
}
type Bar1 struct {
  Foo
}
```
Both `Bar0` and `Bar1` will try to create an index of the same name `my_idx`. In this pull request, it provides a new field of index tag which is named to `composite`. It means the id of composite index. All fields which have the same composite id of the struct are put together to the same index, just like the original rule. But the improvement is it lets the most derived/embedding struct generates the name of index by `NamingStrategy`. For example:
```go
type Foo struct {
  IndexA int `gorm:"index:,unique,composite:myname"`
  IndexB int `gorm:"index:,unique,composite:myname"`
}
```
If the table `Foo` is created, the name of composite index will be `idx_foo_myname`.
```go
type Bar0 struct {
  Foo
}
type Bar1 struct {
  Foo
}
```
Respectively, the name of composite index are `idx_bar0_myname` and `idx_bar1_myname`.

`composite` only works if not specify the name of index, so it keeps the original API from being broken.


Patch:
diff --git a/migrator/migrator.go b/migrator/migrator.go
index a50bb3f..93f4c5d 100644
--- a/migrator/migrator.go
+++ b/migrator/migrator.go
@@ -99,7 +99,10 @@ func (m Migrator) AutoMigrate(values ...interface{}) error {
 			}
 		} else {
 			if err := m.RunWithValue(value, func(stmt *gorm.Statement) (errr error) {
-				columnTypes, _ := m.DB.Migrator().ColumnTypes(value)
+				columnTypes, err := m.DB.Migrator().ColumnTypes(value)
+				if err != nil {
+					return err
+				}
 
 				for _, dbName := range stmt.Schema.DBNames {
 					field := stmt.Schema.FieldsByDBName[dbName]
diff --git a/schema/field.go b/schema/field.go
index fd8b2e6..d6df659 100644
--- a/schema/field.go
+++ b/schema/field.go
@@ -528,6 +528,9 @@ func (field *Field) setupValuerAndSetter() {
 			reflectValType := reflectV.Type()
 
 			if reflectValType.AssignableTo(field.FieldType) {
+				if reflectV.Kind() == reflect.Ptr && reflectV.Elem().Kind() == reflect.Ptr {
+					reflectV = reflect.Indirect(reflectV)
+				}
 				field.ReflectValueOf(ctx, value).Set(reflectV)
 				return
 			} else if reflectValType.ConvertibleTo(field.FieldType) {
diff --git a/schema/index.go b/schema/index.go
index 16d096b..5003c74 100644
--- a/schema/index.go
+++ b/schema/index.go
@@ -1,6 +1,7 @@
 package schema
 
 import (
+	"fmt"
 	"sort"
 	"strconv"
 	"strings"
@@ -31,7 +32,12 @@ func (schema *Schema) ParseIndexes() map[string]Index {
 
 	for _, field := range schema.Fields {
 		if field.TagSettings["INDEX"] != "" || field.TagSettings["UNIQUEINDEX"] != "" {
-			for _, index := range parseFieldIndexes(field) {
+			fieldIndexes, err := parseFieldIndexes(field)
+			if err != nil {
+				schema.err = err
+				break
+			}
+			for _, index := range fieldIndexes {
 				idx := indexes[index.Name]
 				idx.Name = index.Name
 				if idx.Class == "" {
@@ -82,7 +88,7 @@ func (schema *Schema) LookIndex(name string) *Index {
 	return nil
 }
 
-func parseFieldIndexes(field *Field) (indexes []Index) {
+func parseFieldIndexes(field *Field) (indexes []Index, err error) {
 	for _, value := range strings.Split(field.Tag.Get("gorm"), ";") {
 		if value != "" {
 			v := strings.Split(value, ":")
@@ -106,7 +112,20 @@ func parseFieldIndexes(field *Field) (indexes []Index) {
 				}
 
 				if name == "" {
-					name = field.Schema.namer.IndexName(field.Schema.Table, field.Name)
+					subName := field.Name
+					const key = "COMPOSITE"
+					if composite, found := settings[key]; found {
+						if len(composite) == 0 || composite == key {
+							err = fmt.Errorf(
+								"The composite tag of %s.%s cannot be empty",
+								field.Schema.Name,
+								field.Name)
+							return
+						}
+						subName = composite
+					}
+					name = field.Schema.namer.IndexName(
+						field.Schema.Table, subName)
 				}
 
 				if (k == "UNIQUEINDEX") || settings["UNIQUE"] != "" {
@@ -138,5 +157,6 @@ func parseFieldIndexes(field *Field) (indexes []Index) {
 		}
 	}
 
+	err = nil
 	return
 }
diff --git a/statement.go b/statement.go
index 9fcee09..ed3e871 100644
--- a/statement.go
+++ b/statement.go
@@ -312,6 +312,10 @@ func (stmt *Statement) BuildCondition(query interface{}, args ...interface{}) []
 		case clause.Expression:
 			conds = append(conds, v)
 		case *DB:
+			for _, scope := range v.Statement.scopes {
+				v = scope(v)
+			}
+
 			if cs, ok := v.Statement.Clauses["WHERE"]; ok {
 				if where, ok := cs.Expression.(clause.Where); ok {
 					if len(where.Exprs) == 1 {
@@ -605,10 +609,10 @@ func (stmt *Statement) Changed(fields ...string) bool {
 	changed := func(field *schema.Field) bool {
 		fieldValue, _ := field.ValueOf(stmt.Context, modelValue)
 		if v, ok := selectColumns[field.DBName]; (ok && v) || (!ok && !restricted) {
-			if v, ok := stmt.Dest.(map[string]interface{}); ok {
-				if fv, ok := v[field.Name]; ok {
+			if mv, mok := stmt.Dest.(map[string]interface{}); mok {
+				if fv, ok := mv[field.Name]; ok {
 					return !utils.AssertEqual(fv, fieldValue)
-				} else if fv, ok := v[field.DBName]; ok {
+				} else if fv, ok := mv[field.DBName]; ok {
 					return !utils.AssertEqual(fv, fieldValue)
 				}
 			} else {
@@ -618,6 +622,9 @@ func (stmt *Statement) Changed(fields ...string) bool {
 				}
 
 				changedValue, zero := field.ValueOf(stmt.Context, destValue)
+				if v {
+					return !utils.AssertEqual(changedValue, fieldValue)
+				}
 				return !zero && !utils.AssertEqual(changedValue, fieldValue)
 			}
 		}


Test patch:
diff --git a/schema/index_test.go b/schema/index_test.go
index 3c4582b..1fe31cc 100644
--- a/schema/index_test.go
+++ b/schema/index_test.go
@@ -19,6 +19,36 @@ type UserIndex struct {
 	OID          int64  `gorm:"index:idx_id;index:idx_oid,unique"`
 	MemberNumber string `gorm:"index:idx_id,priority:1"`
 	Name7        string `gorm:"index:type"`
+
+	// Composite Index: Flattened structure.
+	Data0A string `gorm:"index:,composite:comp_id0"`
+	Data0B string `gorm:"index:,composite:comp_id0"`
+
+	// Composite Index: Nested structure.
+	Data1A string `gorm:"index:,composite:comp_id1"`
+	CompIdxLevel1C
+
+	// Composite Index: Unique and priority.
+	Data2A string `gorm:"index:,unique,composite:comp_id2,priority:2"`
+	CompIdxLevel2C
+}
+
+type CompIdxLevel1C struct {
+	CompIdxLevel1B
+	Data1C string `gorm:"index:,composite:comp_id1"`
+}
+
+type CompIdxLevel1B struct {
+	Data1B string `gorm:"index:,composite:comp_id1"`
+}
+
+type CompIdxLevel2C struct {
+	CompIdxLevel2B
+	Data2C string `gorm:"index:,unique,composite:comp_id2,priority:1"`
+}
+
+type CompIdxLevel2B struct {
+	Data2B string `gorm:"index:,unique,composite:comp_id2,priority:3"`
 }
 
 func TestParseIndex(t *testing.T) {
@@ -84,6 +114,36 @@ func TestParseIndex(t *testing.T) {
 			Type:   "",
 			Fields: []schema.IndexOption{{Field: &schema.Field{Name: "Name7"}}},
 		},
+		"idx_user_indices_comp_id0": {
+			Name: "idx_user_indices_comp_id0",
+			Type: "",
+			Fields: []schema.IndexOption{{
+				Field: &schema.Field{Name: "Data0A"},
+			}, {
+				Field: &schema.Field{Name: "Data0B"},
+			}},
+		},
+		"idx_user_indices_comp_id1": {
+			Name: "idx_user_indices_comp_id1",
+			Fields: []schema.IndexOption{{
+				Field: &schema.Field{Name: "Data1A"},
+			}, {
+				Field: &schema.Field{Name: "Data1B"},
+			}, {
+				Field: &schema.Field{Name: "Data1C"},
+			}},
+		},
+		"idx_user_indices_comp_id2": {
+			Name:  "idx_user_indices_comp_id2",
+			Class: "UNIQUE",
+			Fields: []schema.IndexOption{{
+				Field: &schema.Field{Name: "Data2C"},
+			}, {
+				Field: &schema.Field{Name: "Data2A"},
+			}, {
+				Field: &schema.Field{Name: "Data2B"},
+			}},
+		},
 	}
 
 	indices := user.ParseIndexes()
diff --git a/tests/hooks_test.go b/tests/hooks_test.go
index 0e6ab2f..20e8dc1 100644
--- a/tests/hooks_test.go
+++ b/tests/hooks_test.go
@@ -375,13 +375,19 @@ func TestSetColumn(t *testing.T) {
 		t.Errorf("invalid data after update, got %+v", product)
 	}
 
+	// Code changed, price should changed
+	DB.Model(&product).Select("Name", "Code", "Price").Updates(Product3{Name: "Product New4", Code: ""})
+	if product.Name != "Product New4" || product.Price != 320 || product.Code != "" {
+		t.Errorf("invalid data after update, got %+v", product)
+	}
+
 	DB.Model(&product).UpdateColumns(Product3{Code: "L1215"})
-	if product.Price != 270 || product.Code != "L1215" {
+	if product.Price != 320 || product.Code != "L1215" {
 		t.Errorf("invalid data after update, got %+v", product)
 	}
 
 	DB.Model(&product).Session(&gorm.Session{SkipHooks: true}).Updates(Product3{Code: "L1216"})
-	if product.Price != 270 || product.Code != "L1216" {
+	if product.Price != 320 || product.Code != "L1216" {
 		t.Errorf("invalid data after update, got %+v", product)
 	}
 
diff --git a/tests/migrate_test.go b/tests/migrate_test.go
index f72c4c0..d6a6c4d 100644
--- a/tests/migrate_test.go
+++ b/tests/migrate_test.go
@@ -574,3 +574,65 @@ func TestMigrateColumnOrder(t *testing.T) {
 		}
 	}
 }
+
+// https://github.com/go-gorm/gorm/issues/5047
+func TestMigrateSerialColumn(t *testing.T) {
+	if DB.Dialector.Name() != "postgres" {
+		return
+	}
+
+	type Event struct {
+		ID  uint `gorm:"primarykey"`
+		UID uint32
+	}
+
+	type Event1 struct {
+		ID  uint   `gorm:"primarykey"`
+		UID uint32 `gorm:"not null;autoIncrement"`
+	}
+
+	type Event2 struct {
+		ID  uint   `gorm:"primarykey"`
+		UID uint16 `gorm:"not null;autoIncrement"`
+	}
+
+	var err error
+	err = DB.Migrator().DropTable(&Event{})
+	if err != nil {
+		t.Errorf("DropTable err:%v", err)
+	}
+
+	// create sequence
+	err = DB.Table("events").AutoMigrate(&Event1{})
+	if err != nil {
+		t.Errorf("AutoMigrate err:%v", err)
+	}
+
+	// delete sequence
+	err = DB.Table("events").AutoMigrate(&Event{})
+	if err != nil {
+		t.Errorf("AutoMigrate err:%v", err)
+	}
+
+	// update sequence
+	err = DB.Table("events").AutoMigrate(&Event1{})
+	if err != nil {
+		t.Errorf("AutoMigrate err:%v", err)
+	}
+	err = DB.Table("events").AutoMigrate(&Event2{})
+	if err != nil {
+		t.Errorf("AutoMigrate err:%v", err)
+	}
+
+	DB.Table("events").Save(&Event2{})
+	DB.Table("events").Save(&Event2{})
+	DB.Table("events").Save(&Event2{})
+
+	events := make([]*Event, 0)
+	DB.Table("events").Find(&events)
+
+	AssertEqual(t, 3, len(events))
+	for _, v := range events {
+		AssertEqual(t, v.ID, v.UID)
+	}
+}
diff --git a/tests/sql_builder_test.go b/tests/sql_builder_test.go
index a763027..a9b920d 100644
--- a/tests/sql_builder_test.go
+++ b/tests/sql_builder_test.go
@@ -243,6 +243,21 @@ func TestGroupConditions(t *testing.T) {
 	if !strings.HasSuffix(result, expects) {
 		t.Errorf("expects: %v, got %v", expects, result)
 	}
+
+	stmt2 := dryRunDB.Where(
+		DB.Scopes(NameIn1And2),
+	).Or(
+		DB.Where("pizza = ?", "hawaiian").Where("size = ?", "xlarge"),
+	).Find(&Pizza{}).Statement
+
+	execStmt2 := dryRunDB.Exec(`WHERE name in ? OR (pizza = ? AND size = ?)`, []string{"ScopeUser1", "ScopeUser2"}, "hawaiian", "xlarge").Statement
+
+	result2 := DB.Dialector.Explain(stmt2.SQL.String(), stmt2.Vars...)
+	expects2 := DB.Dialector.Explain(execStmt2.SQL.String(), execStmt2.Vars...)
+
+	if !strings.HasSuffix(result2, expects2) {
+		t.Errorf("expects: %v, got %v", expects2, result2)
+	}
 }
 
 func TestCombineStringConditions(t *testing.T) {