From 62fdc2bb3b4f991a8ed1ec2fdb47571a64fd18ef Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 11 Jul 2022 11:51:05 +0800 Subject: [PATCH 01/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 758a6421..21be3c35 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 f3e9d260..7a788a43 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 7232f9df..95d25699 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 02/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 850af6cb..79e29915 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 a89cc7d2..4432cda4 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 03/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 79e29915..aa5c2993 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 4432cda4..19ab38f7 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 04/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 aa5c2993..9a621179 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 19ab38f7..a537c7be 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 05/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 9a621179..12687810 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 06/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 7ee2a576..d1af0739 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 8fe0f289..4e94cfd5 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 c926fbcf..40af6ae7 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 0fc89a93..d7634580 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 07/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 45505cc9..72733326 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 4a50e6c2..00e00fcc 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 5737c511..27dfee16 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 08/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 35e10ddd..06229caa 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 e729e979..42b32afc 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 09/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 7a3f27ba..af9afb63 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 6a6bb032..c852e60c 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 253d8409..4569fe1a 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 10/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 c8b194c3..bcacc7cf 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 11/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 d4dfbd6f..47f3994f 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 312a5c37..e309d06c 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 12/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 eca113e9..3791237d 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 13/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 47f3994f..1589d984 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 21be3c35..00a4f85f 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 7a788a43..eb8f336d 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 95d25699..946536bf 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 14/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 22e8e659..ec1651a3 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 15/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 af9afb63..bdf0437d 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 16/36] fix: correct grammar --- gorm.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gorm.go b/gorm.go index c852e60c..1f1dac21 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 17/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 b97da3f4..367f4ccd 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 18/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 a537c7be..761daf37 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 19/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 57362745..03f84379 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 2ffd28d5..ce088561 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 eb8f336d..19280434 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 20/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 66b988c3..97af6db3 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 21/36] Optimize: code logic db.scanIntoStruct() (#5633) --- scan.go | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/scan.go b/scan.go index 6250fb57..2db43160 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 - } - - 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]) + if field == nil { + continue } + + 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]) } } From b3eb1c8c512430c1600f720a96b2af777c91d1da Mon Sep 17 00:00:00 2001 From: Jiepeng Cao Date: Mon, 5 Sep 2022 15:39:19 +0800 Subject: [PATCH 22/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 87ac7745..c1d7e0e7 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 12687810..cc26fe37 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 f90c4518..e84dc14a 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 23/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 bdf0437d..835a6984 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 24/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 c1d7e0e7..e6782a13 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 25/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 48c61bf4..b596df9a 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 26/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 e6782a13..d7ebf276 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 0b5bc5eb..32e84e77 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 27/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 2db43160..df5a3714 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 28/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 0aa33e51..bb8aeb64 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: rf, ForeignKey: f, }) } - - relation.References = append(relation.References, &Reference{ - PrimaryKey: fieldsMap[f.Name], - ForeignKey: f, - OwnPrimaryKey: ownPrimaryField, - }) } } } diff --git a/schema/relationship_test.go b/schema/relationship_test.go index 6fffbfcb..85c45589 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 19280434..ebebabc0 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 29/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 df5a3714..70cd4284 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 4569fe1a..eccf0133 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 30/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 34e888f2..882fc4cc 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 d7ebf276..29c0c00c 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 31/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 1f1dac21..81b6e2af 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 8730e547..86e3630d 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 32/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 3791237d..42ff5c45 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 97af6db3..b5b672a9 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() From e1dd0dcbc41741e94702d0973df88f4a7afd98e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Sep 2022 11:13:01 +0800 Subject: [PATCH 33/36] chore(deps): bump actions/stale from 5 to 6 (#5717) Bumps [actions/stale](https://github.com/actions/stale) from 5 to 6. - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/stale dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/invalid_question.yml | 2 +- .github/workflows/missing_playground.yml | 2 +- .github/workflows/stale.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/invalid_question.yml b/.github/workflows/invalid_question.yml index aa1812d4..bc4487ae 100644 --- a/.github/workflows/invalid_question.yml +++ b/.github/workflows/invalid_question.yml @@ -16,7 +16,7 @@ jobs: ACTIONS_STEP_DEBUG: true steps: - name: Close Stale Issues - uses: actions/stale@v5 + uses: actions/stale@v6 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: "This issue has been marked as invalid question, please give more information by following the `Question` template, if you believe there is a bug of GORM, please create a pull request that could reproduce the issue on [https://github.com/go-gorm/playground](https://github.com/go-gorm/playground), the issue will be closed in 30 days if no further activity occurs. most likely your question already answered https://github.com/go-gorm/gorm/issues or described in the document https://gorm.io ✨ [Search Before Asking](https://stackoverflow.com/help/how-to-ask) ✨" diff --git a/.github/workflows/missing_playground.yml b/.github/workflows/missing_playground.yml index c3c92beb..f9f51aa0 100644 --- a/.github/workflows/missing_playground.yml +++ b/.github/workflows/missing_playground.yml @@ -16,7 +16,7 @@ jobs: ACTIONS_STEP_DEBUG: true steps: - name: Close Stale Issues - uses: actions/stale@v5 + uses: actions/stale@v6 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: "The issue has been automatically marked as stale as it missing playground pull request link, which is important to help others understand your issue effectively and make sure the issue hasn't been fixed on latest master, checkout [https://github.com/go-gorm/playground](https://github.com/go-gorm/playground) for details. it will be closed in 30 days if no further activity occurs. if you are asking question, please use the `Question` template, most likely your question already answered https://github.com/go-gorm/gorm/issues or described in the document https://gorm.io ✨ [Search Before Asking](https://stackoverflow.com/help/how-to-ask) ✨" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index af8d3636..a9aff43a 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -16,7 +16,7 @@ jobs: ACTIONS_STEP_DEBUG: true steps: - name: Close Stale Issues - uses: actions/stale@v5 + uses: actions/stale@v6 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: "This issue has been automatically marked as stale because it has been open 360 days with no activity. Remove stale label or comment or this will be closed in 180 days" From be440e75122de5f7c19e2242a59246a92ce8edfe Mon Sep 17 00:00:00 2001 From: "jesse.tang" <1430482733@qq.com> Date: Fri, 30 Sep 2022 11:14:34 +0800 Subject: [PATCH 34/36] fix possible nil panic in tests (#5720) * fix maybe nil panic * reset code --- tests/callbacks_test.go | 3 +++ tests/transaction_test.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/callbacks_test.go b/tests/callbacks_test.go index 2bf9496b..4479da4c 100644 --- a/tests/callbacks_test.go +++ b/tests/callbacks_test.go @@ -113,6 +113,9 @@ func TestCallbacks(t *testing.T) { for idx, data := range datas { db, err := gorm.Open(nil, nil) + if err != nil { + t.Fatal(err) + } callbacks := db.Callback() for _, c := range data.callbacks { diff --git a/tests/transaction_test.go b/tests/transaction_test.go index 0ac04a04..5872da94 100644 --- a/tests/transaction_test.go +++ b/tests/transaction_test.go @@ -102,7 +102,7 @@ func TestTransactionWithBlock(t *testing.T) { return errors.New("the error message") }) - if err.Error() != "the error message" { + if err != nil && err.Error() != "the error message" { t.Fatalf("Transaction return error will equal the block returns error") } From a3cc6c6088c1e2aa8cbd174f4714e7fc6d0acd59 Mon Sep 17 00:00:00 2001 From: Stephano George Date: Fri, 30 Sep 2022 17:18:42 +0800 Subject: [PATCH 35/36] Fix: wrong value when Find with Join with same column name, close #5723, #5711 --- scan.go | 31 ++++++++++++++----------------- tests/go.mod | 4 ++-- tests/joins_test.go | 31 +++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/scan.go b/scan.go index 70cd4284..3a753dca 100644 --- a/scan.go +++ b/scan.go @@ -163,11 +163,10 @@ func Scan(rows Rows, db *DB, mode ScanMode) { } default: var ( - fields = make([]*schema.Field, len(columns)) - selectedColumnsMap = make(map[string]int, len(columns)) - joinFields [][2]*schema.Field - sch = db.Statement.Schema - reflectValue = db.Statement.ReflectValue + fields = make([]*schema.Field, len(columns)) + joinFields [][2]*schema.Field + sch = db.Statement.Schema + reflectValue = db.Statement.ReflectValue ) if reflectValue.Kind() == reflect.Interface { @@ -200,26 +199,24 @@ func Scan(rows Rows, db *DB, mode ScanMode) { // Not Pluck if sch != nil { - schFieldsCount := len(sch.Fields) + matchedFieldCount := make(map[string]int, len(columns)) for idx, column := range columns { if field := sch.LookUpField(column); field != nil && field.Readable { - if curIndex, ok := selectedColumnsMap[column]; ok { - fields[idx] = field // handle duplicate fields - offset := curIndex + 1 - // handle sch inconsistent with database - // like Raw(`...`).Scan - if schFieldsCount > offset { - for fieldIndex, selectField := range sch.Fields[offset:] { - if selectField.DBName == column && selectField.Readable { - selectedColumnsMap[column] = curIndex + fieldIndex + 1 + fields[idx] = field + if count, ok := matchedFieldCount[column]; ok { + // handle duplicate fields + for _, selectField := range sch.Fields { + if selectField.DBName == column && selectField.Readable { + if count == 0 { + matchedFieldCount[column]++ fields[idx] = selectField break } + count-- } } } else { - fields[idx] = field - selectedColumnsMap[column] = idx + matchedFieldCount[column] = 1 } } else if names := strings.Split(column, "__"); len(names) > 1 { if rel, ok := sch.Relationships.Relations[names[0]]; ok { diff --git a/tests/go.mod b/tests/go.mod index ebebabc0..c1e1e0ce 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -9,12 +9,12 @@ require ( github.com/jinzhu/now v1.1.5 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 + golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be // 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.9 + gorm.io/gorm v1.23.10 ) replace gorm.io/gorm => ../ diff --git a/tests/joins_test.go b/tests/joins_test.go index 4908e5ba..7519db82 100644 --- a/tests/joins_test.go +++ b/tests/joins_test.go @@ -229,3 +229,34 @@ func TestJoinWithSoftDeleted(t *testing.T) { t.Fatalf("joins NamedPet and Account should not empty:%v", user2) } } + +func TestJoinWithSameColumnName(t *testing.T) { + user := GetUser("TestJoinWithSameColumnName", Config{ + Languages: 1, + Pets: 1, + }) + DB.Create(user) + type UserSpeak struct { + UserID uint + LanguageCode string + } + type Result struct { + User + UserSpeak + Language + Pet + } + + results := make([]Result, 0, 1) + DB.Select("users.*, user_speaks.*, languages.*, pets.*").Table("users").Joins("JOIN user_speaks ON user_speaks.user_id = users.id"). + Joins("JOIN languages ON languages.code = user_speaks.language_code"). + Joins("LEFT OUTER JOIN pets ON pets.user_id = users.id").Find(&results) + + if len(results) == 0 { + t.Fatalf("no record find") + } else if results[0].Pet.UserID == nil || *(results[0].Pet.UserID) != user.ID { + t.Fatalf("wrong user id in pet") + } else if results[0].Pet.Name != user.Pets[0].Name { + t.Fatalf("wrong pet name") + } +} From 0b7113b618584edd76d74e7a73eecc2a28a4d17a Mon Sep 17 00:00:00 2001 From: Cr <631807682@qq.com> Date: Fri, 30 Sep 2022 18:13:36 +0800 Subject: [PATCH 36/36] fix: prepare deadlock (#5568) * fix: prepare deadlock * chore[ci skip]: code style * chore[ci skip]: test remove unnecessary params * fix: prepare deadlock * fix: double check prepare * test: more goroutines * chore[ci skip]: improve code comments Co-authored-by: Jinzhu --- gorm.go | 2 +- prepare_stmt.go | 54 ++++++++++++++++++++++++------- tests/prepared_stmt_test.go | 63 +++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 12 deletions(-) diff --git a/gorm.go b/gorm.go index 81b6e2af..589fc4ff 100644 --- a/gorm.go +++ b/gorm.go @@ -179,7 +179,7 @@ func Open(dialector Dialector, opts ...Option) (db *DB, err error) { preparedStmt := &PreparedStmtDB{ ConnPool: db.ConnPool, - Stmts: map[string]Stmt{}, + Stmts: map[string](*Stmt){}, Mux: &sync.RWMutex{}, PreparedSQL: make([]string, 0, 100), } diff --git a/prepare_stmt.go b/prepare_stmt.go index b062b0d6..3934bb97 100644 --- a/prepare_stmt.go +++ b/prepare_stmt.go @@ -9,10 +9,12 @@ import ( type Stmt struct { *sql.Stmt Transaction bool + prepared chan struct{} + prepareErr error } type PreparedStmtDB struct { - Stmts map[string]Stmt + Stmts map[string]*Stmt PreparedSQL []string Mux *sync.RWMutex ConnPool @@ -46,27 +48,57 @@ func (db *PreparedStmtDB) prepare(ctx context.Context, conn ConnPool, isTransact db.Mux.RLock() if stmt, ok := db.Stmts[query]; ok && (!stmt.Transaction || isTransaction) { db.Mux.RUnlock() - return stmt, nil + // wait for other goroutines prepared + <-stmt.prepared + if stmt.prepareErr != nil { + return Stmt{}, stmt.prepareErr + } + + return *stmt, nil } db.Mux.RUnlock() db.Mux.Lock() - defer db.Mux.Unlock() - // double check if stmt, ok := db.Stmts[query]; ok && (!stmt.Transaction || isTransaction) { - return stmt, nil - } else if ok { - go stmt.Close() + db.Mux.Unlock() + // wait for other goroutines prepared + <-stmt.prepared + if stmt.prepareErr != nil { + return Stmt{}, stmt.prepareErr + } + + return *stmt, nil } + // cache preparing stmt first + cacheStmt := Stmt{Transaction: isTransaction, prepared: make(chan struct{})} + db.Stmts[query] = &cacheStmt + db.Mux.Unlock() + + // prepare completed + defer close(cacheStmt.prepared) + + // Reason why cannot lock conn.PrepareContext + // suppose the maxopen is 1, g1 is creating record and g2 is querying record. + // 1. g1 begin tx, g1 is requeued because of waiting for the system call, now `db.ConnPool` db.numOpen == 1. + // 2. g2 select lock `conn.PrepareContext(ctx, query)`, now db.numOpen == db.maxOpen , wait for release. + // 3. g1 tx exec insert, wait for unlock `conn.PrepareContext(ctx, query)` to finish tx and release. stmt, err := conn.PrepareContext(ctx, query) - if err == nil { - db.Stmts[query] = Stmt{Stmt: stmt, Transaction: isTransaction} - db.PreparedSQL = append(db.PreparedSQL, query) + if err != nil { + cacheStmt.prepareErr = err + db.Mux.Lock() + delete(db.Stmts, query) + db.Mux.Unlock() + return Stmt{}, err } - return db.Stmts[query], err + db.Mux.Lock() + cacheStmt.Stmt = stmt + db.PreparedSQL = append(db.PreparedSQL, query) + db.Mux.Unlock() + + return cacheStmt, nil } func (db *PreparedStmtDB) BeginTx(ctx context.Context, opt *sql.TxOptions) (ConnPool, error) { diff --git a/tests/prepared_stmt_test.go b/tests/prepared_stmt_test.go index 86e3630d..c7f251f2 100644 --- a/tests/prepared_stmt_test.go +++ b/tests/prepared_stmt_test.go @@ -2,6 +2,7 @@ package tests_test import ( "context" + "sync" "errors" "testing" "time" @@ -90,6 +91,68 @@ func TestPreparedStmtFromTransaction(t *testing.T) { tx2.Commit() } +func TestPreparedStmtDeadlock(t *testing.T) { + tx, err := OpenTestConnection() + AssertEqual(t, err, nil) + + sqlDB, _ := tx.DB() + sqlDB.SetMaxOpenConns(1) + + tx = tx.Session(&gorm.Session{PrepareStmt: true}) + + wg := sync.WaitGroup{} + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + user := User{Name: "jinzhu"} + tx.Create(&user) + + var result User + tx.First(&result) + wg.Done() + }() + } + wg.Wait() + + conn, ok := tx.ConnPool.(*gorm.PreparedStmtDB) + AssertEqual(t, ok, true) + AssertEqual(t, len(conn.Stmts), 2) + for _, stmt := range conn.Stmts { + if stmt == nil { + t.Fatalf("stmt cannot bee nil") + } + } + + AssertEqual(t, sqlDB.Stats().InUse, 0) +} + +func TestPreparedStmtError(t *testing.T) { + tx, err := OpenTestConnection() + AssertEqual(t, err, nil) + + sqlDB, _ := tx.DB() + sqlDB.SetMaxOpenConns(1) + + tx = tx.Session(&gorm.Session{PrepareStmt: true}) + + wg := sync.WaitGroup{} + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + // err prepare + tag := Tag{Locale: "zh"} + tx.Table("users").Find(&tag) + wg.Done() + }() + } + wg.Wait() + + conn, ok := tx.ConnPool.(*gorm.PreparedStmtDB) + AssertEqual(t, ok, true) + AssertEqual(t, len(conn.Stmts), 0) + AssertEqual(t, sqlDB.Stats().InUse, 0) +} + func TestPreparedStmtInTransaction(t *testing.T) { user := User{Name: "jinzhu"}