From 46bce170cae701615e2b2f8b2448b54524be9648 Mon Sep 17 00:00:00 2001 From: Cr <631807682@qq.com> Date: Mon, 4 Jul 2022 16:42:27 +0800 Subject: [PATCH 01/36] test: pg array type (#5480) --- tests/migrate_test.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/migrate_test.go b/tests/migrate_test.go index 3d6a78589..0b5bc5ebd 100644 --- a/tests/migrate_test.go +++ b/tests/migrate_test.go @@ -923,3 +923,39 @@ func TestDifferentTypeWithoutDeclaredLength(t *testing.T) { AssertEqual(t, "text", strings.ToLower(ct.DatabaseTypeName())) } + +func TestMigrateArrayTypeModel(t *testing.T) { + if DB.Dialector.Name() != "postgres" { + return + } + + type ArrayTypeModel struct { + ID uint + Number string `gorm:"type:varchar(51);NOT NULL"` + TextArray []string `gorm:"type:text[];NOT NULL"` + NestedTextArray [][]string `gorm:"type:text[][]"` + NestedIntArray [][]int64 `gorm:"type:integer[3][3]"` + } + + var err error + DB.Migrator().DropTable(&ArrayTypeModel{}) + + err = DB.AutoMigrate(&ArrayTypeModel{}) + AssertEqual(t, nil, err) + + ct, err := findColumnType(&ArrayTypeModel{}, "number") + AssertEqual(t, nil, err) + AssertEqual(t, "varchar", ct.DatabaseTypeName()) + + ct, err = findColumnType(&ArrayTypeModel{}, "text_array") + AssertEqual(t, nil, err) + AssertEqual(t, "text[]", ct.DatabaseTypeName()) + + ct, err = findColumnType(&ArrayTypeModel{}, "nested_text_array") + AssertEqual(t, nil, err) + AssertEqual(t, "text[]", ct.DatabaseTypeName()) + + ct, err = findColumnType(&ArrayTypeModel{}, "nested_int_array") + AssertEqual(t, nil, err) + AssertEqual(t, "integer[]", ct.DatabaseTypeName()) +} From fe01e1b9f43070e3814817b4b762dfd08a3ced30 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Thu, 7 Jul 2022 14:43:33 +0800 Subject: [PATCH 02/36] Fix Model with slice data --- callbacks/update.go | 2 +- tests/go.mod | 12 +++++++----- tests/update_test.go | 8 ++++++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/callbacks/update.go b/callbacks/update.go index 01f40509e..42ffe2f69 100644 --- a/callbacks/update.go +++ b/callbacks/update.go @@ -172,7 +172,7 @@ func ConvertToAssignments(stmt *gorm.Statement) (set clause.Set) { } } - stmt.AddClause(clause.Where{Exprs: []clause.Expression{clause.Or(primaryKeyExprs...)}}) + stmt.AddClause(clause.Where{Exprs: []clause.Expression{clause.And(clause.Or(primaryKeyExprs...))}}) } case reflect.Struct: for _, field := range stmt.Schema.PrimaryFields { diff --git a/tests/go.mod b/tests/go.mod index bd668420b..f3e9d2602 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -3,16 +3,18 @@ module gorm.io/gorm/tests go 1.14 require ( + github.com/denisenkom/go-mssqldb v0.12.2 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/google/uuid v1.3.0 github.com/jinzhu/now v1.1.5 - github.com/lib/pq v1.10.5 - golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect + github.com/lib/pq v1.10.6 + github.com/mattn/go-sqlite3 v1.14.14 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect gorm.io/driver/mysql v1.3.4 - gorm.io/driver/postgres v1.3.5 - gorm.io/driver/sqlite v1.3.2 + gorm.io/driver/postgres v1.3.8 + gorm.io/driver/sqlite v1.3.6 gorm.io/driver/sqlserver v1.3.2 - gorm.io/gorm v1.23.4 + gorm.io/gorm v1.23.7 ) replace gorm.io/gorm => ../ diff --git a/tests/update_test.go b/tests/update_test.go index 41ea5d27b..0fc89a93a 100644 --- a/tests/update_test.go +++ b/tests/update_test.go @@ -122,6 +122,14 @@ func TestUpdate(t *testing.T) { } else { CheckUser(t, result4, *user) } + + if rowsAffected := DB.Model([]User{result4}).Where("age > 0").Update("name", "jinzhu").RowsAffected; rowsAffected != 1 { + t.Errorf("should only update one record, but got %v", rowsAffected) + } + + if rowsAffected := DB.Model(users).Where("age > 0").Update("name", "jinzhu").RowsAffected; rowsAffected != 3 { + t.Errorf("should only update one record, but got %v", rowsAffected) + } } func TestUpdates(t *testing.T) { From 9fd73ae4f1f638e4c49ae4e6fab8beb9863adabc Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Thu, 7 Jul 2022 15:06:48 +0800 Subject: [PATCH 03/36] Revert "use callback to handle transaction" This reverts commit 93f28bc116526ba4decdd969a7b2b0b245ad70f1. --- callbacks.go | 37 ++++++------------------------------- finisher_api.go | 16 +++++++++++++++- 2 files changed, 21 insertions(+), 32 deletions(-) diff --git a/callbacks.go b/callbacks.go index f835e5049..c060ea709 100644 --- a/callbacks.go +++ b/callbacks.go @@ -2,7 +2,6 @@ package gorm import ( "context" - "database/sql" "errors" "fmt" "reflect" @@ -16,13 +15,12 @@ import ( func initializeCallbacks(db *DB) *callbacks { return &callbacks{ processors: map[string]*processor{ - "create": {db: db}, - "query": {db: db}, - "update": {db: db}, - "delete": {db: db}, - "row": {db: db}, - "raw": {db: db}, - "transaction": {db: db}, + "create": {db: db}, + "query": {db: db}, + "update": {db: db}, + "delete": {db: db}, + "row": {db: db}, + "raw": {db: db}, }, } } @@ -74,29 +72,6 @@ func (cs *callbacks) Raw() *processor { return cs.processors["raw"] } -func (cs *callbacks) Transaction() *processor { - return cs.processors["transaction"] -} - -func (p *processor) Begin(tx *DB, opt *sql.TxOptions) *DB { - var err error - - switch beginner := tx.Statement.ConnPool.(type) { - case TxBeginner: - tx.Statement.ConnPool, err = beginner.BeginTx(tx.Statement.Context, opt) - case ConnPoolBeginner: - tx.Statement.ConnPool, err = beginner.BeginTx(tx.Statement.Context, opt) - default: - err = ErrInvalidTransaction - } - - if err != nil { - _ = tx.AddError(err) - } - - return tx -} - func (p *processor) Execute(db *DB) *DB { // call scopes for len(db.Statement.scopes) > 0 { diff --git a/finisher_api.go b/finisher_api.go index 3e406c1cc..7a3f27bae 100644 --- a/finisher_api.go +++ b/finisher_api.go @@ -619,13 +619,27 @@ func (db *DB) Begin(opts ...*sql.TxOptions) *DB { // clone statement tx = db.getInstance().Session(&Session{Context: db.Statement.Context, NewDB: db.clone == 1}) opt *sql.TxOptions + err error ) if len(opts) > 0 { opt = opts[0] } - return tx.callbacks.Transaction().Begin(tx, opt) + switch beginner := tx.Statement.ConnPool.(type) { + case TxBeginner: + tx.Statement.ConnPool, err = beginner.BeginTx(tx.Statement.Context, opt) + case ConnPoolBeginner: + tx.Statement.ConnPool, err = beginner.BeginTx(tx.Statement.Context, opt) + default: + err = ErrInvalidTransaction + } + + if err != nil { + tx.AddError(err) + } + + return tx } // Commit commit a transaction From b13d1757fab7093d769afc02573ee3c359faeb26 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Thu, 7 Jul 2022 15:39:29 +0800 Subject: [PATCH 04/36] Refactor Model with slice data --- callbacks/update.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/callbacks/update.go b/callbacks/update.go index 42ffe2f69..48c61bf43 100644 --- a/callbacks/update.go +++ b/callbacks/update.go @@ -158,21 +158,21 @@ func ConvertToAssignments(stmt *gorm.Statement) (set clause.Set) { switch stmt.ReflectValue.Kind() { case reflect.Slice, reflect.Array: if size := stmt.ReflectValue.Len(); size > 0 { - var primaryKeyExprs []clause.Expression + var isZero bool for i := 0; i < size; i++ { - exprs := make([]clause.Expression, len(stmt.Schema.PrimaryFields)) - var notZero bool - for idx, field := range stmt.Schema.PrimaryFields { - value, isZero := field.ValueOf(stmt.Context, stmt.ReflectValue.Index(i)) - exprs[idx] = clause.Eq{Column: field.DBName, Value: value} - notZero = notZero || !isZero - } - if notZero { - primaryKeyExprs = append(primaryKeyExprs, clause.And(exprs...)) + for _, field := range stmt.Schema.PrimaryFields { + _, isZero = field.ValueOf(stmt.Context, stmt.ReflectValue.Index(i)) + if !isZero { + break + } } } - stmt.AddClause(clause.Where{Exprs: []clause.Expression{clause.And(clause.Or(primaryKeyExprs...))}}) + if !isZero { + _, primaryValues := schema.GetIdentityFieldValuesMap(stmt.Context, stmt.ReflectValue, stmt.Schema.PrimaryFields) + column, values := schema.ToQueryValues("", stmt.Schema.PrimaryFieldDBNames, primaryValues) + stmt.AddClause(clause.Where{Exprs: []clause.Expression{clause.IN{Column: column, Values: values}}}) + } } case reflect.Struct: for _, field := range stmt.Schema.PrimaryFields { From 62fdc2bb3b4f991a8ed1ec2fdb47571a64fd18ef Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 11 Jul 2022 11:51:05 +0800 Subject: [PATCH 05/36] Fix serializer with empty string --- schema/serializer.go | 10 +++++++--- tests/go.mod | 4 ++-- tests/serializer_test.go | 8 ++++++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/schema/serializer.go b/schema/serializer.go index 758a6421f..21be3c35f 100644 --- a/schema/serializer.go +++ b/schema/serializer.go @@ -88,7 +88,9 @@ func (JSONSerializer) Scan(ctx context.Context, field *Field, dst reflect.Value, return fmt.Errorf("failed to unmarshal JSONB value: %#v", dbValue) } - err = json.Unmarshal(bytes, fieldValue.Interface()) + if len(bytes) > 0 { + err = json.Unmarshal(bytes, fieldValue.Interface()) + } } field.ReflectValueOf(ctx, dst).Set(fieldValue.Elem()) @@ -142,8 +144,10 @@ func (GobSerializer) Scan(ctx context.Context, field *Field, dst reflect.Value, default: return fmt.Errorf("failed to unmarshal gob value: %#v", dbValue) } - decoder := gob.NewDecoder(bytes.NewBuffer(bytesValue)) - err = decoder.Decode(fieldValue.Interface()) + if len(bytesValue) > 0 { + decoder := gob.NewDecoder(bytes.NewBuffer(bytesValue)) + err = decoder.Decode(fieldValue.Interface()) + } } field.ReflectValueOf(ctx, dst).Set(fieldValue.Elem()) return diff --git a/tests/go.mod b/tests/go.mod index f3e9d2602..7a788a43f 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -10,11 +10,11 @@ require ( github.com/lib/pq v1.10.6 github.com/mattn/go-sqlite3 v1.14.14 // indirect golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect - gorm.io/driver/mysql v1.3.4 + gorm.io/driver/mysql v1.3.5 gorm.io/driver/postgres v1.3.8 gorm.io/driver/sqlite v1.3.6 gorm.io/driver/sqlserver v1.3.2 - gorm.io/gorm v1.23.7 + gorm.io/gorm v1.23.8 ) replace gorm.io/gorm => ../ diff --git a/tests/serializer_test.go b/tests/serializer_test.go index 7232f9df4..95d25699d 100644 --- a/tests/serializer_test.go +++ b/tests/serializer_test.go @@ -113,6 +113,14 @@ func TestSerializer(t *testing.T) { } AssertEqual(t, result, data) + + if err := DB.Model(&result).Update("roles", "").Error; err != nil { + t.Fatalf("failed to update data's roles, got error %v", err) + } + + if err := DB.First(&result, data.ID).Error; err != nil { + t.Fatalf("failed to query data, got error %v", err) + } } func TestSerializerAssignFirstOrCreate(t *testing.T) { From 08f6d06e47b2ee6285577d726c59e5e2c3ff99ac Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Wed, 13 Jul 2022 17:21:19 +0800 Subject: [PATCH 06/36] Fix select with quoted column name --- statement.go | 2 +- statement_test.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/statement.go b/statement.go index 850af6cbc..79e299150 100644 --- a/statement.go +++ b/statement.go @@ -650,7 +650,7 @@ func (stmt *Statement) Changed(fields ...string) bool { return false } -var nameMatcher = regexp.MustCompile(`^[\W]?(?:[a-z_0-9]+?)[\W]?\.[\W]?([a-z_0-9]+?)[\W]?$`) +var nameMatcher = regexp.MustCompile(`^(?:[\W]?(?:[a-z_0-9]+?)[\W]?\.)?[\W]?([a-z_0-9]+?)[\W]?$`) // SelectAndOmitColumns get select and omit columns, select -> true, omit -> false func (stmt *Statement) SelectAndOmitColumns(requireCreate, requireUpdate bool) (map[string]bool, bool) { diff --git a/statement_test.go b/statement_test.go index a89cc7d29..4432cda45 100644 --- a/statement_test.go +++ b/statement_test.go @@ -45,6 +45,8 @@ func TestNameMatcher(t *testing.T) { "`table_1`.`name23`": "name23", "'table23'.'name_1'": "name_1", "'table23'.name1": "name1", + "'name1'": "name1", + "`name_1`": "name_1", } { if matches := nameMatcher.FindStringSubmatch(k); len(matches) < 2 || matches[1] != v { t.Errorf("failed to match value: %v, got %v, expect: %v", k, matches, v) From a7063848efe743166ad9fae460e8c2acc1b14a6d Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Wed, 13 Jul 2022 17:44:14 +0800 Subject: [PATCH 07/36] Fix select with uppercase column name --- statement.go | 2 +- statement_test.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/statement.go b/statement.go index 79e299150..aa5c29936 100644 --- a/statement.go +++ b/statement.go @@ -650,7 +650,7 @@ func (stmt *Statement) Changed(fields ...string) bool { return false } -var nameMatcher = regexp.MustCompile(`^(?:[\W]?(?:[a-z_0-9]+?)[\W]?\.)?[\W]?([a-z_0-9]+?)[\W]?$`) +var nameMatcher = regexp.MustCompile(`^(?:[\W]?(?:[A-Za-z_0-9]+?)[\W]?\.)?[\W]?([A-Za-z_0-9]+?)[\W]?$`) // SelectAndOmitColumns get select and omit columns, select -> true, omit -> false func (stmt *Statement) SelectAndOmitColumns(requireCreate, requireUpdate bool) (map[string]bool, bool) { diff --git a/statement_test.go b/statement_test.go index 4432cda45..19ab38f70 100644 --- a/statement_test.go +++ b/statement_test.go @@ -47,6 +47,8 @@ func TestNameMatcher(t *testing.T) { "'table23'.name1": "name1", "'name1'": "name1", "`name_1`": "name_1", + "`Name_1`": "Name_1", + "`Table`.`nAme`": "nAme", } { if matches := nameMatcher.FindStringSubmatch(k); len(matches) < 2 || matches[1] != v { t.Errorf("failed to match value: %v, got %v, expect: %v", k, matches, v) From cae30e9a50cb9260b805310062059853927d488c Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Wed, 13 Jul 2022 18:02:11 +0800 Subject: [PATCH 08/36] Fix select with association column --- statement.go | 6 +++--- statement_test.go | 28 ++++++++++++++-------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/statement.go b/statement.go index aa5c29936..9a621179f 100644 --- a/statement.go +++ b/statement.go @@ -650,7 +650,7 @@ func (stmt *Statement) Changed(fields ...string) bool { return false } -var nameMatcher = regexp.MustCompile(`^(?:[\W]?(?:[A-Za-z_0-9]+?)[\W]?\.)?[\W]?([A-Za-z_0-9]+?)[\W]?$`) +var nameMatcher = regexp.MustCompile(`^(?:[\W]?([A-Za-z_0-9]+?)[\W]?\.)?[\W]?([A-Za-z_0-9]+?)[\W]?$`) // SelectAndOmitColumns get select and omit columns, select -> true, omit -> false func (stmt *Statement) SelectAndOmitColumns(requireCreate, requireUpdate bool) (map[string]bool, bool) { @@ -672,8 +672,8 @@ func (stmt *Statement) SelectAndOmitColumns(requireCreate, requireUpdate bool) ( } } else if field := stmt.Schema.LookUpField(column); field != nil && field.DBName != "" { results[field.DBName] = true - } else if matches := nameMatcher.FindStringSubmatch(column); len(matches) == 2 { - results[matches[1]] = true + } else if matches := nameMatcher.FindStringSubmatch(column); len(matches) == 3 && matches[1] == stmt.Table { + results[matches[2]] = true } else { results[column] = true } diff --git a/statement_test.go b/statement_test.go index 19ab38f70..a537c7be3 100644 --- a/statement_test.go +++ b/statement_test.go @@ -36,21 +36,21 @@ func TestWhereCloneCorruption(t *testing.T) { } func TestNameMatcher(t *testing.T) { - for k, v := range map[string]string{ - "table.name": "name", - "`table`.`name`": "name", - "'table'.'name'": "name", - "'table'.name": "name", - "table1.name_23": "name_23", - "`table_1`.`name23`": "name23", - "'table23'.'name_1'": "name_1", - "'table23'.name1": "name1", - "'name1'": "name1", - "`name_1`": "name_1", - "`Name_1`": "Name_1", - "`Table`.`nAme`": "nAme", + for k, v := range map[string][]string{ + "table.name": []string{"table", "name"}, + "`table`.`name`": []string{"table", "name"}, + "'table'.'name'": []string{"table", "name"}, + "'table'.name": []string{"table", "name"}, + "table1.name_23": []string{"table1", "name_23"}, + "`table_1`.`name23`": []string{"table_1", "name23"}, + "'table23'.'name_1'": []string{"table23", "name_1"}, + "'table23'.name1": []string{"table23", "name1"}, + "'name1'": []string{"", "name1"}, + "`name_1`": []string{"", "name_1"}, + "`Name_1`": []string{"", "Name_1"}, + "`Table`.`nAme`": []string{"Table", "nAme"}, } { - if matches := nameMatcher.FindStringSubmatch(k); len(matches) < 2 || matches[1] != v { + if matches := nameMatcher.FindStringSubmatch(k); len(matches) < 3 || matches[1] != v[0] || matches[2] != v[1] { t.Errorf("failed to match value: %v, got %v, expect: %v", k, matches, v) } } From 3262daf8d46818395a7b01778e8f813afc0dc3d2 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Wed, 13 Jul 2022 18:26:35 +0800 Subject: [PATCH 09/36] Fix select with association column --- statement.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/statement.go b/statement.go index 9a621179f..126878103 100644 --- a/statement.go +++ b/statement.go @@ -672,7 +672,7 @@ func (stmt *Statement) SelectAndOmitColumns(requireCreate, requireUpdate bool) ( } } else if field := stmt.Schema.LookUpField(column); field != nil && field.DBName != "" { results[field.DBName] = true - } else if matches := nameMatcher.FindStringSubmatch(column); len(matches) == 3 && matches[1] == stmt.Table { + } else if matches := nameMatcher.FindStringSubmatch(column); len(matches) == 3 && (matches[1] == stmt.Table || matches[1] == "") { results[matches[2]] = true } else { results[column] = true From 4d40e34734289137d9ca8fc2b69bf8de98a7448c Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Thu, 14 Jul 2022 14:39:43 +0800 Subject: [PATCH 10/36] Update select tests --- tests/helper_test.go | 2 ++ tests/update_belongs_to_test.go | 15 +++++++++++++++ tests/update_has_one_test.go | 10 +++++++--- tests/update_test.go | 2 ++ 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/tests/helper_test.go b/tests/helper_test.go index 7ee2a5761..d1af0739e 100644 --- a/tests/helper_test.go +++ b/tests/helper_test.go @@ -80,6 +80,7 @@ func CheckPet(t *testing.T, pet Pet, expect Pet) { t.Fatalf("errors happened when query: %v", err) } else { AssertObjEqual(t, newPet, pet, "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "UserID", "Name") + AssertObjEqual(t, newPet, expect, "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "UserID", "Name") } } @@ -174,6 +175,7 @@ func CheckUser(t *testing.T, user User, expect User) { var manager User DB.First(&manager, "id = ?", *user.ManagerID) AssertObjEqual(t, manager, user.Manager, "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "Name", "Age", "Birthday", "CompanyID", "ManagerID", "Active") + AssertObjEqual(t, manager, expect.Manager, "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "Name", "Age", "Birthday", "CompanyID", "ManagerID", "Active") } } else if user.ManagerID != nil { t.Errorf("Manager should not be created for zero value, got: %+v", user.ManagerID) diff --git a/tests/update_belongs_to_test.go b/tests/update_belongs_to_test.go index 8fe0f2897..4e94cfd55 100644 --- a/tests/update_belongs_to_test.go +++ b/tests/update_belongs_to_test.go @@ -41,4 +41,19 @@ func TestUpdateBelongsTo(t *testing.T) { var user4 User DB.Preload("Company").Preload("Manager").Find(&user4, "id = ?", user.ID) CheckUser(t, user4, user) + + user.Company.Name += "new2" + user.Manager.Name += "new2" + if err := DB.Session(&gorm.Session{FullSaveAssociations: true}).Select("`Company`").Save(&user).Error; err != nil { + t.Fatalf("errors happened when update: %v", err) + } + + var user5 User + DB.Preload("Company").Preload("Manager").Find(&user5, "id = ?", user.ID) + if user5.Manager.Name != user4.Manager.Name { + t.Errorf("should not update user's manager") + } else { + user.Manager.Name = user4.Manager.Name + } + CheckUser(t, user, user5) } diff --git a/tests/update_has_one_test.go b/tests/update_has_one_test.go index c926fbcf8..40af6ae7f 100644 --- a/tests/update_has_one_test.go +++ b/tests/update_has_one_test.go @@ -90,8 +90,9 @@ func TestUpdateHasOne(t *testing.T) { t.Run("Restriction", func(t *testing.T) { type CustomizeAccount struct { gorm.Model - UserID sql.NullInt64 - Number string `gorm:"<-:create"` + UserID sql.NullInt64 + Number string `gorm:"<-:create"` + Number2 string } type CustomizeUser struct { @@ -114,7 +115,8 @@ func TestUpdateHasOne(t *testing.T) { cusUser := CustomizeUser{ Name: "update-has-one-associations", Account: CustomizeAccount{ - Number: number, + Number: number, + Number2: number, }, } @@ -122,6 +124,7 @@ func TestUpdateHasOne(t *testing.T) { t.Fatalf("errors happened when create: %v", err) } cusUser.Account.Number += "-update" + cusUser.Account.Number2 += "-update" if err := DB.Session(&gorm.Session{FullSaveAssociations: true}).Updates(&cusUser).Error; err != nil { t.Fatalf("errors happened when create: %v", err) } @@ -129,5 +132,6 @@ func TestUpdateHasOne(t *testing.T) { var account2 CustomizeAccount DB.Find(&account2, "user_id = ?", cusUser.ID) AssertEqual(t, account2.Number, number) + AssertEqual(t, account2.Number2, cusUser.Account.Number2) }) } diff --git a/tests/update_test.go b/tests/update_test.go index 0fc89a93a..d76345807 100644 --- a/tests/update_test.go +++ b/tests/update_test.go @@ -307,6 +307,8 @@ func TestSelectWithUpdate(t *testing.T) { if utils.AssertEqual(result.UpdatedAt, user.UpdatedAt) { t.Fatalf("Update struct should update UpdatedAt, was %+v, got %+v", result.UpdatedAt, user.UpdatedAt) } + + AssertObjEqual(t, result, User{Name: "update_with_select"}, "Name", "Age") } func TestSelectWithUpdateWithMap(t *testing.T) { From 099813bf11dc1c4e614d73daee5766f4963136cf Mon Sep 17 00:00:00 2001 From: alingse Date: Thu, 14 Jul 2022 20:05:22 +0800 Subject: [PATCH 11/36] Adjust ToStringKey use unpack params, fix pass []any as any in variadic function (#5500) * fix pass []any as any in variadic function * add .vscode to gitignore --- .gitignore | 3 ++- callbacks/associations.go | 4 ++-- utils/utils_test.go | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 45505cc93..727333260 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ documents coverage.txt _book .idea -vendor \ No newline at end of file +vendor +.vscode diff --git a/callbacks/associations.go b/callbacks/associations.go index 4a50e6c24..00e00fcc3 100644 --- a/callbacks/associations.go +++ b/callbacks/associations.go @@ -206,7 +206,7 @@ func SaveAfterAssociations(create bool) func(db *gorm.DB) { } } - cacheKey := utils.ToStringKey(relPrimaryValues) + cacheKey := utils.ToStringKey(relPrimaryValues...) if len(relPrimaryValues) != len(rel.FieldSchema.PrimaryFields) || !identityMap[cacheKey] { identityMap[cacheKey] = true if isPtr { @@ -292,7 +292,7 @@ func SaveAfterAssociations(create bool) func(db *gorm.DB) { } } - cacheKey := utils.ToStringKey(relPrimaryValues) + cacheKey := utils.ToStringKey(relPrimaryValues...) if len(relPrimaryValues) != len(rel.FieldSchema.PrimaryFields) || !identityMap[cacheKey] { identityMap[cacheKey] = true distinctElems = reflect.Append(distinctElems, elem) diff --git a/utils/utils_test.go b/utils/utils_test.go index 5737c511f..27dfee16c 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -12,3 +12,20 @@ func TestIsValidDBNameChar(t *testing.T) { } } } + +func TestToStringKey(t *testing.T) { + cases := []struct { + values []interface{} + key string + }{ + {[]interface{}{"a"}, "a"}, + {[]interface{}{1, 2, 3}, "1_2_3"}, + {[]interface{}{[]interface{}{1, 2, 3}}, "[1 2 3]"}, + {[]interface{}{[]interface{}{"1", "2", "3"}}, "[1 2 3]"}, + } + for _, c := range cases { + if key := ToStringKey(c.values...); key != c.key { + t.Errorf("%v: expected %v, got %v", c.values, c.key, key) + } + } +} From 2ba599e8b7d2197739669970fa88d591423f0cae Mon Sep 17 00:00:00 2001 From: Goxiaoy Date: Fri, 15 Jul 2022 11:15:18 +0800 Subject: [PATCH 12/36] fix empty QueryClauses in association (#5502) (#5503) * fix empty QueryClauses in association (#5502) * test: empty QueryClauses in association (#5502) * style: empty QueryClauses in association (#5502) * style: empty QueryClauses in association (#5502) --- association.go | 4 ++- tests/associations_test.go | 64 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/association.go b/association.go index 35e10ddd4..06229caa7 100644 --- a/association.go +++ b/association.go @@ -507,7 +507,9 @@ func (association *Association) buildCondition() *DB { joinStmt.AddClause(queryClause) } joinStmt.Build("WHERE") - tx.Clauses(clause.Expr{SQL: strings.Replace(joinStmt.SQL.String(), "WHERE ", "", 1), Vars: joinStmt.Vars}) + if len(joinStmt.SQL.String()) > 0 { + tx.Clauses(clause.Expr{SQL: strings.Replace(joinStmt.SQL.String(), "WHERE ", "", 1), Vars: joinStmt.Vars}) + } } tx = tx.Session(&Session{QueryFields: true}).Clauses(clause.From{Joins: []clause.Join{{ diff --git a/tests/associations_test.go b/tests/associations_test.go index e729e979b..42b32afc0 100644 --- a/tests/associations_test.go +++ b/tests/associations_test.go @@ -4,6 +4,8 @@ import ( "testing" "gorm.io/gorm" + "gorm.io/gorm/clause" + "gorm.io/gorm/schema" . "gorm.io/gorm/utils/tests" ) @@ -284,3 +286,65 @@ func TestAssociationError(t *testing.T) { err = DB.Model(&emptyUser).Association("Languages").Delete(&user1.Languages) AssertEqual(t, err, gorm.ErrPrimaryKeyRequired) } + +type ( + myType string + emptyQueryClause struct { + Field *schema.Field + } +) + +func (myType) QueryClauses(f *schema.Field) []clause.Interface { + return []clause.Interface{emptyQueryClause{Field: f}} +} + +func (sd emptyQueryClause) Name() string { + return "empty" +} + +func (sd emptyQueryClause) Build(clause.Builder) { +} + +func (sd emptyQueryClause) MergeClause(*clause.Clause) { +} + +func (sd emptyQueryClause) ModifyStatement(stmt *gorm.Statement) { + // do nothing +} + +func TestAssociationEmptyQueryClause(t *testing.T) { + type Organization struct { + gorm.Model + Name string + } + type Region struct { + gorm.Model + Name string + Organizations []Organization `gorm:"many2many:region_orgs;"` + } + type RegionOrg struct { + RegionId uint + OrganizationId uint + Empty myType + } + if err := DB.SetupJoinTable(&Region{}, "Organizations", &RegionOrg{}); err != nil { + t.Fatalf("Failed to set up join table, got error: %s", err) + } + if err := DB.Migrator().DropTable(&Organization{}, &Region{}); err != nil { + t.Fatalf("Failed to migrate, got error: %s", err) + } + if err := DB.AutoMigrate(&Organization{}, &Region{}); err != nil { + t.Fatalf("Failed to migrate, got error: %v", err) + } + region := &Region{Name: "Region1"} + if err := DB.Create(region).Error; err != nil { + t.Fatalf("fail to create region %v", err) + } + var orgs []Organization + + if err := DB.Model(&Region{}).Association("Organizations").Find(&orgs); err != nil { + t.Fatalf("fail to find region organizations %v", err) + } else { + AssertEqual(t, len(orgs), 0) + } +} From 75720099b5540a38fa9f7c26d8237df2cd1570a9 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 18 Jul 2022 18:06:45 +0800 Subject: [PATCH 13/36] Create a new db in FindInBatches --- finisher_api.go | 4 +++- gorm.go | 3 ++- tests/query_test.go | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/finisher_api.go b/finisher_api.go index 7a3f27bae..af9afb636 100644 --- a/finisher_api.go +++ b/finisher_api.go @@ -202,7 +202,9 @@ func (db *DB) FindInBatches(dest interface{}, batchSize int, fc func(tx *DB, bat batch++ if result.Error == nil && result.RowsAffected != 0 { - tx.AddError(fc(result, batch)) + fcTx := result.Session(&Session{NewDB: true}) + fcTx.RowsAffected = result.RowsAffected + tx.AddError(fc(fcTx, batch)) } else if result.Error != nil { tx.AddError(result.Error) } diff --git a/gorm.go b/gorm.go index 6a6bb0322..c852e60cd 100644 --- a/gorm.go +++ b/gorm.go @@ -300,7 +300,8 @@ func (db *DB) WithContext(ctx context.Context) *DB { // Debug start debug mode func (db *DB) Debug() (tx *DB) { - return db.Session(&Session{ + tx = db.getInstance() + return tx.Session(&Session{ Logger: db.Logger.LogMode(logger.Info), }) } diff --git a/tests/query_test.go b/tests/query_test.go index 253d84092..4569fe1a1 100644 --- a/tests/query_test.go +++ b/tests/query_test.go @@ -257,7 +257,7 @@ func TestFindInBatches(t *testing.T) { totalBatch int ) - if result := DB.Where("name = ?", users[0].Name).FindInBatches(&results, 2, func(tx *gorm.DB, batch int) error { + if result := DB.Table("users as u").Where("name = ?", users[0].Name).FindInBatches(&results, 2, func(tx *gorm.DB, batch int) error { totalBatch += batch if tx.RowsAffected != 2 { @@ -273,7 +273,7 @@ func TestFindInBatches(t *testing.T) { } if err := tx.Save(results).Error; err != nil { - t.Errorf("failed to save users, got error %v", err) + t.Fatalf("failed to save users, got error %v", err) } return nil From bab3cd1724cb111961d931f514e1bda316de8572 Mon Sep 17 00:00:00 2001 From: Xudong Zhang Date: Mon, 18 Jul 2022 20:47:00 +0800 Subject: [PATCH 14/36] fix bad logging performance of bulk create (#5520) (#5521) --- logger/sql.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/logger/sql.go b/logger/sql.go index c8b194c3c..bcacc7cf5 100644 --- a/logger/sql.go +++ b/logger/sql.go @@ -30,6 +30,8 @@ func isPrintable(s string) bool { var convertibleTypes = []reflect.Type{reflect.TypeOf(time.Time{}), reflect.TypeOf(false), reflect.TypeOf([]byte{})} +var numericPlaceholderRe = regexp.MustCompile(`\$\d+\$`) + // ExplainSQL generate SQL string with given parameters, the generated SQL is expected to be used in logger, execute it might introduce a SQL injection vulnerability func ExplainSQL(sql string, numericPlaceholder *regexp.Regexp, escaper string, avars ...interface{}) string { var ( @@ -138,9 +140,18 @@ func ExplainSQL(sql string, numericPlaceholder *regexp.Regexp, escaper string, a sql = newSQL.String() } else { sql = numericPlaceholder.ReplaceAllString(sql, "$$$1$$") - for idx, v := range vars { - sql = strings.Replace(sql, "$"+strconv.Itoa(idx+1)+"$", v, 1) - } + + sql = numericPlaceholderRe.ReplaceAllStringFunc(sql, func(v string) string { + num := v[1 : len(v)-1] + n, _ := strconv.Atoi(num) + + // position var start from 1 ($1, $2) + n -= 1 + if n >= 0 && n <= len(vars)-1 { + return vars[n] + } + return v + }) } return sql From 06e174e24ddc3a49716ccd877aac221ca2469331 Mon Sep 17 00:00:00 2001 From: Cr <631807682@qq.com> Date: Mon, 25 Jul 2022 14:10:30 +0800 Subject: [PATCH 15/36] fix: embedded default value (#5540) --- schema/field.go | 8 ++------ tests/embedded_struct_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/schema/field.go b/schema/field.go index d4dfbd6f7..47f3994fe 100644 --- a/schema/field.go +++ b/schema/field.go @@ -403,18 +403,14 @@ func (schema *Schema) ParseField(fieldStruct reflect.StructField) *Field { } if ef.PrimaryKey { - if val, ok := ef.TagSettings["PRIMARYKEY"]; ok && utils.CheckTruth(val) { - ef.PrimaryKey = true - } else if val, ok := ef.TagSettings["PRIMARY_KEY"]; ok && utils.CheckTruth(val) { - ef.PrimaryKey = true - } else { + if !utils.CheckTruth(ef.TagSettings["PRIMARYKEY"], ef.TagSettings["PRIMARY_KEY"]) { ef.PrimaryKey = false if val, ok := ef.TagSettings["AUTOINCREMENT"]; !ok || !utils.CheckTruth(val) { ef.AutoIncrement = false } - if ef.DefaultValue == "" { + if !ef.AutoIncrement && ef.DefaultValue == "" { ef.HasDefaultValue = false } } diff --git a/tests/embedded_struct_test.go b/tests/embedded_struct_test.go index 312a5c371..e309d06c4 100644 --- a/tests/embedded_struct_test.go +++ b/tests/embedded_struct_test.go @@ -168,3 +168,29 @@ func TestEmbeddedRelations(t *testing.T) { } } } + +func TestEmbeddedTagSetting(t *testing.T) { + type Tag1 struct { + Id int64 `gorm:"autoIncrement"` + } + type Tag2 struct { + Id int64 + } + + type EmbeddedTag struct { + Tag1 Tag1 `gorm:"Embedded;"` + Tag2 Tag2 `gorm:"Embedded;EmbeddedPrefix:t2_"` + Name string + } + + DB.Migrator().DropTable(&EmbeddedTag{}) + err := DB.Migrator().AutoMigrate(&EmbeddedTag{}) + AssertEqual(t, err, nil) + + t1 := EmbeddedTag{Name: "embedded_tag"} + err = DB.Save(&t1).Error + AssertEqual(t, err, nil) + if t1.Tag1.Id == 0 { + t.Errorf("embedded struct's primary field should be rewrited") + } +} From 3c6eb14c92679e34cd49de53ef0b3d327f4dd06a Mon Sep 17 00:00:00 2001 From: MJrocker <1725014728@qq.com> Date: Tue, 26 Jul 2022 20:01:20 +0800 Subject: [PATCH 16/36] Fixed some typos in the code comment --- schema/schema.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/schema/schema.go b/schema/schema.go index eca113e96..3791237de 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -112,7 +112,7 @@ func ParseWithSpecialTableName(dest interface{}, cacheStore *sync.Map, namer Nam schemaCacheKey = modelType } - // Load exist schmema cache, return if exists + // Load exist schema cache, return if exists if v, ok := cacheStore.Load(schemaCacheKey); ok { s := v.(*Schema) // Wait for the initialization of other goroutines to complete @@ -146,7 +146,7 @@ func ParseWithSpecialTableName(dest interface{}, cacheStore *sync.Map, namer Nam // When the schema initialization is completed, the channel will be closed defer close(schema.initialized) - // Load exist schmema cache, return if exists + // Load exist schema cache, return if exists if v, ok := cacheStore.Load(schemaCacheKey); ok { s := v.(*Schema) // Wait for the initialization of other goroutines to complete From 6e03b97e266f30db994d8bc24bca2afd74a106b9 Mon Sep 17 00:00:00 2001 From: "hjwblog.com" Date: Wed, 27 Jul 2022 13:59:47 +0800 Subject: [PATCH 17/36] fix: empty serilizer err #5524 (#5525) * fix: empty serilizer err #5524 * feat: fix UnixSecondSerializer return nil * feat: split type case Co-authored-by: huanjiawei --- schema/field.go | 5 +---- schema/serializer.go | 10 ++++++++-- tests/go.mod | 1 - tests/serializer_test.go | 29 +++++++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/schema/field.go b/schema/field.go index 47f3994fe..1589d984a 100644 --- a/schema/field.go +++ b/schema/field.go @@ -468,9 +468,6 @@ func (field *Field) setupValuerAndSetter() { oldValuerOf := field.ValueOf field.ValueOf = func(ctx context.Context, v reflect.Value) (interface{}, bool) { value, zero := oldValuerOf(ctx, v) - if zero { - return value, zero - } s, ok := value.(SerializerValuerInterface) if !ok { @@ -483,7 +480,7 @@ func (field *Field) setupValuerAndSetter() { Destination: v, Context: ctx, fieldValue: value, - }, false + }, zero } } diff --git a/schema/serializer.go b/schema/serializer.go index 21be3c35f..00a4f85f0 100644 --- a/schema/serializer.go +++ b/schema/serializer.go @@ -119,9 +119,15 @@ func (UnixSecondSerializer) Scan(ctx context.Context, field *Field, dst reflect. // Value implements serializer interface func (UnixSecondSerializer) Value(ctx context.Context, field *Field, dst reflect.Value, fieldValue interface{}) (result interface{}, err error) { + rv := reflect.ValueOf(fieldValue) switch v := fieldValue.(type) { - case int64, int, uint, uint64, int32, uint32, int16, uint16, *int64, *int, *uint, *uint64, *int32, *uint32, *int16, *uint16: - result = time.Unix(reflect.Indirect(reflect.ValueOf(v)).Int(), 0) + case int64, int, uint, uint64, int32, uint32, int16, uint16: + result = time.Unix(reflect.Indirect(rv).Int(), 0) + case *int64, *int, *uint, *uint64, *int32, *uint32, *int16, *uint16: + if rv.IsZero() { + return nil, nil + } + result = time.Unix(reflect.Indirect(rv).Int(), 0) default: err = fmt.Errorf("invalid field type %#v for UnixSecondSerializer, only int, uint supported", v) } diff --git a/tests/go.mod b/tests/go.mod index 7a788a43f..eb8f336d6 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -3,7 +3,6 @@ module gorm.io/gorm/tests go 1.14 require ( - github.com/denisenkom/go-mssqldb v0.12.2 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/google/uuid v1.3.0 github.com/jinzhu/now v1.1.5 diff --git a/tests/serializer_test.go b/tests/serializer_test.go index 95d25699d..946536bf4 100644 --- a/tests/serializer_test.go +++ b/tests/serializer_test.go @@ -123,6 +123,35 @@ func TestSerializer(t *testing.T) { } } +func TestSerializerZeroValue(t *testing.T) { + schema.RegisterSerializer("custom", NewCustomSerializer("hello")) + DB.Migrator().DropTable(&SerializerStruct{}) + if err := DB.Migrator().AutoMigrate(&SerializerStruct{}); err != nil { + t.Fatalf("no error should happen when migrate scanner, valuer struct, got error %v", err) + } + + data := SerializerStruct{} + + if err := DB.Create(&data).Error; err != nil { + t.Fatalf("failed to create data, got error %v", err) + } + + var result SerializerStruct + if err := DB.First(&result, data.ID).Error; err != nil { + t.Fatalf("failed to query data, got error %v", err) + } + + AssertEqual(t, result, data) + + if err := DB.Model(&result).Update("roles", "").Error; err != nil { + t.Fatalf("failed to update data's roles, got error %v", err) + } + + if err := DB.First(&result, data.ID).Error; err != nil { + t.Fatalf("failed to query data, got error %v", err) + } +} + func TestSerializerAssignFirstOrCreate(t *testing.T) { schema.RegisterSerializer("custom", NewCustomSerializer("hello")) DB.Migrator().DropTable(&SerializerStruct{}) From f22327938485f1673eab443949ae92367293c566 Mon Sep 17 00:00:00 2001 From: Cr <631807682@qq.com> Date: Wed, 10 Aug 2022 11:03:42 +0800 Subject: [PATCH 18/36] chore: fix gorm tag (#5577) --- utils/tests/models.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/tests/models.go b/utils/tests/models.go index 22e8e659f..ec1651a35 100644 --- a/utils/tests/models.go +++ b/utils/tests/models.go @@ -64,8 +64,8 @@ type Language struct { type Coupon struct { ID int `gorm:"primarykey; size:255"` AppliesToProduct []*CouponProduct `gorm:"foreignKey:CouponId;constraint:OnDelete:CASCADE"` - AmountOff uint32 `gorm:"amount_off"` - PercentOff float32 `gorm:"percent_off"` + AmountOff uint32 `gorm:"column:amount_off"` + PercentOff float32 `gorm:"column:percent_off"` } type CouponProduct struct { From a35883590b7f9467bedf43b9611b2c0d0ff30ffd Mon Sep 17 00:00:00 2001 From: Bruce MacKenzie Date: Wed, 10 Aug 2022 23:38:04 -0400 Subject: [PATCH 19/36] update Delete Godoc to describe soft delete behaviour (#5554) --- finisher_api.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/finisher_api.go b/finisher_api.go index af9afb636..bdf0437dc 100644 --- a/finisher_api.go +++ b/finisher_api.go @@ -388,7 +388,9 @@ func (db *DB) UpdateColumns(values interface{}) (tx *DB) { return tx.callbacks.Update().Execute(tx) } -// Delete delete value match given conditions, if the value has primary key, then will including the primary key as condition +// Delete deletes value matching given conditions. If value contains primary key it is included in the conditions. +// If value includes a deleted_at field, then Delete performs a soft delete instead by setting deleted_at with the current +// time if null. func (db *DB) Delete(value interface{}, conds ...interface{}) (tx *DB) { tx = db.getInstance() if len(conds) > 0 { From 573b9fa536050c156968b4d228cab05a119d78df Mon Sep 17 00:00:00 2001 From: enwawerueli Date: Fri, 12 Aug 2022 16:46:18 +0300 Subject: [PATCH 20/36] fix: correct grammar --- gorm.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gorm.go b/gorm.go index c852e60cd..1f1dac213 100644 --- a/gorm.go +++ b/gorm.go @@ -413,7 +413,7 @@ func (db *DB) SetupJoinTable(model interface{}, field string, joinTable interfac relation, ok := modelSchema.Relationships.Relations[field] isRelation := ok && relation.JoinTable != nil if !isRelation { - return fmt.Errorf("failed to found relation: %s", field) + return fmt.Errorf("failed to find relation: %s", field) } for _, ref := range relation.References { From ba227e8939d05f249a3ede8901193801d8da8603 Mon Sep 17 00:00:00 2001 From: Aoang Date: Mon, 15 Aug 2022 10:46:57 +0800 Subject: [PATCH 21/36] Add Go 1.19 Support (#5608) --- .github/workflows/tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b97da3f45..367f4ccd9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: sqlite: strategy: matrix: - go: ['1.18', '1.17', '1.16'] + go: ['1.19', '1.18', '1.17', '1.16'] platform: [ubuntu-latest] # can not run in windows OS runs-on: ${{ matrix.platform }} @@ -42,7 +42,7 @@ jobs: strategy: matrix: dbversion: ['mysql:latest', 'mysql:5.7', 'mariadb:latest'] - go: ['1.18', '1.17', '1.16'] + go: ['1.19', '1.18', '1.17', '1.16'] platform: [ubuntu-latest] runs-on: ${{ matrix.platform }} @@ -86,7 +86,7 @@ jobs: strategy: matrix: dbversion: ['postgres:latest', 'postgres:13', 'postgres:12', 'postgres:11', 'postgres:10'] - go: ['1.18', '1.17', '1.16'] + go: ['1.19', '1.18', '1.17', '1.16'] platform: [ubuntu-latest] # can not run in macOS and Windows runs-on: ${{ matrix.platform }} @@ -128,7 +128,7 @@ jobs: sqlserver: strategy: matrix: - go: ['1.18', '1.17', '1.16'] + go: ['1.19', '1.18', '1.17', '1.16'] platform: [ubuntu-latest] # can not run test in macOS and windows runs-on: ${{ matrix.platform }} From 3f92b9b0df84736750d6645e074596a7383ae089 Mon Sep 17 00:00:00 2001 From: Shunsuke Otani Date: Mon, 15 Aug 2022 11:47:26 +0900 Subject: [PATCH 22/36] Refactor: redundant type from composite literal (#5604) --- statement_test.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/statement_test.go b/statement_test.go index a537c7be3..761daf37f 100644 --- a/statement_test.go +++ b/statement_test.go @@ -37,18 +37,18 @@ func TestWhereCloneCorruption(t *testing.T) { func TestNameMatcher(t *testing.T) { for k, v := range map[string][]string{ - "table.name": []string{"table", "name"}, - "`table`.`name`": []string{"table", "name"}, - "'table'.'name'": []string{"table", "name"}, - "'table'.name": []string{"table", "name"}, - "table1.name_23": []string{"table1", "name_23"}, - "`table_1`.`name23`": []string{"table_1", "name23"}, - "'table23'.'name_1'": []string{"table23", "name_1"}, - "'table23'.name1": []string{"table23", "name1"}, - "'name1'": []string{"", "name1"}, - "`name_1`": []string{"", "name_1"}, - "`Name_1`": []string{"", "Name_1"}, - "`Table`.`nAme`": []string{"Table", "nAme"}, + "table.name": {"table", "name"}, + "`table`.`name`": {"table", "name"}, + "'table'.'name'": {"table", "name"}, + "'table'.name": {"table", "name"}, + "table1.name_23": {"table1", "name_23"}, + "`table_1`.`name23`": {"table_1", "name23"}, + "'table23'.'name_1'": {"table23", "name_1"}, + "'table23'.name1": {"table23", "name1"}, + "'name1'": {"", "name1"}, + "`name_1`": {"", "name_1"}, + "`Name_1`": {"", "Name_1"}, + "`Table`.`nAme`": {"Table", "nAme"}, } { if matches := nameMatcher.FindStringSubmatch(k); len(matches) < 3 || matches[1] != v[0] || matches[2] != v[1] { t.Errorf("failed to match value: %v, got %v, expect: %v", k, matches, v) From 8c3018b96aea241a35b769291de6edd2a3378b44 Mon Sep 17 00:00:00 2001 From: Shunsuke Otani Date: Mon, 15 Aug 2022 11:50:06 +0900 Subject: [PATCH 23/36] Replace `ioutil.Discard` with `io.Discard` (#5603) --- go.mod | 2 +- logger/logger.go | 6 +++--- tests/go.mod | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 573627455..03f843799 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module gorm.io/gorm -go 1.14 +go 1.16 require ( github.com/jinzhu/inflection v1.0.0 diff --git a/logger/logger.go b/logger/logger.go index 2ffd28d5a..ce088561c 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -4,7 +4,7 @@ import ( "context" "errors" "fmt" - "io/ioutil" + "io" "log" "os" "time" @@ -68,8 +68,8 @@ type Interface interface { } var ( - // Discard Discard logger will print any log to ioutil.Discard - Discard = New(log.New(ioutil.Discard, "", log.LstdFlags), Config{}) + // Discard Discard logger will print any log to io.Discard + Discard = New(log.New(io.Discard, "", log.LstdFlags), Config{}) // Default Default logger Default = New(log.New(os.Stdout, "\r\n", log.LstdFlags), Config{ SlowThreshold: 200 * time.Millisecond, diff --git a/tests/go.mod b/tests/go.mod index eb8f336d6..19280434e 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -1,6 +1,6 @@ module gorm.io/gorm/tests -go 1.14 +go 1.16 require ( github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect From d71caef7d9d08287971a129bc19068eb1f48ed8f Mon Sep 17 00:00:00 2001 From: Cr <631807682@qq.com> Date: Sat, 3 Sep 2022 20:00:21 +0800 Subject: [PATCH 24/36] fix: remove uuid autoincrement (#5620) --- tests/postgres_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/postgres_test.go b/tests/postgres_test.go index 66b988c3a..97af6db3d 100644 --- a/tests/postgres_test.go +++ b/tests/postgres_test.go @@ -63,13 +63,13 @@ func TestPostgres(t *testing.T) { } type Post struct { - ID uuid.UUID `gorm:"primary_key;type:uuid;default:uuid_generate_v4();autoincrement"` + ID uuid.UUID `gorm:"primary_key;type:uuid;default:uuid_generate_v4();"` Title string Categories []*Category `gorm:"Many2Many:post_categories"` } type Category struct { - ID uuid.UUID `gorm:"primary_key;type:uuid;default:uuid_generate_v4();autoincrement"` + ID uuid.UUID `gorm:"primary_key;type:uuid;default:uuid_generate_v4();"` Title string Posts []*Post `gorm:"Many2Many:post_categories"` } From f78f635fae6f332a76e8f3e38d939864d1f5c209 Mon Sep 17 00:00:00 2001 From: "jesse.tang" <1430482733@qq.com> Date: Mon, 5 Sep 2022 15:34:33 +0800 Subject: [PATCH 25/36] Optimize: code logic db.scanIntoStruct() (#5633) --- scan.go | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/scan.go b/scan.go index 6250fb576..2db431601 100644 --- a/scan.go +++ b/scan.go @@ -66,30 +66,32 @@ func (db *DB) scanIntoStruct(rows Rows, reflectValue reflect.Value, values []int db.RowsAffected++ db.AddError(rows.Scan(values...)) - joinedSchemaMap := make(map[*schema.Field]interface{}, 0) + joinedSchemaMap := make(map[*schema.Field]interface{}) for idx, field := range fields { - if field != nil { - if len(joinFields) == 0 || joinFields[idx][0] == nil { - db.AddError(field.Set(db.Statement.Context, reflectValue, values[idx])) - } else { - joinSchema := joinFields[idx][0] - relValue := joinSchema.ReflectValueOf(db.Statement.Context, reflectValue) - if relValue.Kind() == reflect.Ptr { - if _, ok := joinedSchemaMap[joinSchema]; !ok { - if value := reflect.ValueOf(values[idx]).Elem(); value.Kind() == reflect.Ptr && value.IsNil() { - continue - } + if field == nil { + continue + } - relValue.Set(reflect.New(relValue.Type().Elem())) - joinedSchemaMap[joinSchema] = nil + if len(joinFields) == 0 || joinFields[idx][0] == nil { + db.AddError(field.Set(db.Statement.Context, reflectValue, values[idx])) + } else { + joinSchema := joinFields[idx][0] + relValue := joinSchema.ReflectValueOf(db.Statement.Context, reflectValue) + if relValue.Kind() == reflect.Ptr { + if _, ok := joinedSchemaMap[joinSchema]; !ok { + if value := reflect.ValueOf(values[idx]).Elem(); value.Kind() == reflect.Ptr && value.IsNil() { + continue } + + relValue.Set(reflect.New(relValue.Type().Elem())) + joinedSchemaMap[joinSchema] = nil } - db.AddError(joinFields[idx][1].Set(db.Statement.Context, relValue, values[idx])) } - - // release data to pool - field.NewValuePool.Put(values[idx]) + db.AddError(joinFields[idx][1].Set(db.Statement.Context, relValue, values[idx])) } + + // release data to pool + field.NewValuePool.Put(values[idx]) } } From b3eb1c8c512430c1600f720a96b2af777c91d1da Mon Sep 17 00:00:00 2001 From: Jiepeng Cao Date: Mon, 5 Sep 2022 15:39:19 +0800 Subject: [PATCH 26/36] simplified regexp (#5677) --- migrator/migrator.go | 2 +- statement.go | 2 +- tests/upsert_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/migrator/migrator.go b/migrator/migrator.go index 87ac77451..c1d7e0e75 100644 --- a/migrator/migrator.go +++ b/migrator/migrator.go @@ -15,7 +15,7 @@ import ( ) var ( - regFullDataType = regexp.MustCompile(`[^\d]*(\d+)[^\d]?`) + regFullDataType = regexp.MustCompile(`\D*(\d+)\D?`) ) // Migrator m struct diff --git a/statement.go b/statement.go index 126878103..cc26fe377 100644 --- a/statement.go +++ b/statement.go @@ -650,7 +650,7 @@ func (stmt *Statement) Changed(fields ...string) bool { return false } -var nameMatcher = regexp.MustCompile(`^(?:[\W]?([A-Za-z_0-9]+?)[\W]?\.)?[\W]?([A-Za-z_0-9]+?)[\W]?$`) +var nameMatcher = regexp.MustCompile(`^(?:\W?(\w+?)\W?\.)?\W?(\w+?)\W?$`) // SelectAndOmitColumns get select and omit columns, select -> true, omit -> false func (stmt *Statement) SelectAndOmitColumns(requireCreate, requireUpdate bool) (map[string]bool, bool) { diff --git a/tests/upsert_test.go b/tests/upsert_test.go index f90c4518f..e84dc14a1 100644 --- a/tests/upsert_test.go +++ b/tests/upsert_test.go @@ -62,7 +62,7 @@ func TestUpsert(t *testing.T) { } r := DB.Session(&gorm.Session{DryRun: true}).Clauses(clause.OnConflict{UpdateAll: true}).Create(&RestrictedLanguage{Code: "upsert_code", Name: "upsert_name", Lang: "upsert_lang"}) - if !regexp.MustCompile(`INTO .restricted_languages. .*\(.code.,.name.,.lang.\) .* (SET|UPDATE) .name.=.*.name.[^\w]*$`).MatchString(r.Statement.SQL.String()) { + if !regexp.MustCompile(`INTO .restricted_languages. .*\(.code.,.name.,.lang.\) .* (SET|UPDATE) .name.=.*.name.\W*$`).MatchString(r.Statement.SQL.String()) { t.Errorf("Table with escape character, got %v", r.Statement.SQL.String()) } } From f29afdd3297d94b3e789e1f8d0ab8c823325eba5 Mon Sep 17 00:00:00 2001 From: Bruce MacKenzie Date: Thu, 8 Sep 2022 23:16:41 -0400 Subject: [PATCH 27/36] Rewrite of finisher_api Godocs (#5618) --- finisher_api.go | 49 +++++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/finisher_api.go b/finisher_api.go index bdf0437dc..835a6984c 100644 --- a/finisher_api.go +++ b/finisher_api.go @@ -13,7 +13,7 @@ import ( "gorm.io/gorm/utils" ) -// Create insert the value into database +// Create inserts value, returning the inserted data's primary key in value's id func (db *DB) Create(value interface{}) (tx *DB) { if db.CreateBatchSize > 0 { return db.CreateInBatches(value, db.CreateBatchSize) @@ -24,7 +24,7 @@ func (db *DB) Create(value interface{}) (tx *DB) { return tx.callbacks.Create().Execute(tx) } -// CreateInBatches insert the value in batches into database +// CreateInBatches inserts value in batches of batchSize func (db *DB) CreateInBatches(value interface{}, batchSize int) (tx *DB) { reflectValue := reflect.Indirect(reflect.ValueOf(value)) @@ -68,7 +68,7 @@ func (db *DB) CreateInBatches(value interface{}, batchSize int) (tx *DB) { return } -// Save update value in database, if the value doesn't have primary key, will insert it +// Save updates value in database. If value doesn't contain a matching primary key, value is inserted. func (db *DB) Save(value interface{}) (tx *DB) { tx = db.getInstance() tx.Statement.Dest = value @@ -114,7 +114,7 @@ func (db *DB) Save(value interface{}) (tx *DB) { return } -// First find first record that match given conditions, order by primary key +// First finds the first record ordered by primary key, matching given conditions conds func (db *DB) First(dest interface{}, conds ...interface{}) (tx *DB) { tx = db.Limit(1).Order(clause.OrderByColumn{ Column: clause.Column{Table: clause.CurrentTable, Name: clause.PrimaryKey}, @@ -129,7 +129,7 @@ func (db *DB) First(dest interface{}, conds ...interface{}) (tx *DB) { return tx.callbacks.Query().Execute(tx) } -// Take return a record that match given conditions, the order will depend on the database implementation +// Take finds the first record returned by the database in no specified order, matching given conditions conds func (db *DB) Take(dest interface{}, conds ...interface{}) (tx *DB) { tx = db.Limit(1) if len(conds) > 0 { @@ -142,7 +142,7 @@ func (db *DB) Take(dest interface{}, conds ...interface{}) (tx *DB) { return tx.callbacks.Query().Execute(tx) } -// Last find last record that match given conditions, order by primary key +// Last finds the last record ordered by primary key, matching given conditions conds func (db *DB) Last(dest interface{}, conds ...interface{}) (tx *DB) { tx = db.Limit(1).Order(clause.OrderByColumn{ Column: clause.Column{Table: clause.CurrentTable, Name: clause.PrimaryKey}, @@ -158,7 +158,7 @@ func (db *DB) Last(dest interface{}, conds ...interface{}) (tx *DB) { return tx.callbacks.Query().Execute(tx) } -// Find find records that match given conditions +// Find finds all records matching given conditions conds func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB) { tx = db.getInstance() if len(conds) > 0 { @@ -170,7 +170,7 @@ func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB) { return tx.callbacks.Query().Execute(tx) } -// FindInBatches find records in batches +// FindInBatches finds all records in batches of batchSize func (db *DB) FindInBatches(dest interface{}, batchSize int, fc func(tx *DB, batch int) error) *DB { var ( tx = db.Order(clause.OrderByColumn{ @@ -286,7 +286,8 @@ func (db *DB) assignInterfacesToValue(values ...interface{}) { } } -// FirstOrInit gets the first matched record or initialize a new instance with given conditions (only works with struct or map conditions) +// FirstOrInit finds the first matching record, otherwise if not found initializes a new instance with given conds. +// Each conds must be a struct or map. func (db *DB) FirstOrInit(dest interface{}, conds ...interface{}) (tx *DB) { queryTx := db.Limit(1).Order(clause.OrderByColumn{ Column: clause.Column{Table: clause.CurrentTable, Name: clause.PrimaryKey}, @@ -312,7 +313,8 @@ func (db *DB) FirstOrInit(dest interface{}, conds ...interface{}) (tx *DB) { return } -// FirstOrCreate gets the first matched record or create a new one with given conditions (only works with struct, map conditions) +// FirstOrCreate finds the first matching record, otherwise if not found creates a new instance with given conds. +// Each conds must be a struct or map. func (db *DB) FirstOrCreate(dest interface{}, conds ...interface{}) (tx *DB) { tx = db.getInstance() queryTx := db.Session(&Session{}).Limit(1).Order(clause.OrderByColumn{ @@ -360,14 +362,14 @@ func (db *DB) FirstOrCreate(dest interface{}, conds ...interface{}) (tx *DB) { return tx } -// Update update attributes with callbacks, refer: https://gorm.io/docs/update.html#Update-Changed-Fields +// Update updates column with value using callbacks. Reference: https://gorm.io/docs/update.html#Update-Changed-Fields func (db *DB) Update(column string, value interface{}) (tx *DB) { tx = db.getInstance() tx.Statement.Dest = map[string]interface{}{column: value} return tx.callbacks.Update().Execute(tx) } -// Updates update attributes with callbacks, refer: https://gorm.io/docs/update.html#Update-Changed-Fields +// Updates updates attributes using callbacks. values must be a struct or map. Reference: https://gorm.io/docs/update.html#Update-Changed-Fields func (db *DB) Updates(values interface{}) (tx *DB) { tx = db.getInstance() tx.Statement.Dest = values @@ -388,8 +390,8 @@ func (db *DB) UpdateColumns(values interface{}) (tx *DB) { return tx.callbacks.Update().Execute(tx) } -// Delete deletes value matching given conditions. If value contains primary key it is included in the conditions. -// If value includes a deleted_at field, then Delete performs a soft delete instead by setting deleted_at with the current +// Delete deletes value matching given conditions. If value contains primary key it is included in the conditions. If +// value includes a deleted_at field, then Delete performs a soft delete instead by setting deleted_at with the current // time if null. func (db *DB) Delete(value interface{}, conds ...interface{}) (tx *DB) { tx = db.getInstance() @@ -484,7 +486,7 @@ func (db *DB) Rows() (*sql.Rows, error) { return rows, tx.Error } -// Scan scan value to a struct +// Scan scans selected value to the struct dest func (db *DB) Scan(dest interface{}) (tx *DB) { config := *db.Config currentLogger, newLogger := config.Logger, logger.Recorder.New() @@ -509,7 +511,7 @@ func (db *DB) Scan(dest interface{}) (tx *DB) { return } -// Pluck used to query single column from a model as a map +// Pluck queries a single column from a model, returning in the slice dest. E.g.: // var ages []int64 // db.Model(&users).Pluck("age", &ages) func (db *DB) Pluck(column string, dest interface{}) (tx *DB) { @@ -552,7 +554,8 @@ func (db *DB) ScanRows(rows *sql.Rows, dest interface{}) error { return tx.Error } -// Connection use a db conn to execute Multiple commands,this conn will put conn pool after it is executed. +// Connection uses a db connection to execute an arbitrary number of commands in fc. When finished, the connection is +// returned to the connection pool. func (db *DB) Connection(fc func(tx *DB) error) (err error) { if db.Error != nil { return db.Error @@ -574,7 +577,9 @@ func (db *DB) Connection(fc func(tx *DB) error) (err error) { return fc(tx) } -// Transaction start a transaction as a block, return error will rollback, otherwise to commit. +// Transaction start a transaction as a block, return error will rollback, otherwise to commit. Transaction executes an +// arbitrary number of commands in fc within a transaction. On success the changes are committed; if an error occurs +// they are rolled back. func (db *DB) Transaction(fc func(tx *DB) error, opts ...*sql.TxOptions) (err error) { panicked := true @@ -617,7 +622,7 @@ func (db *DB) Transaction(fc func(tx *DB) error, opts ...*sql.TxOptions) (err er return } -// Begin begins a transaction +// Begin begins a transaction with any transaction options opts func (db *DB) Begin(opts ...*sql.TxOptions) *DB { var ( // clone statement @@ -646,7 +651,7 @@ func (db *DB) Begin(opts ...*sql.TxOptions) *DB { return tx } -// Commit commit a transaction +// Commit commits the changes in a transaction func (db *DB) Commit() *DB { if committer, ok := db.Statement.ConnPool.(TxCommitter); ok && committer != nil && !reflect.ValueOf(committer).IsNil() { db.AddError(committer.Commit()) @@ -656,7 +661,7 @@ func (db *DB) Commit() *DB { return db } -// Rollback rollback a transaction +// Rollback rollbacks the changes in a transaction func (db *DB) Rollback() *DB { if committer, ok := db.Statement.ConnPool.(TxCommitter); ok && committer != nil { if !reflect.ValueOf(committer).IsNil() { @@ -686,7 +691,7 @@ func (db *DB) RollbackTo(name string) *DB { return db } -// Exec execute raw sql +// Exec executes raw sql func (db *DB) Exec(sql string, values ...interface{}) (tx *DB) { tx = db.getInstance() tx.Statement.SQL = strings.Builder{} From edb00c10adff38445c4350c0cb524faa6ec2d592 Mon Sep 17 00:00:00 2001 From: Googol Lee Date: Wed, 14 Sep 2022 04:26:51 +0200 Subject: [PATCH 28/36] AutoMigrate() should always migrate checks, even there is no relationship constraints. (#5644) * fix: remove uuid autoincrement * AutoMigrate() should always migrate checks, even there is no relationship constranits. Co-authored-by: a631807682 <631807682@qq.com> --- migrator/migrator.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/migrator/migrator.go b/migrator/migrator.go index c1d7e0e75..e6782a13b 100644 --- a/migrator/migrator.go +++ b/migrator/migrator.go @@ -135,12 +135,12 @@ func (m Migrator) AutoMigrate(values ...interface{}) error { } } } + } - for _, chk := range stmt.Schema.ParseCheckConstraints() { - if !tx.Migrator().HasConstraint(value, chk.Name) { - if err := tx.Migrator().CreateConstraint(value, chk.Name); err != nil { - return err - } + for _, chk := range stmt.Schema.ParseCheckConstraints() { + if !tx.Migrator().HasConstraint(value, chk.Name) { + if err := tx.Migrator().CreateConstraint(value, chk.Name); err != nil { + return err } } } From 490625981a1c3474eeca7f2e4fde791cd94c84fa Mon Sep 17 00:00:00 2001 From: qqxhb <30866940+qqxhb@users.noreply.github.com> Date: Fri, 16 Sep 2022 15:02:44 +0800 Subject: [PATCH 29/36] fix: update omit (#5699) --- callbacks/update.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/callbacks/update.go b/callbacks/update.go index 48c61bf43..b596df9a0 100644 --- a/callbacks/update.go +++ b/callbacks/update.go @@ -70,10 +70,12 @@ func Update(config *Config) func(db *gorm.DB) { if db.Statement.SQL.Len() == 0 { db.Statement.SQL.Grow(180) db.Statement.AddClauseIfNotExists(clause.Update{}) - if set := ConvertToAssignments(db.Statement); len(set) != 0 { - db.Statement.AddClause(set) - } else if _, ok := db.Statement.Clauses["SET"]; !ok { - return + if _, ok := db.Statement.Clauses["SET"]; !ok { + if set := ConvertToAssignments(db.Statement); len(set) != 0 { + db.Statement.AddClause(set) + } else { + return + } } db.Statement.Build(db.Statement.BuildClauses...) From 5ed7b1a65e2aeeb92bb12f2b1ebcac2e4d3402fe Mon Sep 17 00:00:00 2001 From: Cr <631807682@qq.com> Date: Thu, 22 Sep 2022 11:25:03 +0800 Subject: [PATCH 30/36] fix: same embedded filed name (#5705) --- migrator/migrator.go | 2 +- tests/migrate_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/migrator/migrator.go b/migrator/migrator.go index e6782a13b..d7ebf276e 100644 --- a/migrator/migrator.go +++ b/migrator/migrator.go @@ -478,7 +478,7 @@ func (m Migrator) MigrateColumn(value interface{}, field *schema.Field, columnTy } if alterColumn && !field.IgnoreMigration { - return m.DB.Migrator().AlterColumn(value, field.Name) + return m.DB.Migrator().AlterColumn(value, field.DBName) } return nil diff --git a/tests/migrate_test.go b/tests/migrate_test.go index 0b5bc5ebd..32e84e774 100644 --- a/tests/migrate_test.go +++ b/tests/migrate_test.go @@ -959,3 +959,41 @@ func TestMigrateArrayTypeModel(t *testing.T) { AssertEqual(t, nil, err) AssertEqual(t, "integer[]", ct.DatabaseTypeName()) } + +func TestMigrateSameEmbeddedFieldName(t *testing.T) { + type UserStat struct { + GroundDestroyCount int + } + + type GameUser struct { + gorm.Model + StatAb UserStat `gorm:"embedded;embeddedPrefix:stat_ab_"` + } + + type UserStat1 struct { + GroundDestroyCount string + } + + type GroundRate struct { + GroundDestroyCount int + } + + type GameUser1 struct { + gorm.Model + StatAb UserStat1 `gorm:"embedded;embeddedPrefix:stat_ab_"` + GroundRateRb GroundRate `gorm:"embedded;embeddedPrefix:rate_ground_rb_"` + } + + DB.Migrator().DropTable(&GameUser{}) + err := DB.AutoMigrate(&GameUser{}) + AssertEqual(t, nil, err) + + err = DB.Table("game_users").AutoMigrate(&GameUser1{}) + AssertEqual(t, nil, err) + + _, err = findColumnType(&GameUser{}, "stat_ab_ground_destory_count") + AssertEqual(t, nil, err) + + _, err = findColumnType(&GameUser{}, "rate_ground_rb_ground_destory_count") + AssertEqual(t, nil, err) +} From 1f634c39377f914187ae9efb1bc1bdbc94e97028 Mon Sep 17 00:00:00 2001 From: "jesse.tang" <1430482733@qq.com> Date: Thu, 22 Sep 2022 14:50:35 +0800 Subject: [PATCH 31/36] support scan assign slice cap (#5634) * support scan assign slice cap * fix --- scan.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scan.go b/scan.go index 2db431601..df5a3714b 100644 --- a/scan.go +++ b/scan.go @@ -248,7 +248,13 @@ func Scan(rows Rows, db *DB, mode ScanMode) { if !update || reflectValue.Len() == 0 { update = false - db.Statement.ReflectValue.Set(reflect.MakeSlice(reflectValue.Type(), 0, 20)) + // if the slice cap is externally initialized, the externally initialized slice is directly used here + if reflectValue.Cap() == 0 { + db.Statement.ReflectValue.Set(reflect.MakeSlice(reflectValue.Type(), 0, 20)) + } else { + reflectValue.SetLen(0) + db.Statement.ReflectValue.Set(reflectValue) + } } for initialized || rows.Next() { From 3a72ba102ec1ce729f703be4ac00e0049b82b0e2 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Wed, 21 Sep 2022 17:29:38 +0800 Subject: [PATCH 32/36] Allow shared foreign key for many2many jointable --- schema/relationship.go | 60 ++++++++++++++++++++++--------------- schema/relationship_test.go | 29 +++++++++++++++++- tests/go.mod | 13 ++++---- 3 files changed, 71 insertions(+), 31 deletions(-) diff --git a/schema/relationship.go b/schema/relationship.go index 0aa33e518..bb8aeb649 100644 --- a/schema/relationship.go +++ b/schema/relationship.go @@ -191,7 +191,8 @@ func (schema *Schema) buildMany2ManyRelation(relation *Relationship, field *Fiel err error joinTableFields []reflect.StructField fieldsMap = map[string]*Field{} - ownFieldsMap = map[string]bool{} // fix self join many2many + ownFieldsMap = map[string]*Field{} // fix self join many2many + referFieldsMap = map[string]*Field{} joinForeignKeys = toColumns(field.TagSettings["JOINFOREIGNKEY"]) joinReferences = toColumns(field.TagSettings["JOINREFERENCES"]) ) @@ -229,7 +230,7 @@ func (schema *Schema) buildMany2ManyRelation(relation *Relationship, field *Fiel joinFieldName = strings.Title(joinForeignKeys[idx]) } - ownFieldsMap[joinFieldName] = true + ownFieldsMap[joinFieldName] = ownField fieldsMap[joinFieldName] = ownField joinTableFields = append(joinTableFields, reflect.StructField{ Name: joinFieldName, @@ -242,9 +243,6 @@ func (schema *Schema) buildMany2ManyRelation(relation *Relationship, field *Fiel for idx, relField := range refForeignFields { joinFieldName := strings.Title(relation.FieldSchema.Name) + relField.Name - if len(joinReferences) > idx { - joinFieldName = strings.Title(joinReferences[idx]) - } if _, ok := ownFieldsMap[joinFieldName]; ok { if field.Name != relation.FieldSchema.Name { @@ -254,14 +252,22 @@ func (schema *Schema) buildMany2ManyRelation(relation *Relationship, field *Fiel } } - fieldsMap[joinFieldName] = relField - joinTableFields = append(joinTableFields, reflect.StructField{ - Name: joinFieldName, - PkgPath: relField.StructField.PkgPath, - Type: relField.StructField.Type, - Tag: removeSettingFromTag(appendSettingFromTag(relField.StructField.Tag, "primaryKey"), - "column", "autoincrement", "index", "unique", "uniqueindex"), - }) + if len(joinReferences) > idx { + joinFieldName = strings.Title(joinReferences[idx]) + } + + referFieldsMap[joinFieldName] = relField + + if _, ok := fieldsMap[joinFieldName]; !ok { + fieldsMap[joinFieldName] = relField + joinTableFields = append(joinTableFields, reflect.StructField{ + Name: joinFieldName, + PkgPath: relField.StructField.PkgPath, + Type: relField.StructField.Type, + Tag: removeSettingFromTag(appendSettingFromTag(relField.StructField.Tag, "primaryKey"), + "column", "autoincrement", "index", "unique", "uniqueindex"), + }) + } } joinTableFields = append(joinTableFields, reflect.StructField{ @@ -317,31 +323,37 @@ func (schema *Schema) buildMany2ManyRelation(relation *Relationship, field *Fiel f.Size = fieldsMap[f.Name].Size } relation.JoinTable.PrimaryFields = append(relation.JoinTable.PrimaryFields, f) - ownPrimaryField := schema == fieldsMap[f.Name].Schema && ownFieldsMap[f.Name] - if ownPrimaryField { + if of, ok := ownFieldsMap[f.Name]; ok { joinRel := relation.JoinTable.Relationships.Relations[relName] joinRel.Field = relation.Field joinRel.References = append(joinRel.References, &Reference{ - PrimaryKey: fieldsMap[f.Name], + PrimaryKey: of, ForeignKey: f, }) - } else { + + relation.References = append(relation.References, &Reference{ + PrimaryKey: of, + ForeignKey: f, + OwnPrimaryKey: true, + }) + } + + if rf, ok := referFieldsMap[f.Name]; ok { joinRefRel := relation.JoinTable.Relationships.Relations[relRefName] if joinRefRel.Field == nil { joinRefRel.Field = relation.Field } joinRefRel.References = append(joinRefRel.References, &Reference{ - PrimaryKey: fieldsMap[f.Name], + PrimaryKey: rf, ForeignKey: f, }) - } - relation.References = append(relation.References, &Reference{ - PrimaryKey: fieldsMap[f.Name], - ForeignKey: f, - OwnPrimaryKey: ownPrimaryField, - }) + relation.References = append(relation.References, &Reference{ + PrimaryKey: rf, + ForeignKey: f, + }) + } } } } diff --git a/schema/relationship_test.go b/schema/relationship_test.go index 6fffbfcbc..85c45589b 100644 --- a/schema/relationship_test.go +++ b/schema/relationship_test.go @@ -10,7 +10,7 @@ import ( func checkStructRelation(t *testing.T, data interface{}, relations ...Relation) { if s, err := schema.Parse(data, &sync.Map{}, schema.NamingStrategy{}); err != nil { - t.Errorf("Failed to parse schema") + t.Errorf("Failed to parse schema, got error %v", err) } else { for _, rel := range relations { checkSchemaRelation(t, s, rel) @@ -305,6 +305,33 @@ func TestMany2ManyOverrideForeignKey(t *testing.T) { }) } +func TestMany2ManySharedForeignKey(t *testing.T) { + type Profile struct { + gorm.Model + Name string + Kind string + ProfileRefer uint + } + + type User struct { + gorm.Model + Profiles []Profile `gorm:"many2many:user_profiles;foreignKey:Refer,Kind;joinForeignKey:UserRefer,Kind;References:ProfileRefer,Kind;joinReferences:ProfileR,Kind"` + Kind string + Refer uint + } + + checkStructRelation(t, &User{}, Relation{ + Name: "Profiles", Type: schema.Many2Many, Schema: "User", FieldSchema: "Profile", + JoinTable: JoinTable{Name: "user_profiles", Table: "user_profiles"}, + References: []Reference{ + {"Refer", "User", "UserRefer", "user_profiles", "", true}, + {"Kind", "User", "Kind", "user_profiles", "", true}, + {"ProfileRefer", "Profile", "ProfileR", "user_profiles", "", false}, + {"Kind", "Profile", "Kind", "user_profiles", "", false}, + }, + }) +} + func TestMany2ManyOverrideJoinForeignKey(t *testing.T) { type Profile struct { gorm.Model diff --git a/tests/go.mod b/tests/go.mod index 19280434e..ebebabc0b 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -3,17 +3,18 @@ module gorm.io/gorm/tests go 1.16 require ( + github.com/denisenkom/go-mssqldb v0.12.2 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/google/uuid v1.3.0 github.com/jinzhu/now v1.1.5 - github.com/lib/pq v1.10.6 - github.com/mattn/go-sqlite3 v1.14.14 // indirect - golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect - gorm.io/driver/mysql v1.3.5 - gorm.io/driver/postgres v1.3.8 + github.com/lib/pq v1.10.7 + github.com/mattn/go-sqlite3 v1.14.15 // indirect + golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 // indirect + gorm.io/driver/mysql v1.3.6 + gorm.io/driver/postgres v1.3.10 gorm.io/driver/sqlite v1.3.6 gorm.io/driver/sqlserver v1.3.2 - gorm.io/gorm v1.23.8 + gorm.io/gorm v1.23.9 ) replace gorm.io/gorm => ../ From 101a7c789fa2c41f409da439056806756fd8ce22 Mon Sep 17 00:00:00 2001 From: Cr <631807682@qq.com> Date: Thu, 22 Sep 2022 15:51:47 +0800 Subject: [PATCH 33/36] fix: scan array (#5624) Co-authored-by: Jinzhu --- scan.go | 22 +++++++++++++++------- tests/query_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/scan.go b/scan.go index df5a3714b..70cd42843 100644 --- a/scan.go +++ b/scan.go @@ -243,15 +243,18 @@ func Scan(rows Rows, db *DB, mode ScanMode) { switch reflectValue.Kind() { case reflect.Slice, reflect.Array: - var elem reflect.Value - recyclableStruct := reflect.New(reflectValueType) + var ( + elem reflect.Value + recyclableStruct = reflect.New(reflectValueType) + isArrayKind = reflectValue.Kind() == reflect.Array + ) if !update || reflectValue.Len() == 0 { update = false // if the slice cap is externally initialized, the externally initialized slice is directly used here if reflectValue.Cap() == 0 { db.Statement.ReflectValue.Set(reflect.MakeSlice(reflectValue.Type(), 0, 20)) - } else { + } else if !isArrayKind { reflectValue.SetLen(0) db.Statement.ReflectValue.Set(reflectValue) } @@ -285,10 +288,15 @@ func Scan(rows Rows, db *DB, mode ScanMode) { db.scanIntoStruct(rows, elem, values, fields, joinFields) if !update { - if isPtr { - reflectValue = reflect.Append(reflectValue, elem) + if !isPtr { + elem = elem.Elem() + } + if isArrayKind { + if reflectValue.Len() >= int(db.RowsAffected) { + reflectValue.Index(int(db.RowsAffected - 1)).Set(elem) + } } else { - reflectValue = reflect.Append(reflectValue, elem.Elem()) + reflectValue = reflect.Append(reflectValue, elem) } } } @@ -312,4 +320,4 @@ func Scan(rows Rows, db *DB, mode ScanMode) { if db.RowsAffected == 0 && db.Statement.RaiseErrorOnNotFound && db.Error == nil { db.AddError(ErrRecordNotFound) } -} +} \ No newline at end of file diff --git a/tests/query_test.go b/tests/query_test.go index 4569fe1a1..eccf0133d 100644 --- a/tests/query_test.go +++ b/tests/query_test.go @@ -216,6 +216,30 @@ func TestFind(t *testing.T) { } } + // test array + var models2 [3]User + if err := DB.Where("name in (?)", []string{"find"}).Find(&models2).Error; err != nil || len(models2) != 3 { + t.Errorf("errors happened when query find with in clause: %v, length: %v", err, len(models2)) + } else { + for idx, user := range users { + t.Run("FindWithInClause#"+strconv.Itoa(idx+1), func(t *testing.T) { + CheckUser(t, models2[idx], user) + }) + } + } + + // test smaller array + var models3 [2]User + if err := DB.Where("name in (?)", []string{"find"}).Find(&models3).Error; err != nil || len(models3) != 2 { + t.Errorf("errors happened when query find with in clause: %v, length: %v", err, len(models3)) + } else { + for idx, user := range users[:2] { + t.Run("FindWithInClause#"+strconv.Itoa(idx+1), func(t *testing.T) { + CheckUser(t, models3[idx], user) + }) + } + } + var none []User if err := DB.Where("name in (?)", []string{}).Find(&none).Error; err != nil || len(none) != 0 { t.Errorf("errors happened when query find with in clause and zero length parameter: %v, length: %v", err, len(none)) From 73bc53f061ee1f54b9ef562a3466b5e3c5438aea Mon Sep 17 00:00:00 2001 From: Cr <631807682@qq.com> Date: Thu, 22 Sep 2022 15:56:32 +0800 Subject: [PATCH 34/36] feat: migrator support type aliases (#5627) * feat: migrator support type aliases * perf: check type --- migrator.go | 1 + migrator/migrator.go | 29 ++++++++++++++++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/migrator.go b/migrator.go index 34e888f2b..882fc4cc5 100644 --- a/migrator.go +++ b/migrator.go @@ -68,6 +68,7 @@ type Migrator interface { // Database CurrentDatabase() string FullDataTypeOf(*schema.Field) clause.Expr + GetTypeAliases(databaseTypeName string) []string // Tables CreateTable(dst ...interface{}) error diff --git a/migrator/migrator.go b/migrator/migrator.go index d7ebf276e..29c0c00c0 100644 --- a/migrator/migrator.go +++ b/migrator/migrator.go @@ -408,9 +408,27 @@ func (m Migrator) MigrateColumn(value interface{}, field *schema.Field, columnTy alterColumn := false - // check type - if !field.PrimaryKey && !strings.HasPrefix(fullDataType, realDataType) { - alterColumn = true + if !field.PrimaryKey { + // check type + var isSameType bool + if strings.HasPrefix(fullDataType, realDataType) { + isSameType = true + } + + // check type aliases + if !isSameType { + aliases := m.DB.Migrator().GetTypeAliases(realDataType) + for _, alias := range aliases { + if strings.HasPrefix(fullDataType, alias) { + isSameType = true + break + } + } + } + + if !isSameType { + alterColumn = true + } } // check size @@ -863,3 +881,8 @@ func (m Migrator) CurrentTable(stmt *gorm.Statement) interface{} { func (m Migrator) GetIndexes(dst interface{}) ([]gorm.Index, error) { return nil, errors.New("not support") } + +// GetTypeAliases return database type aliases +func (m Migrator) GetTypeAliases(databaseTypeName string) []string { + return nil +} From 12237454ed695461eb750aee9fca6bac7faa8b8b Mon Sep 17 00:00:00 2001 From: kinggo Date: Thu, 22 Sep 2022 16:47:31 +0800 Subject: [PATCH 35/36] fix: use preparestmt in trasaction will use new conn, close #5508 --- gorm.go | 16 ++++++++++++---- tests/prepared_stmt_test.go | 17 +++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/gorm.go b/gorm.go index 1f1dac213..81b6e2afc 100644 --- a/gorm.go +++ b/gorm.go @@ -248,10 +248,18 @@ func (db *DB) Session(config *Session) *DB { if config.PrepareStmt { if v, ok := db.cacheStore.Load(preparedStmtDBKey); ok { preparedStmt := v.(*PreparedStmtDB) - tx.Statement.ConnPool = &PreparedStmtDB{ - ConnPool: db.Config.ConnPool, - Mux: preparedStmt.Mux, - Stmts: preparedStmt.Stmts, + switch t := tx.Statement.ConnPool.(type) { + case Tx: + tx.Statement.ConnPool = &PreparedStmtTX{ + Tx: t, + PreparedStmtDB: preparedStmt, + } + default: + tx.Statement.ConnPool = &PreparedStmtDB{ + ConnPool: db.Config.ConnPool, + Mux: preparedStmt.Mux, + Stmts: preparedStmt.Stmts, + } } txConfig.ConnPool = tx.Statement.ConnPool txConfig.PrepareStmt = true diff --git a/tests/prepared_stmt_test.go b/tests/prepared_stmt_test.go index 8730e5474..86e3630da 100644 --- a/tests/prepared_stmt_test.go +++ b/tests/prepared_stmt_test.go @@ -2,6 +2,7 @@ package tests_test import ( "context" + "errors" "testing" "time" @@ -88,3 +89,19 @@ func TestPreparedStmtFromTransaction(t *testing.T) { } tx2.Commit() } + +func TestPreparedStmtInTransaction(t *testing.T) { + user := User{Name: "jinzhu"} + + if err := DB.Transaction(func(tx *gorm.DB) error { + tx.Session(&gorm.Session{PrepareStmt: true}).Create(&user) + return errors.New("test") + }); err == nil { + t.Error(err) + } + + var result User + if err := DB.First(&result, user.ID).Error; err == nil { + t.Errorf("Failed, got error: %v", err) + } +} From 328f3019825c95be6264cc94d3b4c32fe3cf61d1 Mon Sep 17 00:00:00 2001 From: Nguyen Huu Tuan <54979794+nohattee@users.noreply.github.com> Date: Thu, 22 Sep 2022 17:35:21 +0700 Subject: [PATCH 36/36] add some test case which related the logic (#5477) --- schema/schema.go | 8 +++++++ tests/postgres_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/schema/schema.go b/schema/schema.go index 3791237de..42ff5c456 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -239,6 +239,14 @@ func ParseWithSpecialTableName(dest interface{}, cacheStore *sync.Map, namer Nam field.HasDefaultValue = true field.AutoIncrement = true } + case String: + if _, ok := field.TagSettings["PRIMARYKEY"]; !ok { + if !field.HasDefaultValue || field.DefaultValueInterface != nil { + schema.FieldsWithDefaultDBValue = append(schema.FieldsWithDefaultDBValue, field) + } + + field.HasDefaultValue = true + } } } diff --git a/tests/postgres_test.go b/tests/postgres_test.go index 97af6db3d..b5b672a93 100644 --- a/tests/postgres_test.go +++ b/tests/postgres_test.go @@ -9,6 +9,56 @@ import ( "gorm.io/gorm" ) +func TestPostgresReturningIDWhichHasStringType(t *testing.T) { + if DB.Dialector.Name() != "postgres" { + t.Skip() + } + + type Yasuo struct { + ID string `gorm:"default:gen_random_uuid()"` + Name string + CreatedAt time.Time `gorm:"type:TIMESTAMP WITHOUT TIME ZONE"` + UpdatedAt time.Time `gorm:"type:TIMESTAMP WITHOUT TIME ZONE;default:current_timestamp"` + } + + if err := DB.Exec("CREATE EXTENSION IF NOT EXISTS pgcrypto;").Error; err != nil { + t.Errorf("Failed to create extension pgcrypto, got error %v", err) + } + + DB.Migrator().DropTable(&Yasuo{}) + + if err := DB.AutoMigrate(&Yasuo{}); err != nil { + t.Fatalf("Failed to migrate for uuid default value, got error: %v", err) + } + + yasuo := Yasuo{Name: "jinzhu"} + if err := DB.Create(&yasuo).Error; err != nil { + t.Fatalf("should be able to create data, but got %v", err) + } + + if yasuo.ID == "" { + t.Fatal("should be able to has ID, but got zero value") + } + + var result Yasuo + if err := DB.First(&result, "id = ?", yasuo.ID).Error; err != nil || yasuo.Name != "jinzhu" { + t.Errorf("No error should happen, but got %v", err) + } + + if err := DB.Where("id = $1", yasuo.ID).First(&Yasuo{}).Error; err != nil || yasuo.Name != "jinzhu" { + t.Errorf("No error should happen, but got %v", err) + } + + yasuo.Name = "jinzhu1" + if err := DB.Save(&yasuo).Error; err != nil { + t.Errorf("Failed to update date, got error %v", err) + } + + if err := DB.First(&result, "id = ?", yasuo.ID).Error; err != nil || yasuo.Name != "jinzhu1" { + t.Errorf("No error should happen, but got %v", err) + } +} + func TestPostgres(t *testing.T) { if DB.Dialector.Name() != "postgres" { t.Skip()