From 80497f27a61df4daea49b1ec1bb1d473459fa28f Mon Sep 17 00:00:00 2001 From: wangyuehong Date: Tue, 13 Jul 2021 17:36:22 +0900 Subject: [PATCH 01/51] title foreign schema for many2many to avoid panic (#4496) Co-authored-by: yuehong.wang --- schema/relationship.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema/relationship.go b/schema/relationship.go index db496e30..84556bae 100644 --- a/schema/relationship.go +++ b/schema/relationship.go @@ -238,7 +238,7 @@ func (schema *Schema) buildMany2ManyRelation(relation *Relationship, field *Fiel } for idx, relField := range refForeignFields { - joinFieldName := relation.FieldSchema.Name + relField.Name + joinFieldName := strings.Title(relation.FieldSchema.Name) + relField.Name if len(joinReferences) > idx { joinFieldName = strings.Title(joinReferences[idx]) } From 0329b800b0d174009fba5acd2d6e2603ae566dbb Mon Sep 17 00:00:00 2001 From: Burak Demirpolat <44942068+bdemirpolat@users.noreply.github.com> Date: Tue, 13 Jul 2021 11:38:44 +0300 Subject: [PATCH 02/51] slightly better callback warning (#4495) --- schema/schema.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema/schema.go b/schema/schema.go index 8ade2ed7..4d5b7346 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -228,7 +228,7 @@ func Parse(dest interface{}, cacheStore *sync.Map, namer Namer) (*Schema, error) case "func(*gorm.DB) error": // TODO hack reflect.Indirect(reflect.ValueOf(schema)).FieldByName(name).SetBool(true) default: - logger.Default.Warn(context.Background(), "Model %v don't match %vInterface, should be %v(*gorm.DB)", schema, name, name) + logger.Default.Warn(context.Background(), "Model %v don't match %vInterface, should be `%v(*gorm.DB) error`. Please see https://gorm.io/docs/hooks.html", schema, name, name) } } } From 2ec7043818f88fcf548b2268bad01a95fdc12351 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Tue, 13 Jul 2021 18:04:42 +0800 Subject: [PATCH 03/51] Respect update permission for OnConflict Create --- callbacks/create.go | 16 ++++++++-------- tests/upsert_test.go | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/callbacks/create.go b/callbacks/create.go index 04ee6b30..2ebe5cab 100644 --- a/callbacks/create.go +++ b/callbacks/create.go @@ -37,7 +37,6 @@ func Create(config *Config) func(db *gorm.DB) { return func(db *gorm.DB) { if db.Error != nil { - // maybe record logger TODO return } @@ -64,11 +63,9 @@ func Create(config *Config) func(db *gorm.DB) { } db.RowsAffected, _ = result.RowsAffected() - if !(db.RowsAffected > 0) { - return - } - if db.Statement.Schema != nil && db.Statement.Schema.PrioritizedPrimaryField != nil && db.Statement.Schema.PrioritizedPrimaryField.HasDefaultValue { + if db.RowsAffected != 0 && db.Statement.Schema != nil && + db.Statement.Schema.PrioritizedPrimaryField != nil && db.Statement.Schema.PrioritizedPrimaryField.HasDefaultValue { if insertID, err := result.LastInsertId(); err == nil && insertID > 0 { switch db.Statement.ReflectValue.Kind() { case reflect.Slice, reflect.Array: @@ -107,7 +104,6 @@ func Create(config *Config) func(db *gorm.DB) { db.AddError(err) } } - } } } @@ -349,11 +345,15 @@ func ConvertToCreateValues(stmt *gorm.Statement) (values clause.Values) { if c, ok := stmt.Clauses["ON CONFLICT"]; ok { if onConflict, _ := c.Expression.(clause.OnConflict); onConflict.UpdateAll { if stmt.Schema != nil && len(values.Columns) > 1 { + selectColumns, restricted := stmt.SelectAndOmitColumns(true, true) + columns := make([]string, 0, len(values.Columns)-1) for _, column := range values.Columns { if field := stmt.Schema.LookUpField(column.Name); field != nil { - if !field.PrimaryKey && (!field.HasDefaultValue || field.DefaultValueInterface != nil) && field.AutoCreateTime == 0 { - columns = append(columns, column.Name) + if v, ok := selectColumns[field.DBName]; (ok && v) || (!ok && !restricted) { + if !field.PrimaryKey && (!field.HasDefaultValue || field.DefaultValueInterface != nil) && field.AutoCreateTime == 0 { + columns = append(columns, column.Name) + } } } } diff --git a/tests/upsert_test.go b/tests/upsert_test.go index 0ba8b9f0..867110d8 100644 --- a/tests/upsert_test.go +++ b/tests/upsert_test.go @@ -1,9 +1,11 @@ package tests_test import ( + "regexp" "testing" "time" + "gorm.io/gorm" "gorm.io/gorm/clause" . "gorm.io/gorm/utils/tests" ) @@ -51,6 +53,19 @@ func TestUpsert(t *testing.T) { if err := DB.Find(&result, "code = ?", lang.Code).Error; err != nil || result.Name != lang.Name { t.Fatalf("failed to upsert, got name %v", result.Name) } + + if name := DB.Dialector.Name(); name != "sqlserver" { + type RestrictedLanguage struct { + Code string `gorm:"primarykey"` + Name string + Lang string `gorm:"<-:create"` + } + + 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()) { + t.Errorf("Table with escape character, got %v", r.Statement.SQL.String()) + } + } } func TestUpsertSlice(t *testing.T) { From 76cd73cb82f9aa046cd1efa0f718a74bbf0d993f Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Tue, 13 Jul 2021 18:48:43 +0800 Subject: [PATCH 04/51] Fix wipes out MySQL global variables from the query, close #4515 --- clause/expression.go | 7 ++++++- clause/expression_test.go | 15 ++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/clause/expression.go b/clause/expression.go index 2bdd4a30..a177c5d8 100644 --- a/clause/expression.go +++ b/clause/expression.go @@ -173,7 +173,12 @@ func (expr NamedExpr) Build(builder Builder) { } if inName { - builder.AddVar(builder, namedMap[string(name)]) + if nv, ok := namedMap[string(name)]; ok { + builder.AddVar(builder, nv) + } else { + builder.WriteByte('@') + builder.WriteString(string(name)) + } } } diff --git a/clause/expression_test.go b/clause/expression_test.go index 1c8217ed..0ccd0771 100644 --- a/clause/expression_test.go +++ b/clause/expression_test.go @@ -60,6 +60,11 @@ func TestNamedExpr(t *testing.T) { Vars: []interface{}{sql.Named("name", "jinzhu")}, Result: "name1 = ? AND name2 = ?", ExpectedVars: []interface{}{"jinzhu", "jinzhu"}, + }, { + SQL: "name1 = @name AND name2 = @@name", + Vars: []interface{}{map[string]interface{}{"name": "jinzhu"}}, + Result: "name1 = ? AND name2 = @@name", + ExpectedVars: []interface{}{"jinzhu"}, }, { SQL: "name1 = @name1 AND name2 = @name2 AND name3 = @name1", Vars: []interface{}{sql.Named("name1", "jinzhu"), sql.Named("name2", "jinzhu2")}, @@ -73,13 +78,13 @@ func TestNamedExpr(t *testing.T) { }, { SQL: "@@test AND name1 = @name1 AND name2 = @name2 AND name3 = @name1 @notexist", Vars: []interface{}{sql.Named("name1", "jinzhu"), sql.Named("name2", "jinzhu2")}, - Result: "@@test AND name1 = ? AND name2 = ? AND name3 = ? ?", - ExpectedVars: []interface{}{"jinzhu", "jinzhu2", "jinzhu", nil}, + Result: "@@test AND name1 = ? AND name2 = ? AND name3 = ? @notexist", + ExpectedVars: []interface{}{"jinzhu", "jinzhu2", "jinzhu"}, }, { - SQL: "@@test AND name1 = @Name1 AND name2 = @Name2 AND name3 = @Name1 @Notexist", + SQL: "@@test AND name1 = @Name1 AND name2 = @Name2 AND name3 = @Name1 @notexist", Vars: []interface{}{NamedArgument{Name1: "jinzhu", Base: Base{Name2: "jinzhu2"}}}, - Result: "@@test AND name1 = ? AND name2 = ? AND name3 = ? ?", - ExpectedVars: []interface{}{"jinzhu", "jinzhu2", "jinzhu", nil}, + Result: "@@test AND name1 = ? AND name2 = ? AND name3 = ? @notexist", + ExpectedVars: []interface{}{"jinzhu", "jinzhu2", "jinzhu"}, }, { SQL: "create table ? (? ?, ? ?)", Vars: []interface{}{}, From b616d810eb43678ec37d078b1ffb633416003764 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Tue, 13 Jul 2021 19:29:10 +0800 Subject: [PATCH 05/51] Fix scan single value to custom type, close #4501 --- scan.go | 2 ++ tests/scan_test.go | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/scan.go b/scan.go index e82e3f07..c4f88cf8 100644 --- a/scan.go +++ b/scan.go @@ -238,6 +238,8 @@ func Scan(rows *sql.Rows, db *DB, initialized bool) { } } } + default: + db.AddError(rows.Scan(dest)) } } diff --git a/tests/scan_test.go b/tests/scan_test.go index 86cb0399..67d5f385 100644 --- a/tests/scan_test.go +++ b/tests/scan_test.go @@ -63,6 +63,13 @@ func TestScan(t *testing.T) { if len(results) != 2 || results[0].Name != user2.Name || results[1].Name != user3.Name { t.Errorf("Scan into struct map, got %#v", results) } + + type ID uint64 + var id ID + DB.Raw("select id from users where id = ?", user2.ID).Scan(&id) + if uint(id) != user2.ID { + t.Errorf("Failed to scan to customized data type") + } } func TestScanRows(t *testing.T) { From c73fe96cfdba8abc2b164a7cf0ec644db8e5e65a Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Tue, 13 Jul 2021 19:59:31 +0800 Subject: [PATCH 06/51] Fix scan into decimal.Decimal, close #4457 --- scan.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scan.go b/scan.go index c4f88cf8..2beecd45 100644 --- a/scan.go +++ b/scan.go @@ -208,6 +208,8 @@ func Scan(rows *sql.Rows, db *DB, initialized bool) { } } values[idx] = &sql.RawBytes{} + } else if len(columns) == 1 { + values[idx] = dest } else { values[idx] = &sql.RawBytes{} } From b13732c450770779dde472dd71da3344461d9602 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Tue, 13 Jul 2021 20:23:05 +0800 Subject: [PATCH 07/51] Fix invalid preload SQL when no data found, close #4443 --- callbacks/preload.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/callbacks/preload.go b/callbacks/preload.go index 25c5e659..47986ff1 100644 --- a/callbacks/preload.go +++ b/callbacks/preload.go @@ -104,15 +104,17 @@ func preload(db *gorm.DB, rel *schema.Relationship, conds []interface{}, preload reflectResults := rel.FieldSchema.MakeSlice().Elem() column, values := schema.ToQueryValues(clause.CurrentTable, relForeignKeys, foreignValues) - for _, cond := range conds { - if fc, ok := cond.(func(*gorm.DB) *gorm.DB); ok { - tx = fc(tx) - } else { - inlineConds = append(inlineConds, cond) + if len(values) != 0 { + for _, cond := range conds { + if fc, ok := cond.(func(*gorm.DB) *gorm.DB); ok { + tx = fc(tx) + } else { + inlineConds = append(inlineConds, cond) + } } - } - db.AddError(tx.Where(clause.IN{Column: column, Values: values}).Find(reflectResults.Addr().Interface(), inlineConds...).Error) + db.AddError(tx.Where(clause.IN{Column: column, Values: values}).Find(reflectResults.Addr().Interface(), inlineConds...).Error) + } fieldValues := make([]interface{}, len(relForeignFields)) From 52b72d7ef265a83a6a6a4aefb8b2ac3d91096be6 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Tue, 13 Jul 2021 21:00:13 +0800 Subject: [PATCH 08/51] Add error explanations when preloading assocations w/o foreign fields, close #4356 --- callbacks/preload.go | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/callbacks/preload.go b/callbacks/preload.go index 47986ff1..9882590c 100644 --- a/callbacks/preload.go +++ b/callbacks/preload.go @@ -1,6 +1,7 @@ package callbacks import ( + "fmt" "reflect" "gorm.io/gorm" @@ -144,23 +145,27 @@ func preload(db *gorm.DB, rel *schema.Relationship, conds []interface{}, preload fieldValues[idx], _ = field.ValueOf(elem) } - for _, data := range identityMap[utils.ToStringKey(fieldValues...)] { - reflectFieldValue := rel.Field.ReflectValueOf(data) - if reflectFieldValue.Kind() == reflect.Ptr && reflectFieldValue.IsNil() { - reflectFieldValue.Set(reflect.New(rel.Field.FieldType.Elem())) - } + if datas, ok := identityMap[utils.ToStringKey(fieldValues...)]; ok { + for _, data := range datas { + reflectFieldValue := rel.Field.ReflectValueOf(data) + if reflectFieldValue.Kind() == reflect.Ptr && reflectFieldValue.IsNil() { + reflectFieldValue.Set(reflect.New(rel.Field.FieldType.Elem())) + } - reflectFieldValue = reflect.Indirect(reflectFieldValue) - switch reflectFieldValue.Kind() { - case reflect.Struct: - rel.Field.Set(data, reflectResults.Index(i).Interface()) - case reflect.Slice, reflect.Array: - if reflectFieldValue.Type().Elem().Kind() == reflect.Ptr { - rel.Field.Set(data, reflect.Append(reflectFieldValue, elem).Interface()) - } else { - rel.Field.Set(data, reflect.Append(reflectFieldValue, elem.Elem()).Interface()) + reflectFieldValue = reflect.Indirect(reflectFieldValue) + switch reflectFieldValue.Kind() { + case reflect.Struct: + rel.Field.Set(data, reflectResults.Index(i).Interface()) + case reflect.Slice, reflect.Array: + if reflectFieldValue.Type().Elem().Kind() == reflect.Ptr { + rel.Field.Set(data, reflect.Append(reflectFieldValue, elem).Interface()) + } else { + rel.Field.Set(data, reflect.Append(reflectFieldValue, elem.Elem()).Interface()) + } } } + } else { + db.AddError(fmt.Errorf("failed to assign association %#v, make sure foreign fields exists", elem.Interface())) } } } From 83530ec65950f0731b895ca7ee8e89b1a29c7aa8 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Tue, 13 Jul 2021 21:17:43 +0800 Subject: [PATCH 09/51] Fix delete order by clause when counting, close #4478 --- finisher_api.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/finisher_api.go b/finisher_api.go index 51f394b4..537c955a 100644 --- a/finisher_api.go +++ b/finisher_api.go @@ -376,7 +376,7 @@ func (db *DB) Count(count *int64) (tx *DB) { if selectClause, ok := db.Statement.Clauses["SELECT"]; ok { defer func() { - db.Statement.Clauses["SELECT"] = selectClause + tx.Statement.Clauses["SELECT"] = selectClause }() } else { defer delete(tx.Statement.Clauses, "SELECT") @@ -410,9 +410,9 @@ func (db *DB) Count(count *int64) (tx *DB) { if orderByClause, ok := db.Statement.Clauses["ORDER BY"]; ok { if _, ok := db.Statement.Clauses["GROUP BY"]; !ok { - delete(db.Statement.Clauses, "ORDER BY") + delete(tx.Statement.Clauses, "ORDER BY") defer func() { - db.Statement.Clauses["ORDER BY"] = orderByClause + tx.Statement.Clauses["ORDER BY"] = orderByClause }() } } From d4f3c109d6d6f2d0f4ae3780f7a74457bfd4a28a Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Tue, 13 Jul 2021 21:29:31 +0800 Subject: [PATCH 10/51] Fix OnConflict with one column, close #4370 --- callbacks/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/callbacks/create.go b/callbacks/create.go index 2ebe5cab..8a3c593c 100644 --- a/callbacks/create.go +++ b/callbacks/create.go @@ -344,7 +344,7 @@ func ConvertToCreateValues(stmt *gorm.Statement) (values clause.Values) { if c, ok := stmt.Clauses["ON CONFLICT"]; ok { if onConflict, _ := c.Expression.(clause.OnConflict); onConflict.UpdateAll { - if stmt.Schema != nil && len(values.Columns) > 1 { + if stmt.Schema != nil && len(values.Columns) >= 1 { selectColumns, restricted := stmt.SelectAndOmitColumns(true, true) columns := make([]string, 0, len(values.Columns)-1) From ac97aec51344986339b6e905c83386de81888715 Mon Sep 17 00:00:00 2001 From: River Date: Wed, 14 Jul 2021 15:51:24 +0800 Subject: [PATCH 11/51] New Comma Expression (#4524) * Add new comma expression * Add comma expression unit test --- clause/select.go | 14 ++++++++++++++ clause/select_test.go | 12 ++++++++++++ 2 files changed, 26 insertions(+) diff --git a/clause/select.go b/clause/select.go index b93b8769..d8e9f801 100644 --- a/clause/select.go +++ b/clause/select.go @@ -43,3 +43,17 @@ func (s Select) MergeClause(clause *Clause) { clause.Expression = s } } + +// CommaExpression represents a group of expressions separated by commas. +type CommaExpression struct { + Exprs []Expression +} + +func (comma CommaExpression) Build(builder Builder) { + for idx, expr := range comma.Exprs { + if idx > 0 { + _, _ = builder.WriteString(", ") + } + expr.Build(builder) + } +} diff --git a/clause/select_test.go b/clause/select_test.go index b7296434..9fce0783 100644 --- a/clause/select_test.go +++ b/clause/select_test.go @@ -31,6 +31,18 @@ func TestSelect(t *testing.T) { }, clause.From{}}, "SELECT `name` FROM `users`", nil, }, + { + []clause.Interface{clause.Select{ + Expression: clause.CommaExpression{ + Exprs: []clause.Expression{ + clause.NamedExpr{"?", []interface{}{clause.Column{Name: "id"}}}, + clause.NamedExpr{"?", []interface{}{clause.Column{Name: "name"}}}, + clause.NamedExpr{"LENGTH(?)", []interface{}{clause.Column{Name: "mobile"}}}, + }, + }, + }, clause.From{}}, + "SELECT `id`, `name`, LENGTH(`mobile`) FROM `users`", nil, + }, } for idx, result := range results { From 74752018dcf9c07d95dace4bb2b98b3b169fad0f Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Wed, 14 Jul 2021 18:31:50 +0800 Subject: [PATCH 12/51] Fix hang when closing a prepared statement --- prepare_stmt.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/prepare_stmt.go b/prepare_stmt.go index 48a614b7..5faea995 100644 --- a/prepare_stmt.go +++ b/prepare_stmt.go @@ -35,7 +35,7 @@ func (db *PreparedStmtDB) Close() { for _, query := range db.PreparedSQL { if stmt, ok := db.Stmts[query]; ok { delete(db.Stmts, query) - stmt.Close() + go stmt.Close() } } @@ -56,7 +56,7 @@ func (db *PreparedStmtDB) prepare(ctx context.Context, conn ConnPool, isTransact db.Mux.Unlock() return stmt, nil } else if ok { - stmt.Close() + go stmt.Close() } stmt, err := conn.PrepareContext(ctx, query) @@ -83,7 +83,7 @@ func (db *PreparedStmtDB) ExecContext(ctx context.Context, query string, args .. result, err = stmt.ExecContext(ctx, args...) if err != nil { db.Mux.Lock() - stmt.Close() + go stmt.Close() delete(db.Stmts, query) db.Mux.Unlock() } @@ -97,7 +97,7 @@ func (db *PreparedStmtDB) QueryContext(ctx context.Context, query string, args . rows, err = stmt.QueryContext(ctx, args...) if err != nil { db.Mux.Lock() - stmt.Close() + go stmt.Close() delete(db.Stmts, query) db.Mux.Unlock() } @@ -138,7 +138,7 @@ func (tx *PreparedStmtTX) ExecContext(ctx context.Context, query string, args .. result, err = tx.Tx.StmtContext(ctx, stmt.Stmt).ExecContext(ctx, args...) if err != nil { tx.PreparedStmtDB.Mux.Lock() - stmt.Close() + go stmt.Close() delete(tx.PreparedStmtDB.Stmts, query) tx.PreparedStmtDB.Mux.Unlock() } @@ -152,7 +152,7 @@ func (tx *PreparedStmtTX) QueryContext(ctx context.Context, query string, args . rows, err = tx.Tx.Stmt(stmt.Stmt).QueryContext(ctx, args...) if err != nil { tx.PreparedStmtDB.Mux.Lock() - stmt.Close() + go stmt.Close() delete(tx.PreparedStmtDB.Stmts, query) tx.PreparedStmtDB.Mux.Unlock() } From a70254609dbfbd12539e7216e415b734d1b09115 Mon Sep 17 00:00:00 2001 From: daheige Date: Wed, 14 Jul 2021 22:03:17 +0800 Subject: [PATCH 13/51] optimize setupValuerAndSetter func --- schema/field.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/schema/field.go b/schema/field.go index 9efaa44a..ce0e3c13 100644 --- a/schema/field.go +++ b/schema/field.go @@ -490,21 +490,22 @@ func (field *Field) setupValuerAndSetter() { return } else if field.FieldType.Kind() == reflect.Ptr { fieldValue := field.ReflectValueOf(value) + fieldType := field.FieldType.Elem() - if reflectValType.AssignableTo(field.FieldType.Elem()) { + if reflectValType.AssignableTo(fieldType) { if !fieldValue.IsValid() { - fieldValue = reflect.New(field.FieldType.Elem()) + fieldValue = reflect.New(fieldType) } else if fieldValue.IsNil() { - fieldValue.Set(reflect.New(field.FieldType.Elem())) + fieldValue.Set(reflect.New(fieldType)) } fieldValue.Elem().Set(reflectV) return - } else if reflectValType.ConvertibleTo(field.FieldType.Elem()) { + } else if reflectValType.ConvertibleTo(fieldType) { if fieldValue.IsNil() { - fieldValue.Set(reflect.New(field.FieldType.Elem())) + fieldValue.Set(reflect.New(fieldType)) } - fieldValue.Elem().Set(reflectV.Convert(field.FieldType.Elem())) + fieldValue.Elem().Set(reflectV.Convert(fieldType)) return } } @@ -520,7 +521,7 @@ func (field *Field) setupValuerAndSetter() { err = setter(value, v) } } else { - return fmt.Errorf("failed to set value %+v to field %v", v, field.Name) + return fmt.Errorf("failed to set value %+v to field %s", v, field.Name) } } From 2202e99cbf0f43c35315fe4d17b87ac81f0f2d23 Mon Sep 17 00:00:00 2001 From: s-takehana Date: Sun, 18 Jul 2021 12:47:44 +0900 Subject: [PATCH 14/51] Fix create index with comments in MySQL (#4521) * Fix create index with comments in MySQL * Fix tests --- migrator/migrator.go | 8 ++++++++ tests/migrate_test.go | 29 ++++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/migrator/migrator.go b/migrator/migrator.go index 03ffdd02..7c7405b3 100644 --- a/migrator/migrator.go +++ b/migrator/migrator.go @@ -195,6 +195,10 @@ func (m Migrator) CreateTable(values ...interface{}) error { } createTableSQL += "INDEX ? ?" + if idx.Comment != "" { + createTableSQL += fmt.Sprintf(" COMMENT '%s'", idx.Comment) + } + if idx.Option != "" { createTableSQL += " " + idx.Option } @@ -601,6 +605,10 @@ func (m Migrator) CreateIndex(value interface{}, name string) error { createIndexSQL += " USING " + idx.Type } + if idx.Comment != "" { + createIndexSQL += fmt.Sprintf(" COMMENT '%s'", idx.Comment) + } + if idx.Option != "" { createIndexSQL += " " + idx.Option } diff --git a/tests/migrate_test.go b/tests/migrate_test.go index 4da3856f..599ca850 100644 --- a/tests/migrate_test.go +++ b/tests/migrate_test.go @@ -142,17 +142,36 @@ func TestSmartMigrateColumn(t *testing.T) { } -func TestMigrateWithComment(t *testing.T) { - type UserWithComment struct { +func TestMigrateWithColumnComment(t *testing.T) { + type UserWithColumnComment struct { gorm.Model - Name string `gorm:"size:111;index:,comment:这是一个index;comment:this is a 字段"` + Name string `gorm:"size:111;comment:this is a 字段"` } - if err := DB.Migrator().DropTable(&UserWithComment{}); err != nil { + if err := DB.Migrator().DropTable(&UserWithColumnComment{}); err != nil { t.Fatalf("Failed to drop table, got error %v", err) } - if err := DB.AutoMigrate(&UserWithComment{}); err != nil { + if err := DB.AutoMigrate(&UserWithColumnComment{}); err != nil { + t.Fatalf("Failed to auto migrate, but got error %v", err) + } +} + +func TestMigrateWithIndexComment(t *testing.T) { + if DB.Dialector.Name() != "mysql" { + t.Skip() + } + + type UserWithIndexComment struct { + gorm.Model + Name string `gorm:"size:111;index:,comment:这是一个index"` + } + + if err := DB.Migrator().DropTable(&UserWithIndexComment{}); err != nil { + t.Fatalf("Failed to drop table, got error %v", err) + } + + if err := DB.AutoMigrate(&UserWithIndexComment{}); err != nil { t.Fatalf("Failed to auto migrate, but got error %v", err) } } From 5115813c50450869848e6cb23daa9ba827793535 Mon Sep 17 00:00:00 2001 From: heige Date: Wed, 28 Jul 2021 18:50:08 +0800 Subject: [PATCH 15/51] Fix preload fmt.Errorf formatter (#4531) --- callbacks/query.go | 2 +- migrator/migrator.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/callbacks/query.go b/callbacks/query.go index d0341284..3299d015 100644 --- a/callbacks/query.go +++ b/callbacks/query.go @@ -209,7 +209,7 @@ func Preload(db *gorm.DB) { if rel := db.Statement.Schema.Relationships.Relations[name]; rel != nil { preload(db, rel, db.Statement.Preloads[name], preloadMap[name]) } else { - db.AddError(fmt.Errorf("%v: %w for schema %v", name, gorm.ErrUnsupportedRelation, db.Statement.Schema.Name)) + db.AddError(fmt.Errorf("%s: %w for schema %s", name, gorm.ErrUnsupportedRelation, db.Statement.Schema.Name)) } } } diff --git a/migrator/migrator.go b/migrator/migrator.go index 7c7405b3..b42a62ca 100644 --- a/migrator/migrator.go +++ b/migrator/migrator.go @@ -616,7 +616,7 @@ func (m Migrator) CreateIndex(value interface{}, name string) error { return m.DB.Exec(createIndexSQL, values...).Error } - return fmt.Errorf("failed to create index with name %v", name) + return fmt.Errorf("failed to create index with name %s", name) }) } From 41ac73b6a1e89e72e59b61b77815ee690af71fb8 Mon Sep 17 00:00:00 2001 From: daheige Date: Wed, 14 Jul 2021 21:56:58 +0800 Subject: [PATCH 16/51] update comment for ConvertSliceOfMapToValuesForCreate func --- callbacks/helper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/callbacks/helper.go b/callbacks/helper.go index ad85a1c6..d83d20ce 100644 --- a/callbacks/helper.go +++ b/callbacks/helper.go @@ -44,7 +44,7 @@ func ConvertSliceOfMapToValuesForCreate(stmt *gorm.Statement, mapValues []map[st columns = make([]string, 0, len(mapValues)) ) - // when the length of mapValues,return directly here + // when the length of mapValues is zero,return directly here // no need to call stmt.SelectAndOmitColumns method if len(mapValues) == 0 { stmt.AddError(gorm.ErrEmptySlice) From 7a49629fd1c7c35bd76df5016cd4193bf3db7d81 Mon Sep 17 00:00:00 2001 From: daheige Date: Wed, 14 Jul 2021 21:45:23 +0800 Subject: [PATCH 17/51] optimize Parse func for fieldValue.Interface --- schema/schema.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/schema/schema.go b/schema/schema.go index 4d5b7346..0e0501d4 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -244,19 +244,20 @@ func Parse(dest interface{}, cacheStore *sync.Map, namer Namer) (*Schema, error) } fieldValue := reflect.New(field.IndirectFieldType) - if fc, ok := fieldValue.Interface().(CreateClausesInterface); ok { + fieldInterface := fieldValue.Interface() + if fc, ok := fieldInterface.(CreateClausesInterface); ok { field.Schema.CreateClauses = append(field.Schema.CreateClauses, fc.CreateClauses(field)...) } - if fc, ok := fieldValue.Interface().(QueryClausesInterface); ok { + if fc, ok := fieldInterface.(QueryClausesInterface); ok { field.Schema.QueryClauses = append(field.Schema.QueryClauses, fc.QueryClauses(field)...) } - if fc, ok := fieldValue.Interface().(UpdateClausesInterface); ok { + if fc, ok := fieldInterface.(UpdateClausesInterface); ok { field.Schema.UpdateClauses = append(field.Schema.UpdateClauses, fc.UpdateClauses(field)...) } - if fc, ok := fieldValue.Interface().(DeleteClausesInterface); ok { + if fc, ok := fieldInterface.(DeleteClausesInterface); ok { field.Schema.DeleteClauses = append(field.Schema.DeleteClauses, fc.DeleteClauses(field)...) } } From 413fe587c643109206d72736377eb28dde6b9555 Mon Sep 17 00:00:00 2001 From: heige Date: Mon, 2 Aug 2021 18:44:10 +0800 Subject: [PATCH 18/51] Optimize migrator.go MigrateColumn and ColumnTypes func. (#4532) --- migrator/migrator.go | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/migrator/migrator.go b/migrator/migrator.go index b42a62ca..80d58efd 100644 --- a/migrator/migrator.go +++ b/migrator/migrator.go @@ -2,6 +2,7 @@ package migrator import ( "context" + "database/sql" "fmt" "reflect" "regexp" @@ -386,11 +387,11 @@ func (m Migrator) MigrateColumn(value interface{}, field *schema.Field, columnTy alterColumn = true } else { // has size in data type and not equal - // Since the following code is frequently called in the for loop, reg optimization is needed here matches := regRealDataType.FindAllStringSubmatch(realDataType, -1) matches2 := regFullDataType.FindAllStringSubmatch(fullDataType, -1) - if (len(matches) == 1 && matches[0][1] != fmt.Sprint(field.Size) || !field.PrimaryKey) && (len(matches2) == 1 && matches2[0][1] != fmt.Sprint(length)) { + if (len(matches) == 1 && matches[0][1] != fmt.Sprint(field.Size) || !field.PrimaryKey) && + (len(matches2) == 1 && matches2[0][1] != fmt.Sprint(length)) { alterColumn = true } } @@ -418,22 +419,31 @@ func (m Migrator) MigrateColumn(value interface{}, field *schema.Field, columnTy return nil } -func (m Migrator) ColumnTypes(value interface{}) (columnTypes []gorm.ColumnType, err error) { - columnTypes = make([]gorm.ColumnType, 0) - err = m.RunWithValue(value, func(stmt *gorm.Statement) error { +// ColumnTypes return columnTypes []gorm.ColumnType and execErr error +func (m Migrator) ColumnTypes(value interface{}) ([]gorm.ColumnType, error) { + columnTypes := make([]gorm.ColumnType, 0) + execErr := m.RunWithValue(value, func(stmt *gorm.Statement) error { rows, err := m.DB.Session(&gorm.Session{}).Table(stmt.Table).Limit(1).Rows() - if err == nil { - defer rows.Close() - rawColumnTypes, err := rows.ColumnTypes() - if err == nil { - for _, c := range rawColumnTypes { - columnTypes = append(columnTypes, c) - } - } + if err != nil { + return err } - return err + + defer rows.Close() + + var rawColumnTypes []*sql.ColumnType + rawColumnTypes, err = rows.ColumnTypes() + if err != nil { + return err + } + + for _, c := range rawColumnTypes { + columnTypes = append(columnTypes, c) + } + + return nil }) - return + + return columnTypes, execErr } func (m Migrator) CreateView(name string, option gorm.ViewOption) error { From 9e5a4e30b4045ea663b8c03a57ddefd9673f2356 Mon Sep 17 00:00:00 2001 From: heige Date: Tue, 3 Aug 2021 11:40:57 +0800 Subject: [PATCH 19/51] Fix migrator GuessConstraintAndTable method for return value for *schema.Check (#4527) --- migrator/migrator.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/migrator/migrator.go b/migrator/migrator.go index 80d58efd..012ccf65 100644 --- a/migrator/migrator.go +++ b/migrator/migrator.go @@ -503,9 +503,10 @@ func (m Migrator) GuessConstraintAndTable(stmt *gorm.Statement, name string) (_ } if field := stmt.Schema.LookUpField(name); field != nil { - for _, cc := range checkConstraints { - if cc.Field == field { - return nil, &cc, stmt.Table + for k := range checkConstraints { + if checkConstraints[k].Field == field { + v := checkConstraints[k] + return nil, &v, stmt.Table } } From a870486c4f967d732b2786f320886b0230053c18 Mon Sep 17 00:00:00 2001 From: Walter Scheper Date: Mon, 9 Aug 2021 01:14:23 -0400 Subject: [PATCH 20/51] Do not emit ORDER BY for empty values (#4592) This restores the behavior from gorm v1, where calling `DB.Order` with an empty string, nil, or any unexpected type is a no-op. --- chainable_api.go | 14 ++++++++------ tests/query_test.go | 12 +++++++++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/chainable_api.go b/chainable_api.go index e17d9bb2..d5a0907d 100644 --- a/chainable_api.go +++ b/chainable_api.go @@ -209,12 +209,14 @@ func (db *DB) Order(value interface{}) (tx *DB) { tx.Statement.AddClause(clause.OrderBy{ Columns: []clause.OrderByColumn{v}, }) - default: - tx.Statement.AddClause(clause.OrderBy{ - Columns: []clause.OrderByColumn{{ - Column: clause.Column{Name: fmt.Sprint(value), Raw: true}, - }}, - }) + case string: + if v != "" { + tx.Statement.AddClause(clause.OrderBy{ + Columns: []clause.OrderByColumn{{ + Column: clause.Column{Name: v, Raw: true}, + }}, + }) + } } return } diff --git a/tests/query_test.go b/tests/query_test.go index 34999337..36046aee 100644 --- a/tests/query_test.go +++ b/tests/query_test.go @@ -842,7 +842,17 @@ func TestSearchWithEmptyChain(t *testing.T) { func TestOrder(t *testing.T) { dryDB := DB.Session(&gorm.Session{DryRun: true}) - result := dryDB.Order("age desc, name").Find(&User{}) + result := dryDB.Order("").Find(&User{}) + if !regexp.MustCompile("SELECT \\* FROM .*users.* IS NULL$").MatchString(result.Statement.SQL.String()) { + t.Fatalf("Build Order condition, but got %v", result.Statement.SQL.String()) + } + + result = dryDB.Order(nil).Find(&User{}) + if !regexp.MustCompile("SELECT \\* FROM .*users.* IS NULL$").MatchString(result.Statement.SQL.String()) { + t.Fatalf("Build Order condition, but got %v", result.Statement.SQL.String()) + } + + result = dryDB.Order("age desc, name").Find(&User{}) if !regexp.MustCompile("SELECT \\* FROM .*users.* ORDER BY age desc, name").MatchString(result.Statement.SQL.String()) { t.Fatalf("Build Order condition, but got %v", result.Statement.SQL.String()) } From cbe72751ac7e900a86ca843757c677b3c322732b Mon Sep 17 00:00:00 2001 From: Matthieu MOREL Date: Mon, 9 Aug 2021 07:16:25 +0200 Subject: [PATCH 21/51] Update Dependencies (#4582) * Create dependabot.yml * Bump reviewdog/action-golangci-lint from 1 to 2 (#1) Bumps [reviewdog/action-golangci-lint](https://github.com/reviewdog/action-golangci-lint) from 1 to 2. - [Release notes](https://github.com/reviewdog/action-golangci-lint/releases) - [Commits](https://github.com/reviewdog/action-golangci-lint/compare/v1...v2) --- updated-dependencies: - dependency-name: reviewdog/action-golangci-lint dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump actions/stale from 3.0.7 to 4 (#2) Bumps [actions/stale](https://github.com/actions/stale) from 3.0.7 to 4. - [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/v3.0.7...v4) --- updated-dependencies: - dependency-name: actions/stale dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump gorm.io/gorm from 1.21.9 to 1.21.12 in /tests (#3) Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.21.9 to 1.21.12. - [Release notes](https://github.com/go-gorm/gorm/releases) - [Commits](https://github.com/go-gorm/gorm/compare/v1.21.9...v1.21.12) --- updated-dependencies: - dependency-name: gorm.io/gorm dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump gorm.io/driver/mysql from 1.0.5 to 1.1.1 in /tests (#4) Bumps [gorm.io/driver/mysql](https://github.com/go-gorm/mysql) from 1.0.5 to 1.1.1. - [Release notes](https://github.com/go-gorm/mysql/releases) - [Commits](https://github.com/go-gorm/mysql/compare/v1.0.5...v1.1.1) --- updated-dependencies: - dependency-name: gorm.io/driver/mysql dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump github.com/lib/pq from 1.6.0 to 1.10.2 in /tests (#5) Bumps [github.com/lib/pq](https://github.com/lib/pq) from 1.6.0 to 1.10.2. - [Release notes](https://github.com/lib/pq/releases) - [Commits](https://github.com/lib/pq/compare/v1.6.0...v1.10.2) --- updated-dependencies: - dependency-name: github.com/lib/pq dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump github.com/google/uuid from 1.2.0 to 1.3.0 in /tests (#6) Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.2.0 to 1.3.0. - [Release notes](https://github.com/google/uuid/releases) - [Commits](https://github.com/google/uuid/compare/v1.2.0...v1.3.0) --- updated-dependencies: - dependency-name: github.com/google/uuid dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/dependabot.yml | 15 +++++++++++++++ .github/workflows/invalid_question.yml | 2 +- .github/workflows/missing_playground.yml | 2 +- .github/workflows/reviewdog.yml | 2 +- .github/workflows/stale.yml | 2 +- tests/go.mod | 8 ++++---- 6 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..e4e81074 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +--- +version: 2 +updates: + - package-ecosystem: gomod + directory: / + schedule: + interval: weekly + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + - package-ecosystem: gomod + directory: /tests + schedule: + interval: weekly diff --git a/.github/workflows/invalid_question.yml b/.github/workflows/invalid_question.yml index 5b0bd981..dfd2ddd9 100644 --- a/.github/workflows/invalid_question.yml +++ b/.github/workflows/invalid_question.yml @@ -10,7 +10,7 @@ jobs: ACTIONS_STEP_DEBUG: true steps: - name: Close Stale Issues - uses: actions/stale@v3.0.7 + uses: actions/stale@v4 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 2 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 ea3207d6..cdb097de 100644 --- a/.github/workflows/missing_playground.yml +++ b/.github/workflows/missing_playground.yml @@ -10,7 +10,7 @@ jobs: ACTIONS_STEP_DEBUG: true steps: - name: Close Stale Issues - uses: actions/stale@v3.0.7 + uses: actions/stale@v4 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 2 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/reviewdog.yml b/.github/workflows/reviewdog.yml index 4511c378..d55a4699 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/reviewdog.yml @@ -8,4 +8,4 @@ jobs: - name: Check out code into the Go module directory uses: actions/checkout@v1 - name: golangci-lint - uses: reviewdog/action-golangci-lint@v1 + uses: reviewdog/action-golangci-lint@v2 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index f9c1bece..d5419295 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -10,7 +10,7 @@ jobs: ACTIONS_STEP_DEBUG: true steps: - name: Close Stale Issues - uses: actions/stale@v3.0.7 + uses: actions/stale@v4 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: "This issue has been automatically marked as stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 30 days" diff --git a/tests/go.mod b/tests/go.mod index 815f8986..b623b363 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -3,14 +3,14 @@ module gorm.io/gorm/tests go 1.14 require ( - github.com/google/uuid v1.2.0 + github.com/google/uuid v1.3.0 github.com/jinzhu/now v1.1.2 - github.com/lib/pq v1.6.0 - gorm.io/driver/mysql v1.0.5 + github.com/lib/pq v1.10.2 + gorm.io/driver/mysql v1.1.1 gorm.io/driver/postgres v1.1.0 gorm.io/driver/sqlite v1.1.4 gorm.io/driver/sqlserver v1.0.7 - gorm.io/gorm v1.21.9 + gorm.io/gorm v1.21.12 ) replace gorm.io/gorm => ../ From 82fe81530305257eb13f708d8fe5bd63c05cac01 Mon Sep 17 00:00:00 2001 From: SmallTianTian Date: Mon, 9 Aug 2021 13:20:22 +0800 Subject: [PATCH 22/51] fix: table couln't be reentrant (#4556) --- chainable_api.go | 7 +++---- tests/table_test.go | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/chainable_api.go b/chainable_api.go index d5a0907d..88279044 100644 --- a/chainable_api.go +++ b/chainable_api.go @@ -50,15 +50,14 @@ func (db *DB) Table(name string, args ...interface{}) (tx *DB) { tx.Statement.TableExpr = &clause.Expr{SQL: name, Vars: args} if results := tableRegexp.FindStringSubmatch(name); len(results) == 2 { tx.Statement.Table = results[1] - return } } else if tables := strings.Split(name, "."); len(tables) == 2 { tx.Statement.TableExpr = &clause.Expr{SQL: tx.Statement.Quote(name)} tx.Statement.Table = tables[1] - return + } else { + tx.Statement.TableExpr = &clause.Expr{SQL: tx.Statement.Quote(name)} + tx.Statement.Table = name } - - tx.Statement.Table = name return } diff --git a/tests/table_test.go b/tests/table_test.go index 0c6b3eb0..0289b7b8 100644 --- a/tests/table_test.go +++ b/tests/table_test.go @@ -30,6 +30,26 @@ func TestTable(t *testing.T) { t.Errorf("Table with escape character, got %v", r.Statement.SQL.String()) } + r = dryDB.Table("`people`").Table("`user`").Find(&User{}).Statement + if !regexp.MustCompile("SELECT \\* FROM `user`").MatchString(r.Statement.SQL.String()) { + t.Errorf("Table with escape character, got %v", r.Statement.SQL.String()) + } + + r = dryDB.Table("people as p").Table("user as u").Find(&User{}).Statement + if !regexp.MustCompile("SELECT \\* FROM user as u WHERE .u.\\..deleted_at. IS NULL").MatchString(r.Statement.SQL.String()) { + t.Errorf("Table with escape character, got %v", r.Statement.SQL.String()) + } + + r = dryDB.Table("people as p").Table("user").Find(&User{}).Statement + if !regexp.MustCompile("SELECT \\* FROM .user. WHERE .user.\\..deleted_at. IS NULL").MatchString(r.Statement.SQL.String()) { + t.Errorf("Table with escape character, got %v", r.Statement.SQL.String()) + } + + r = dryDB.Table("gorm.people").Table("user").Find(&User{}).Statement + if !regexp.MustCompile("SELECT \\* FROM .user. WHERE .user.\\..deleted_at. IS NULL").MatchString(r.Statement.SQL.String()) { + t.Errorf("Table with escape character, got %v", r.Statement.SQL.String()) + } + r = dryDB.Table("gorm.user").Select("name").Find(&User{}).Statement if !regexp.MustCompile("SELECT .name. FROM .gorm.\\..user. WHERE .user.\\..deleted_at. IS NULL").MatchString(r.Statement.SQL.String()) { t.Errorf("Table with escape character, got %v", r.Statement.SQL.String()) From 21e85b89d68c3d9af5a7f23280471cff05dd2e8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=87=AF=E5=BC=BA?= Date: Mon, 9 Aug 2021 13:23:44 +0800 Subject: [PATCH 23/51] Fix create with ignore migration (#4571) --- migrator/migrator.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/migrator/migrator.go b/migrator/migrator.go index 012ccf65..48db151e 100644 --- a/migrator/migrator.go +++ b/migrator/migrator.go @@ -167,10 +167,12 @@ func (m Migrator) CreateTable(values ...interface{}) error { for _, dbName := range stmt.Schema.DBNames { field := stmt.Schema.FieldsByDBName[dbName] - createTableSQL += "? ?" - hasPrimaryKeyInDataType = hasPrimaryKeyInDataType || strings.Contains(strings.ToUpper(string(field.DataType)), "PRIMARY KEY") - values = append(values, clause.Column{Name: dbName}, m.DB.Migrator().FullDataTypeOf(field)) - createTableSQL += "," + if !field.IgnoreMigration { + createTableSQL += "? ?" + hasPrimaryKeyInDataType = hasPrimaryKeyInDataType || strings.Contains(strings.ToUpper(string(field.DataType)), "PRIMARY KEY") + values = append(values, clause.Column{Name: dbName}, m.DB.Migrator().FullDataTypeOf(field)) + createTableSQL += "," + } } if !hasPrimaryKeyInDataType && len(stmt.Schema.PrimaryFields) > 0 { From a83d25e25e700f8a3c40dda048ac52b23bba31d5 Mon Sep 17 00:00:00 2001 From: Sungyun Hur Date: Wed, 11 Aug 2021 12:49:46 +0900 Subject: [PATCH 24/51] chore(logger): explicitly set config of Default Logger (#4605) --- logger/logger.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/logger/logger.go b/logger/logger.go index 98d1b32e..69d41113 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -64,9 +64,10 @@ type Interface interface { var ( Discard = New(log.New(ioutil.Discard, "", log.LstdFlags), Config{}) Default = New(log.New(os.Stdout, "\r\n", log.LstdFlags), Config{ - SlowThreshold: 200 * time.Millisecond, - LogLevel: Warn, - Colorful: true, + SlowThreshold: 200 * time.Millisecond, + LogLevel: Warn, + IgnoreRecordNotFoundError: false, + Colorful: true, }) Recorder = traceRecorder{Interface: Default, BeginAt: time.Now()} ) From 2b2f6e77af28e57e7bbea5962d58b1a7cb8ff47b Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Wed, 11 Aug 2021 16:20:21 +0800 Subject: [PATCH 25/51] Add SchemaName to NamingStrategy --- schema/naming.go | 20 ++++++++++++++++++++ schema/naming_test.go | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/schema/naming.go b/schema/naming.go index 47e313a7..8407bffa 100644 --- a/schema/naming.go +++ b/schema/naming.go @@ -4,6 +4,7 @@ import ( "crypto/sha1" "encoding/hex" "fmt" + "regexp" "strings" "unicode/utf8" @@ -13,6 +14,7 @@ import ( // Namer namer interface type Namer interface { TableName(table string) string + SchemaName(table string) string ColumnName(table, column string) string JoinTableName(joinTable string) string RelationshipFKName(Relationship) string @@ -41,6 +43,16 @@ func (ns NamingStrategy) TableName(str string) string { return ns.TablePrefix + inflection.Plural(ns.toDBName(str)) } +// SchemaName generate schema name from table name, don't guarantee it is the reverse value of TableName +func (ns NamingStrategy) SchemaName(table string) string { + table = strings.TrimPrefix(table, ns.TablePrefix) + + if ns.SingularTable { + return ns.toSchemaName(table) + } + return ns.toSchemaName(inflection.Singular(table)) +} + // ColumnName convert string to column name func (ns NamingStrategy) ColumnName(table, column string) string { return ns.toDBName(column) @@ -154,3 +166,11 @@ func (ns NamingStrategy) toDBName(name string) string { ret := buf.String() return ret } + +func (ns NamingStrategy) toSchemaName(name string) string { + result := strings.Replace(strings.Title(strings.Replace(name, "_", " ", -1)), " ", "", -1) + for _, initialism := range commonInitialisms { + result = regexp.MustCompile(strings.Title(strings.ToLower(initialism))+"([A-Z]|$|_)").ReplaceAllString(result, initialism+"$1") + } + return result +} diff --git a/schema/naming_test.go b/schema/naming_test.go index face9364..6add338e 100644 --- a/schema/naming_test.go +++ b/schema/naming_test.go @@ -33,6 +33,26 @@ func TestToDBName(t *testing.T) { t.Errorf("%v toName should equal %v, but got %v", key, value, ns.toDBName(key)) } } + + maps = map[string]string{ + "x": "X", + "user_restrictions": "UserRestriction", + "this_is_a_test": "ThisIsATest", + "abc_and_jkl": "AbcAndJkl", + "employee_id": "EmployeeID", + "field_x": "FieldX", + "http_and_smtp": "HTTPAndSMTP", + "http_server_handler_for_url_id": "HTTPServerHandlerForURLID", + "uuid": "UUID", + "http_url": "HTTPURL", + "sha256_hash": "Sha256Hash", + "this_is_actually_a_test_so_we_may_be_able_to_use_this_code_in_gorm_package_also_id_can_be_used_at_the_end_as_id": "ThisIsActuallyATestSoWeMayBeAbleToUseThisCodeInGormPackageAlsoIDCanBeUsedAtTheEndAsID", + } + for key, value := range maps { + if ns.SchemaName(key) != value { + t.Errorf("%v schema name should equal %v, but got %v", key, value, ns.SchemaName(key)) + } + } } func TestNamingStrategy(t *testing.T) { From 25f561a742776af41b3165e2600e782ec9defe8b Mon Sep 17 00:00:00 2001 From: River Date: Thu, 19 Aug 2021 14:33:18 +0800 Subject: [PATCH 26/51] feat: QuoteTo accept clause.Expr (#4621) * feat: QuoteTo accept clause.Expr * test: update Expr build test --- clause/expression_test.go | 12 ++++++++++++ statement.go | 2 ++ 2 files changed, 14 insertions(+) diff --git a/clause/expression_test.go b/clause/expression_test.go index 0ccd0771..05074865 100644 --- a/clause/expression_test.go +++ b/clause/expression_test.go @@ -156,6 +156,18 @@ func TestExpression(t *testing.T) { }, ExpectedVars: []interface{}{"a", "b"}, Result: "`column-name` NOT IN (?,?)", + }, { + Expressions: []clause.Expression{ + clause.Eq{Column: clause.Expr{SQL: "SUM(?)", Vars: []interface{}{clause.Column{Name: "id"}}}, Value: 100}, + }, + ExpectedVars: []interface{}{100}, + Result: "SUM(`id`) = ?", + }, { + Expressions: []clause.Expression{ + clause.Gte{Column: clause.Expr{SQL: "SUM(?)", Vars: []interface{}{clause.Column{Table: "users", Name: "id"}}}, Value: 100}, + }, + ExpectedVars: []interface{}{100}, + Result: "SUM(`users`.`id`) >= ?", }} for idx, result := range results { diff --git a/statement.go b/statement.go index 8b682c84..93b78c12 100644 --- a/statement.go +++ b/statement.go @@ -129,6 +129,8 @@ func (stmt *Statement) QuoteTo(writer clause.Writer, field interface{}) { stmt.QuoteTo(writer, d) } writer.WriteByte(')') + case clause.Expr: + v.Build(stmt) case string: stmt.DB.Dialector.QuoteTo(writer, v) case []string: From 1bb0d8732d2a33d1d796af2591478c7013e36736 Mon Sep 17 00:00:00 2001 From: River Date: Fri, 20 Aug 2021 17:37:21 +0800 Subject: [PATCH 27/51] feat: count accpet `db`.`table` (#4626) * feat: count accpet `db`.`table` * fix: logic fix --- finisher_api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finisher_api.go b/finisher_api.go index 537c955a..34e1596b 100644 --- a/finisher_api.go +++ b/finisher_api.go @@ -390,7 +390,7 @@ func (db *DB) Count(count *int64) (tx *DB) { if len(tx.Statement.Selects) == 1 { dbName := tx.Statement.Selects[0] fields := strings.FieldsFunc(dbName, utils.IsValidDBNameChar) - if len(fields) == 1 || (len(fields) == 3 && strings.ToUpper(fields[1]) == "AS") { + if len(fields) == 1 || (len(fields) == 3 && (strings.ToUpper(fields[1]) == "AS" || fields[1] == ".")) { if tx.Statement.Parse(tx.Statement.Model) == nil { if f := tx.Statement.Schema.LookUpField(dbName); f != nil { dbName = f.DBName From e076e9e0fbe043fcb4717c792a3112684cc8723d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Aug 2021 17:52:48 +0800 Subject: [PATCH 28/51] Bump gorm.io/gorm from 1.21.12 to 1.21.13 in /tests (#4616) Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.21.12 to 1.21.13. - [Release notes](https://github.com/go-gorm/gorm/releases) - [Commits](https://github.com/go-gorm/gorm/compare/v1.21.12...v1.21.13) --- updated-dependencies: - dependency-name: gorm.io/gorm dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/go.mod b/tests/go.mod index b623b363..c456cc92 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -10,7 +10,7 @@ require ( gorm.io/driver/postgres v1.1.0 gorm.io/driver/sqlite v1.1.4 gorm.io/driver/sqlserver v1.0.7 - gorm.io/gorm v1.21.12 + gorm.io/gorm v1.21.13 ) replace gorm.io/gorm => ../ From 7a53d8e46b6b54a5f63ca9214fc8f81b6e692122 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Aug 2021 17:52:56 +0800 Subject: [PATCH 29/51] Bump gorm.io/driver/mysql from 1.1.1 to 1.1.2 in /tests (#4615) Bumps [gorm.io/driver/mysql](https://github.com/go-gorm/mysql) from 1.1.1 to 1.1.2. - [Release notes](https://github.com/go-gorm/mysql/releases) - [Commits](https://github.com/go-gorm/mysql/compare/v1.1.1...v1.1.2) --- updated-dependencies: - dependency-name: gorm.io/driver/mysql dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/go.mod b/tests/go.mod index c456cc92..278ad5b3 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -6,7 +6,7 @@ require ( github.com/google/uuid v1.3.0 github.com/jinzhu/now v1.1.2 github.com/lib/pq v1.10.2 - gorm.io/driver/mysql v1.1.1 + gorm.io/driver/mysql v1.1.2 gorm.io/driver/postgres v1.1.0 gorm.io/driver/sqlite v1.1.4 gorm.io/driver/sqlserver v1.0.7 From 093694fbf2922a3dff56059504ead6974399febf Mon Sep 17 00:00:00 2001 From: Sec Cake Date: Fri, 20 Aug 2021 18:06:48 +0800 Subject: [PATCH 30/51] Fix extra 'AND' when len(values) == 0 ON IN.NegationBuild() (#4618) --- clause/expression.go | 4 ++-- tests/query_test.go | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/clause/expression.go b/clause/expression.go index a177c5d8..f7b93f4c 100644 --- a/clause/expression.go +++ b/clause/expression.go @@ -210,11 +210,12 @@ func (in IN) Build(builder Builder) { } func (in IN) NegationBuild(builder Builder) { + builder.WriteQuoted(in.Column) switch len(in.Values) { case 0: + builder.WriteString(" IS NOT NULL") case 1: if _, ok := in.Values[0].([]interface{}); !ok { - builder.WriteQuoted(in.Column) builder.WriteString(" <> ") builder.AddVar(builder, in.Values[0]) break @@ -222,7 +223,6 @@ func (in IN) NegationBuild(builder Builder) { fallthrough default: - builder.WriteQuoted(in.Column) builder.WriteString(" NOT IN (") builder.AddVar(builder, in.Values...) builder.WriteByte(')') diff --git a/tests/query_test.go b/tests/query_test.go index 36046aee..8a476598 100644 --- a/tests/query_test.go +++ b/tests/query_test.go @@ -436,6 +436,11 @@ func TestNot(t *testing.T) { t.Fatalf("Build NOT condition, but got %v", result.Statement.SQL.String()) } + result = dryDB.Not(map[string]interface{}{"name": []string{}}).Find(&User{}) + if !regexp.MustCompile("SELECT \\* FROM .*users.* WHERE .*name.* IS NOT NULL").MatchString(result.Statement.SQL.String()) { + t.Fatalf("Build NOT condition, but got %v", result.Statement.SQL.String()) + } + result = dryDB.Not(map[string]interface{}{"name": []string{"jinzhu", "jinzhu 2"}}).Find(&User{}) if !regexp.MustCompile("SELECT \\* FROM .*users.* WHERE .*name.* NOT IN \\(.+,.+\\)").MatchString(result.Statement.SQL.String()) { t.Fatalf("Build NOT condition, but got %v", result.Statement.SQL.String()) From 0934b10856246d178c2230bd83054e109a19da23 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Aug 2021 15:30:02 +0800 Subject: [PATCH 31/51] Bump gorm.io/driver/sqlserver from 1.0.7 to 1.0.8 in /tests (#4631) Bumps [gorm.io/driver/sqlserver](https://github.com/go-gorm/sqlserver) from 1.0.7 to 1.0.8. - [Release notes](https://github.com/go-gorm/sqlserver/releases) - [Commits](https://github.com/go-gorm/sqlserver/compare/v1.0.7...v1.0.8) --- updated-dependencies: - dependency-name: gorm.io/driver/sqlserver dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/go.mod b/tests/go.mod index 278ad5b3..db489ee7 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -9,7 +9,7 @@ require ( gorm.io/driver/mysql v1.1.2 gorm.io/driver/postgres v1.1.0 gorm.io/driver/sqlite v1.1.4 - gorm.io/driver/sqlserver v1.0.7 + gorm.io/driver/sqlserver v1.0.8 gorm.io/gorm v1.21.13 ) From f21e35f7c5f6a67cfcf54c0d439d9aef00224b77 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Thu, 26 Aug 2021 13:14:03 +0800 Subject: [PATCH 32/51] Fix table not supported error when using unexpected table name --- callbacks.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/callbacks.go b/callbacks.go index 02e741e7..7ab38926 100644 --- a/callbacks.go +++ b/callbacks.go @@ -102,8 +102,8 @@ func (p *processor) Execute(db *DB) *DB { // parse model values if stmt.Model != nil { - if err := stmt.Parse(stmt.Model); err != nil && (!errors.Is(err, schema.ErrUnsupportedDataType) || (stmt.Table == "" && stmt.SQL.Len() == 0)) { - if errors.Is(err, schema.ErrUnsupportedDataType) && stmt.Table == "" { + if err := stmt.Parse(stmt.Model); err != nil && (!errors.Is(err, schema.ErrUnsupportedDataType) || (stmt.Table == "" && stmt.TableExpr == nil && stmt.SQL.Len() == 0)) { + if errors.Is(err, schema.ErrUnsupportedDataType) && stmt.Table == "" && stmt.TableExpr == nil { db.AddError(fmt.Errorf("%w: Table not set, please set it like: db.Model(&user) or db.Table(\"users\")", err)) } else { db.AddError(err) From e81833fd112370be5cf3268d6919d8a4cda1d46a Mon Sep 17 00:00:00 2001 From: zkqiang Date: Mon, 23 Aug 2021 01:35:32 +0800 Subject: [PATCH 33/51] Fix onConflict with non-updatable in associations --- callbacks/associations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/callbacks/associations.go b/callbacks/associations.go index 78f976c3..14c433c4 100644 --- a/callbacks/associations.go +++ b/callbacks/associations.go @@ -314,7 +314,7 @@ func onConflictOption(stmt *gorm.Statement, s *schema.Schema, selectColumns map[ if stmt.DB.FullSaveAssociations { defaultUpdatingColumns = make([]string, 0, len(s.DBNames)) for _, dbName := range s.DBNames { - if v, ok := selectColumns[dbName]; (ok && !v) || (!ok && restricted) { + if v, ok := selectColumns[dbName]; (ok && !v) || (!ok && restricted) || !s.FieldsByDBName[dbName].Updatable { continue } From 74746211b8b64abc62d4a42e5051da5b6b670fc0 Mon Sep 17 00:00:00 2001 From: zkqiang Date: Mon, 23 Aug 2021 15:15:05 +0800 Subject: [PATCH 34/51] Test update association with non-updatable --- tests/update_has_one_test.go | 45 ++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/update_has_one_test.go b/tests/update_has_one_test.go index a61629f8..59d30e42 100644 --- a/tests/update_has_one_test.go +++ b/tests/update_has_one_test.go @@ -1,6 +1,7 @@ package tests_test import ( + "database/sql" "testing" "time" @@ -85,4 +86,48 @@ func TestUpdateHasOne(t *testing.T) { DB.Preload("Toy").Find(&pet4, "id = ?", pet.ID) CheckPet(t, pet4, pet) }) + + t.Run("Restriction", func(t *testing.T) { + type CustomizeAccount struct { + gorm.Model + UserID sql.NullInt64 + Number string `gorm:"<-:create"` + } + + type CustomizeUser struct { + gorm.Model + Name string + Account CustomizeAccount `gorm:"foreignkey:UserID"` + } + + DB.Migrator().DropTable(&CustomizeUser{}) + DB.Migrator().DropTable(&CustomizeAccount{}) + + if err := DB.AutoMigrate(&CustomizeUser{}); err != nil { + t.Fatalf("failed to migrate, got error: %v", err) + } + if err := DB.AutoMigrate(&CustomizeAccount{}); err != nil { + t.Fatalf("failed to migrate, got error: %v", err) + } + + number := "number-has-one-associations" + cusUser := CustomizeUser{ + Name: "update-has-one-associations", + Account: CustomizeAccount{ + Number: number, + }, + } + + if err := DB.Create(&cusUser).Error; err != nil { + t.Fatalf("errors happened when create: %v", err) + } + cusUser.Account.Number += "-update" + if err := DB.Session(&gorm.Session{FullSaveAssociations: true}).Updates(&cusUser).Error; err != nil { + t.Fatalf("errors happened when create: %v", err) + } + + var account2 CustomizeAccount + DB.Find(&account2, "user_id = ?", cusUser.ID) + AssertEqual(t, account2.Number, number) + }) } From 3a8c25018004480ae170e9b4414dbad1f6d7bfd7 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Thu, 26 Aug 2021 13:37:26 +0800 Subject: [PATCH 35/51] Refactor calc associations onConflictOption --- callbacks/associations.go | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/callbacks/associations.go b/callbacks/associations.go index 14c433c4..d78bd968 100644 --- a/callbacks/associations.go +++ b/callbacks/associations.go @@ -310,33 +310,22 @@ func SaveAfterAssociations(create bool) func(db *gorm.DB) { } } -func onConflictOption(stmt *gorm.Statement, s *schema.Schema, selectColumns map[string]bool, restricted bool, defaultUpdatingColumns []string) clause.OnConflict { - if stmt.DB.FullSaveAssociations { - defaultUpdatingColumns = make([]string, 0, len(s.DBNames)) - for _, dbName := range s.DBNames { - if v, ok := selectColumns[dbName]; (ok && !v) || (!ok && restricted) || !s.FieldsByDBName[dbName].Updatable { - continue - } - - if !s.LookUpField(dbName).PrimaryKey { - defaultUpdatingColumns = append(defaultUpdatingColumns, dbName) - } - } - } - - if len(defaultUpdatingColumns) > 0 { - columns := make([]clause.Column, 0, len(s.PrimaryFieldDBNames)) +func onConflictOption(stmt *gorm.Statement, s *schema.Schema, selectColumns map[string]bool, restricted bool, defaultUpdatingColumns []string) (onConflict clause.OnConflict) { + if len(defaultUpdatingColumns) > 0 || stmt.DB.FullSaveAssociations { + onConflict.Columns = make([]clause.Column, 0, len(s.PrimaryFieldDBNames)) for _, dbName := range s.PrimaryFieldDBNames { - columns = append(columns, clause.Column{Name: dbName}) + onConflict.Columns = append(onConflict.Columns, clause.Column{Name: dbName}) } - return clause.OnConflict{ - Columns: columns, - DoUpdates: clause.AssignmentColumns(defaultUpdatingColumns), + onConflict.UpdateAll = stmt.DB.FullSaveAssociations + if !onConflict.UpdateAll { + onConflict.DoUpdates = clause.AssignmentColumns(defaultUpdatingColumns) } + } else { + onConflict.DoNothing = true } - return clause.OnConflict{DoNothing: true} + return } func saveAssociations(db *gorm.DB, rel *schema.Relationship, values interface{}, selectColumns map[string]bool, restricted bool, defaultUpdatingColumns []string) error { From 15188cf409127bf08394322f95943d674f0459a7 Mon Sep 17 00:00:00 2001 From: jxlwqq Date: Fri, 3 Sep 2021 17:47:32 +0800 Subject: [PATCH 36/51] Add Go 1.17 (#4666) --- .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 8bd2bcb3..d5ee1e88 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: sqlite: strategy: matrix: - go: ['1.16', '1.15'] + go: ['1.17', '1.16', '1.15'] platform: [ubuntu-latest] # can not run in windows OS runs-on: ${{ matrix.platform }} @@ -39,7 +39,7 @@ jobs: strategy: matrix: dbversion: ['mysql:latest', 'mysql:5.7', 'mariadb:latest'] - go: ['1.16', '1.15'] + go: ['1.17', '1.16', '1.15'] platform: [ubuntu-latest] runs-on: ${{ matrix.platform }} @@ -83,7 +83,7 @@ jobs: strategy: matrix: dbversion: ['postgres:latest', 'postgres:12', 'postgres:11', 'postgres:10'] - go: ['1.16', '1.15'] + go: ['1.17', '1.16', '1.15'] platform: [ubuntu-latest] # can not run in macOS and Windows runs-on: ${{ matrix.platform }} @@ -125,7 +125,7 @@ jobs: sqlserver: strategy: matrix: - go: ['1.16', '1.15'] + go: ['1.17', '1.16', '1.15'] platform: [ubuntu-latest] # can not run test in macOS and windows runs-on: ${{ matrix.platform }} From 5f019f74bf81f2d67489ed1dc2d9559b19333eb1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Sep 2021 17:47:50 +0800 Subject: [PATCH 37/51] Bump gorm.io/gorm from 1.21.13 to 1.21.14 in /tests (#4655) Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.21.13 to 1.21.14. - [Release notes](https://github.com/go-gorm/gorm/releases) - [Commits](https://github.com/go-gorm/gorm/compare/v1.21.13...v1.21.14) --- updated-dependencies: - dependency-name: gorm.io/gorm dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/go.mod b/tests/go.mod index db489ee7..3403f6e9 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -10,7 +10,7 @@ require ( gorm.io/driver/postgres v1.1.0 gorm.io/driver/sqlite v1.1.4 gorm.io/driver/sqlserver v1.0.8 - gorm.io/gorm v1.21.13 + gorm.io/gorm v1.21.14 ) replace gorm.io/gorm => ../ From a89d4d8fd5f679b14394336eeaa02c6b2094b526 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Sep 2021 16:26:14 +0800 Subject: [PATCH 38/51] Bump github.com/lib/pq from 1.10.2 to 1.10.3 in /tests (#4676) Bumps [github.com/lib/pq](https://github.com/lib/pq) from 1.10.2 to 1.10.3. - [Release notes](https://github.com/lib/pq/releases) - [Commits](https://github.com/lib/pq/compare/v1.10.2...v1.10.3) --- updated-dependencies: - dependency-name: github.com/lib/pq dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/go.mod b/tests/go.mod index 3403f6e9..a1033a60 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -5,7 +5,7 @@ go 1.14 require ( github.com/google/uuid v1.3.0 github.com/jinzhu/now v1.1.2 - github.com/lib/pq v1.10.2 + github.com/lib/pq v1.10.3 gorm.io/driver/mysql v1.1.2 gorm.io/driver/postgres v1.1.0 gorm.io/driver/sqlite v1.1.4 From 1d9e563023dfb03c829e6675e08536b429fd5c09 Mon Sep 17 00:00:00 2001 From: riverchu Date: Fri, 3 Sep 2021 23:09:20 +0800 Subject: [PATCH 39/51] style: prepose error judgement --- callbacks/update.go | 50 +++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/callbacks/update.go b/callbacks/update.go index 75bb02db..d85c4c22 100644 --- a/callbacks/update.go +++ b/callbacks/update.go @@ -51,37 +51,39 @@ func BeforeUpdate(db *gorm.DB) { } func Update(db *gorm.DB) { - if db.Error == nil { - if db.Statement.Schema != nil && !db.Statement.Unscoped { - for _, c := range db.Statement.Schema.UpdateClauses { - db.Statement.AddClause(c) - } - } + if db.Error != nil { + return + } - if db.Statement.SQL.String() == "" { - db.Statement.SQL.Grow(180) - db.Statement.AddClauseIfNotExists(clause.Update{}) - if set := ConvertToAssignments(db.Statement); len(set) != 0 { - db.Statement.AddClause(set) - } else { - return - } - db.Statement.Build(db.Statement.BuildClauses...) + if db.Statement.Schema != nil && !db.Statement.Unscoped { + for _, c := range db.Statement.Schema.UpdateClauses { + db.Statement.AddClause(c) } + } - if _, ok := db.Statement.Clauses["WHERE"]; !db.AllowGlobalUpdate && !ok { - db.AddError(gorm.ErrMissingWhereClause) + if db.Statement.SQL.String() == "" { + db.Statement.SQL.Grow(180) + db.Statement.AddClauseIfNotExists(clause.Update{}) + if set := ConvertToAssignments(db.Statement); len(set) != 0 { + db.Statement.AddClause(set) + } else { return } + db.Statement.Build(db.Statement.BuildClauses...) + } - if !db.DryRun && db.Error == nil { - result, err := db.Statement.ConnPool.ExecContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...) + if _, ok := db.Statement.Clauses["WHERE"]; !db.AllowGlobalUpdate && !ok { + db.AddError(gorm.ErrMissingWhereClause) + return + } - if err == nil { - db.RowsAffected, _ = result.RowsAffected() - } else { - db.AddError(err) - } + if !db.DryRun && db.Error == nil { + result, err := db.Statement.ConnPool.ExecContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...) + + if err == nil { + db.RowsAffected, _ = result.RowsAffected() + } else { + db.AddError(err) } } } From c89862279137298f794351ace2dad9c1e487b327 Mon Sep 17 00:00:00 2001 From: riverchu Date: Sun, 5 Sep 2021 11:10:48 +0800 Subject: [PATCH 40/51] test: add testcase in TestSave --- tests/update_test.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/update_test.go b/tests/update_test.go index 5ad1bb39..869df769 100644 --- a/tests/update_test.go +++ b/tests/update_test.go @@ -642,6 +642,36 @@ func TestSave(t *testing.T) { if !regexp.MustCompile("WHERE .id. = [^ ]+$").MatchString(stmt.SQL.String()) { t.Fatalf("invalid updating SQL, got %v", stmt.SQL.String()) } + + user3 := *GetUser("save3", Config{}) + DB.Create(&user3) + + if err := DB.First(&User{}, "name = ?", "save3").Error; err != nil { + t.Fatalf("failed to find created user") + } + + user3.Name = "save3_" + DB.Model(User{}).Save(&user3) + + var result2 User + if err := DB.First(&result2, "name = ?", "save3_").Error; err != nil || result2.ID != user3.ID { + t.Fatalf("failed to find updated user") + } + + DB.Model(User{}).Save(&struct { + gorm.Model + Placeholder string + Name string + }{ + Model: user3.Model, + Placeholder: "placeholder", + Name: "save3__", + }) + + var result3 User + if err := DB.First(&result3, "name = ?", "save3__").Error; err != nil || result3.ID != user3.ID { + t.Fatalf("failed to find updated user") + } } func TestSaveWithPrimaryValue(t *testing.T) { From 4581e8b590a83d730dc490e8731990f467ba9e4f Mon Sep 17 00:00:00 2001 From: riverchu Date: Sun, 5 Sep 2021 23:07:28 +0800 Subject: [PATCH 41/51] test: update Save test --- tests/update_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/update_test.go b/tests/update_test.go index 869df769..2a747ce5 100644 --- a/tests/update_test.go +++ b/tests/update_test.go @@ -651,14 +651,14 @@ func TestSave(t *testing.T) { } user3.Name = "save3_" - DB.Model(User{}).Save(&user3) + DB.Model(User{Model: user3.Model}).Save(&user3) var result2 User if err := DB.First(&result2, "name = ?", "save3_").Error; err != nil || result2.ID != user3.ID { t.Fatalf("failed to find updated user") } - DB.Model(User{}).Save(&struct { + DB.Debug().Model(User{Model: user3.Model}).Save(&struct { gorm.Model Placeholder string Name string From eaa63d15e7ac3bab9ea2fd946b19e411ad261dc6 Mon Sep 17 00:00:00 2001 From: riverchu Date: Sun, 5 Sep 2021 23:12:24 +0800 Subject: [PATCH 42/51] feat: copy dest fields to model struct --- callbacks/update.go | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/callbacks/update.go b/callbacks/update.go index d85c4c22..ee60bcd7 100644 --- a/callbacks/update.go +++ b/callbacks/update.go @@ -23,11 +23,38 @@ func SetupUpdateReflectValue(db *gorm.DB) { rel.Field.Set(db.Statement.ReflectValue, dest[rel.Name]) } } + } else if modelType, destType := findType(db.Statement.Model), findType(db.Statement.Dest); modelType.Kind() == reflect.Struct && destType.Kind() == reflect.Struct { + db.Statement.Dest = transToModel(reflect.Indirect(reflect.ValueOf(db.Statement.Dest)), reflect.New(modelType).Elem()) } } } } +func findType(target interface{}) reflect.Type { + t := reflect.TypeOf(target) + if t.Kind() == reflect.Ptr { + return t.Elem() + } + return t +} + +func transToModel(from, to reflect.Value) interface{} { + if from.String() == to.String() { + return from.Interface() + } + + fromType := from.Type() + for i := 0; i < fromType.NumField(); i++ { + fieldName := fromType.Field(i).Name + fromField, toField := from.FieldByName(fieldName), to.FieldByName(fieldName) + if !toField.IsValid() || !toField.CanSet() || toField.Kind() != fromField.Kind() { + continue + } + toField.Set(fromField) + } + return to.Interface() +} + func BeforeUpdate(db *gorm.DB) { if db.Error == nil && db.Statement.Schema != nil && !db.Statement.SkipHooks && (db.Statement.Schema.BeforeSave || db.Statement.Schema.BeforeUpdate) { callMethod(db, func(value interface{}, tx *gorm.DB) (called bool) { @@ -227,7 +254,7 @@ func ConvertToAssignments(stmt *gorm.Statement) (set clause.Set) { set = make([]clause.Assignment, 0, len(stmt.Schema.FieldsByDBName)) for _, dbName := range stmt.Schema.DBNames { field := stmt.Schema.LookUpField(dbName) - if !field.PrimaryKey || (!updatingValue.CanAddr() || stmt.Dest != stmt.Model) { + if !field.PrimaryKey || !updatingValue.CanAddr() || stmt.Dest != stmt.Model { if v, ok := selectColumns[field.DBName]; (ok && v) || (!ok && (!restricted || (!stmt.SkipHooks && field.AutoUpdateTime > 0))) { value, isZero := field.ValueOf(updatingValue) if !stmt.SkipHooks && field.AutoUpdateTime > 0 { From 895c1178a0d1d837cd986c45eac62f6b10a6add4 Mon Sep 17 00:00:00 2001 From: Adrien Carreira Date: Thu, 8 Jul 2021 10:04:40 +0200 Subject: [PATCH 43/51] Proposal, Add Specific on for Joins queries --- callbacks/query.go | 47 ++++++++++++++++++++++++++-------------------- chainable_api.go | 6 ++++++ statement.go | 1 + 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/callbacks/query.go b/callbacks/query.go index 3299d015..e5f1250c 100644 --- a/callbacks/query.go +++ b/callbacks/query.go @@ -125,33 +125,40 @@ func BuildQuerySQL(db *gorm.DB) { }) } - exprs := make([]clause.Expression, len(relation.References)) - for idx, ref := range relation.References { - if ref.OwnPrimaryKey { - exprs[idx] = clause.Eq{ - Column: clause.Column{Table: clause.CurrentTable, Name: ref.PrimaryKey.DBName}, - Value: clause.Column{Table: tableAliasName, Name: ref.ForeignKey.DBName}, - } - } else { - if ref.PrimaryValue == "" { + if join.On != nil { + exprs := make([]clause.Expression, len(relation.References)) + for idx, ref := range relation.References { + if ref.OwnPrimaryKey { exprs[idx] = clause.Eq{ - Column: clause.Column{Table: clause.CurrentTable, Name: ref.ForeignKey.DBName}, - Value: clause.Column{Table: tableAliasName, Name: ref.PrimaryKey.DBName}, + Column: clause.Column{Table: clause.CurrentTable, Name: ref.PrimaryKey.DBName}, + Value: clause.Column{Table: tableAliasName, Name: ref.ForeignKey.DBName}, } } else { - exprs[idx] = clause.Eq{ - Column: clause.Column{Table: tableAliasName, Name: ref.ForeignKey.DBName}, - Value: ref.PrimaryValue, + if ref.PrimaryValue == "" { + exprs[idx] = clause.Eq{ + Column: clause.Column{Table: clause.CurrentTable, Name: ref.ForeignKey.DBName}, + Value: clause.Column{Table: tableAliasName, Name: ref.PrimaryKey.DBName}, + } + } else { + exprs[idx] = clause.Eq{ + Column: clause.Column{Table: tableAliasName, Name: ref.ForeignKey.DBName}, + Value: ref.PrimaryValue, + } } } } + joins = append(joins, clause.Join{ + Type: clause.LeftJoin, + Table: clause.Table{Name: relation.FieldSchema.Table, Alias: tableAliasName}, + ON: clause.Where{Exprs: exprs}, + }) + } else { + joins = append(joins, clause.Join{ + Type: clause.LeftJoin, + Table: clause.Table{Name: relation.FieldSchema.Table, Alias: tableAliasName}, + ON: clause.Where{Exprs: []clause.Expression{join.On}}, + }) } - - joins = append(joins, clause.Join{ - Type: clause.LeftJoin, - Table: clause.Table{Name: relation.FieldSchema.Table, Alias: tableAliasName}, - ON: clause.Where{Exprs: exprs}, - }) } else { joins = append(joins, clause.Join{ Expression: clause.NamedExpr{SQL: join.Name, Vars: join.Conds}, diff --git a/chainable_api.go b/chainable_api.go index 88279044..32943a83 100644 --- a/chainable_api.go +++ b/chainable_api.go @@ -177,6 +177,12 @@ func (db *DB) Joins(query string, args ...interface{}) (tx *DB) { return } +func (db *DB) JoinsOn(query string, on clause.Expression, args ...interface{}) (tx *DB) { + tx = db.getInstance() + tx.Statement.Joins = append(tx.Statement.Joins, join{Name: query, Conds: args, On: on}) + return +} + // Group specify the group method on the find func (db *DB) Group(name string) (tx *DB) { tx = db.getInstance() diff --git a/statement.go b/statement.go index 93b78c12..89824bc1 100644 --- a/statement.go +++ b/statement.go @@ -50,6 +50,7 @@ type Statement struct { type join struct { Name string Conds []interface{} + On clause.Expression } // StatementModifier statement modifier interface From 52cc438d07cef6975b3407594c612f8e856b88af Mon Sep 17 00:00:00 2001 From: Adrien Carreira Date: Sat, 17 Jul 2021 15:45:15 +0200 Subject: [PATCH 44/51] JoinsOn unit test + use all primary keys --- callbacks/query.go | 10 ++++++++-- chainable_api.go | 2 +- statement.go | 2 +- tests/joins_test.go | 20 ++++++++++++++++++++ utils/tests/models.go | 2 ++ 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/callbacks/query.go b/callbacks/query.go index e5f1250c..570a85d0 100644 --- a/callbacks/query.go +++ b/callbacks/query.go @@ -125,7 +125,7 @@ func BuildQuerySQL(db *gorm.DB) { }) } - if join.On != nil { + if join.On == nil { exprs := make([]clause.Expression, len(relation.References)) for idx, ref := range relation.References { if ref.OwnPrimaryKey { @@ -153,10 +153,16 @@ func BuildQuerySQL(db *gorm.DB) { ON: clause.Where{Exprs: exprs}, }) } else { + primaryFields := make([]clause.Column, len(relation.FieldSchema.PrimaryFieldDBNames)) + for idx, ref := range relation.FieldSchema.PrimaryFieldDBNames { + primaryFields[idx] = clause.Column{Table: tableAliasName, Name: ref} + } + + exprs := db.Statement.BuildCondition("(?) = (?)", primaryFields, join.On) joins = append(joins, clause.Join{ Type: clause.LeftJoin, Table: clause.Table{Name: relation.FieldSchema.Table, Alias: tableAliasName}, - ON: clause.Where{Exprs: []clause.Expression{join.On}}, + ON: clause.Where{Exprs: exprs}, }) } } else { diff --git a/chainable_api.go b/chainable_api.go index 32943a83..184931ff 100644 --- a/chainable_api.go +++ b/chainable_api.go @@ -177,7 +177,7 @@ func (db *DB) Joins(query string, args ...interface{}) (tx *DB) { return } -func (db *DB) JoinsOn(query string, on clause.Expression, args ...interface{}) (tx *DB) { +func (db *DB) JoinsOn(query string, on interface{}, args ...interface{}) (tx *DB) { tx = db.getInstance() tx.Statement.Joins = append(tx.Statement.Joins, join{Name: query, Conds: args, On: on}) return diff --git a/statement.go b/statement.go index 89824bc1..b21b8854 100644 --- a/statement.go +++ b/statement.go @@ -50,7 +50,7 @@ type Statement struct { type join struct { Name string Conds []interface{} - On clause.Expression + On interface{} } // StatementModifier statement modifier interface diff --git a/tests/joins_test.go b/tests/joins_test.go index 46611f5f..0b46d69c 100644 --- a/tests/joins_test.go +++ b/tests/joins_test.go @@ -104,6 +104,26 @@ func TestJoinConds(t *testing.T) { } } +func TestJoinOn(t *testing.T) { + var user = *GetUser("joins-on", Config{Pets: 2}) + DB.Save(&user) + + var user1 User + onQuery := DB.Select("id").Where("user_id = users.id AND name = ?", "joins-on_pet_1").Model(&Pet{}) + + if err := DB.JoinsOn("NamedPet", onQuery).Where("users.name = ?", user.Name).First(&user1).Error; err != nil { + t.Fatalf("Failed to load with joins on, got error: %v", err) + } + AssertEqual(t, user1.NamedPet.Name, "joins-on_pet_1") + + onQuery2 := DB.Select("id").Where("user_id = users.id AND name = ?", "joins-on_pet_2").Model(&Pet{}) + var user2 User + if err := DB.JoinsOn("NamedPet", onQuery2).Where("users.name = ?", user.Name).First(&user2).Error; err != nil { + t.Fatalf("Failed to load with joins on, got error: %v", err) + } + AssertEqual(t, user2.NamedPet.Name, "joins-on_pet_2") +} + func TestJoinsWithSelect(t *testing.T) { type result struct { ID uint diff --git a/utils/tests/models.go b/utils/tests/models.go index 2c5e71c0..8e833c93 100644 --- a/utils/tests/models.go +++ b/utils/tests/models.go @@ -11,6 +11,7 @@ import ( // He works in a Company (belongs to), he has a Manager (belongs to - single-table), and also managed a Team (has many - single-table) // He speaks many languages (many to many) and has many friends (many to many - single-table) // His pet also has one Toy (has one - polymorphic) +// NamedPet is a reference to a Named `Pets` (has many) type User struct { gorm.Model Name string @@ -18,6 +19,7 @@ type User struct { Birthday *time.Time Account Account Pets []*Pet + NamedPet *Pet Toys []Toy `gorm:"polymorphic:Owner"` CompanyID *int Company Company From c301aeb524234036192ceaca1a7bee18ce1de4fa Mon Sep 17 00:00:00 2001 From: Adrien Carreira Date: Sun, 18 Jul 2021 12:04:18 +0200 Subject: [PATCH 45/51] Refactor for readability --- callbacks/query.go | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/callbacks/query.go b/callbacks/query.go index 570a85d0..a4093c63 100644 --- a/callbacks/query.go +++ b/callbacks/query.go @@ -125,7 +125,19 @@ func BuildQuerySQL(db *gorm.DB) { }) } - if join.On == nil { + if join.On != nil { + primaryFields := make([]clause.Column, len(relation.FieldSchema.PrimaryFieldDBNames)) + for idx, ref := range relation.FieldSchema.PrimaryFieldDBNames { + primaryFields[idx] = clause.Column{Table: tableAliasName, Name: ref} + } + + exprs := db.Statement.BuildCondition("(?) = (?)", primaryFields, join.On) + joins = append(joins, clause.Join{ + Type: clause.LeftJoin, + Table: clause.Table{Name: relation.FieldSchema.Table, Alias: tableAliasName}, + ON: clause.Where{Exprs: exprs}, + }) + } else { exprs := make([]clause.Expression, len(relation.References)) for idx, ref := range relation.References { if ref.OwnPrimaryKey { @@ -147,18 +159,7 @@ func BuildQuerySQL(db *gorm.DB) { } } } - joins = append(joins, clause.Join{ - Type: clause.LeftJoin, - Table: clause.Table{Name: relation.FieldSchema.Table, Alias: tableAliasName}, - ON: clause.Where{Exprs: exprs}, - }) - } else { - primaryFields := make([]clause.Column, len(relation.FieldSchema.PrimaryFieldDBNames)) - for idx, ref := range relation.FieldSchema.PrimaryFieldDBNames { - primaryFields[idx] = clause.Column{Table: tableAliasName, Name: ref} - } - exprs := db.Statement.BuildCondition("(?) = (?)", primaryFields, join.On) joins = append(joins, clause.Join{ Type: clause.LeftJoin, Table: clause.Table{Name: relation.FieldSchema.Table, Alias: tableAliasName}, From d047f854e66b669785cbe6be8227269807db1782 Mon Sep 17 00:00:00 2001 From: Adrien Carreira Date: Sat, 28 Aug 2021 10:27:19 +0200 Subject: [PATCH 46/51] PR Comments --- chainable_api.go | 15 +++++++++------ tests/joins_test.go | 4 ++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/chainable_api.go b/chainable_api.go index 184931ff..8fd7ee3c 100644 --- a/chainable_api.go +++ b/chainable_api.go @@ -171,15 +171,18 @@ func (db *DB) Or(query interface{}, args ...interface{}) (tx *DB) { // Joins specify Joins conditions // db.Joins("Account").Find(&user) // db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Find(&user) +// db.Joins("Account", DB.Select("id").Where("user_id = users.id AND name = ?", "someName").Model(&Account{})) func (db *DB) Joins(query string, args ...interface{}) (tx *DB) { tx = db.getInstance() - tx.Statement.Joins = append(tx.Statement.Joins, join{Name: query, Conds: args}) - return -} -func (db *DB) JoinsOn(query string, on interface{}, args ...interface{}) (tx *DB) { - tx = db.getInstance() - tx.Statement.Joins = append(tx.Statement.Joins, join{Name: query, Conds: args, On: on}) + if len(args) > 0 { + if db, ok := args[0].(*DB); ok { + tx.Statement.Joins = append(tx.Statement.Joins, join{Name: query, Conds: args[1:], On: db}) + return + } + } + + tx.Statement.Joins = append(tx.Statement.Joins, join{Name: query, Conds: args}) return } diff --git a/tests/joins_test.go b/tests/joins_test.go index 0b46d69c..21c73c19 100644 --- a/tests/joins_test.go +++ b/tests/joins_test.go @@ -111,14 +111,14 @@ func TestJoinOn(t *testing.T) { var user1 User onQuery := DB.Select("id").Where("user_id = users.id AND name = ?", "joins-on_pet_1").Model(&Pet{}) - if err := DB.JoinsOn("NamedPet", onQuery).Where("users.name = ?", user.Name).First(&user1).Error; err != nil { + if err := DB.Joins("NamedPet", onQuery).Where("users.name = ?", user.Name).First(&user1).Error; err != nil { t.Fatalf("Failed to load with joins on, got error: %v", err) } AssertEqual(t, user1.NamedPet.Name, "joins-on_pet_1") onQuery2 := DB.Select("id").Where("user_id = users.id AND name = ?", "joins-on_pet_2").Model(&Pet{}) var user2 User - if err := DB.JoinsOn("NamedPet", onQuery2).Where("users.name = ?", user.Name).First(&user2).Error; err != nil { + if err := DB.Joins("NamedPet", onQuery2).Where("users.name = ?", user.Name).First(&user2).Error; err != nil { t.Fatalf("Failed to load with joins on, got error: %v", err) } AssertEqual(t, user2.NamedPet.Name, "joins-on_pet_2") From 3b6a7c8aecd66eb78e0f22710cc203b7abe0c894 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Tue, 7 Sep 2021 12:01:19 +0800 Subject: [PATCH 47/51] Update sqlserver driver --- tests/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/go.mod b/tests/go.mod index a1033a60..d7ab65ad 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -9,7 +9,7 @@ require ( gorm.io/driver/mysql v1.1.2 gorm.io/driver/postgres v1.1.0 gorm.io/driver/sqlite v1.1.4 - gorm.io/driver/sqlserver v1.0.8 + gorm.io/driver/sqlserver v1.0.9 gorm.io/gorm v1.21.14 ) From 6c94b07e98eca77e3ba1ca2e2341a5f5b75a0727 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Tue, 7 Sep 2021 15:30:14 +0800 Subject: [PATCH 48/51] try to fix fatal error: concurrent map read and map write --- schema/schema.go | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/schema/schema.go b/schema/schema.go index 0e0501d4..faba2e21 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -119,20 +119,13 @@ func Parse(dest interface{}, cacheStore *sync.Map, namer Namer) (*Schema, error) // When the schema initialization is completed, the channel will be closed defer close(schema.initialized) - if v, loaded := cacheStore.LoadOrStore(modelType, schema); loaded { + if v, loaded := cacheStore.Load(modelType); loaded { s := v.(*Schema) // Wait for the initialization of other goroutines to complete <-s.initialized return s, s.err } - defer func() { - if schema.err != nil { - logger.Default.Error(context.Background(), schema.err.Error()) - cacheStore.Delete(modelType) - } - }() - for i := 0; i < modelType.NumField(); i++ { if fieldStruct := modelType.Field(i); ast.IsExported(fieldStruct.Name) { if field := schema.ParseField(fieldStruct); field.EmbeddedSchema != nil { @@ -233,6 +226,20 @@ func Parse(dest interface{}, cacheStore *sync.Map, namer Namer) (*Schema, error) } } + if v, loaded := cacheStore.LoadOrStore(modelType, schema); loaded { + s := v.(*Schema) + // Wait for the initialization of other goroutines to complete + <-s.initialized + return s, s.err + } + + defer func() { + if schema.err != nil { + logger.Default.Error(context.Background(), schema.err.Error()) + cacheStore.Delete(modelType) + } + }() + if _, embedded := schema.cacheStore.Load(embeddedCacheKey); !embedded { for _, field := range schema.Fields { if field.DataType == "" && (field.Creatable || field.Updatable || field.Readable) { From ba16b2368f253572195de14fef62272a752595ef Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Tue, 7 Sep 2021 20:04:54 +0800 Subject: [PATCH 49/51] Refactor update record (#4679) --- callbacks/update.go | 81 +++++++++++++++++--------------------------- tests/update_test.go | 12 ++++--- 2 files changed, 40 insertions(+), 53 deletions(-) diff --git a/callbacks/update.go b/callbacks/update.go index ee60bcd7..7d5ea4a4 100644 --- a/callbacks/update.go +++ b/callbacks/update.go @@ -23,38 +23,11 @@ func SetupUpdateReflectValue(db *gorm.DB) { rel.Field.Set(db.Statement.ReflectValue, dest[rel.Name]) } } - } else if modelType, destType := findType(db.Statement.Model), findType(db.Statement.Dest); modelType.Kind() == reflect.Struct && destType.Kind() == reflect.Struct { - db.Statement.Dest = transToModel(reflect.Indirect(reflect.ValueOf(db.Statement.Dest)), reflect.New(modelType).Elem()) } } } } -func findType(target interface{}) reflect.Type { - t := reflect.TypeOf(target) - if t.Kind() == reflect.Ptr { - return t.Elem() - } - return t -} - -func transToModel(from, to reflect.Value) interface{} { - if from.String() == to.String() { - return from.Interface() - } - - fromType := from.Type() - for i := 0; i < fromType.NumField(); i++ { - fieldName := fromType.Field(i).Name - fromField, toField := from.FieldByName(fieldName), to.FieldByName(fieldName) - if !toField.IsValid() || !toField.CanSet() || toField.Kind() != fromField.Kind() { - continue - } - toField.Set(fromField) - } - return to.Interface() -} - func BeforeUpdate(db *gorm.DB) { if db.Error == nil && db.Statement.Schema != nil && !db.Statement.SkipHooks && (db.Statement.Schema.BeforeSave || db.Statement.Schema.BeforeUpdate) { callMethod(db, func(value interface{}, tx *gorm.DB) (called bool) { @@ -249,35 +222,45 @@ func ConvertToAssignments(stmt *gorm.Statement) (set clause.Set) { } } default: + var updatingSchema = stmt.Schema + if !updatingValue.CanAddr() || stmt.Dest != stmt.Model { + // different schema + updatingStmt := &gorm.Statement{DB: stmt.DB} + if err := updatingStmt.Parse(stmt.Dest); err == nil { + updatingSchema = updatingStmt.Schema + } + } + switch updatingValue.Kind() { case reflect.Struct: set = make([]clause.Assignment, 0, len(stmt.Schema.FieldsByDBName)) for _, dbName := range stmt.Schema.DBNames { - field := stmt.Schema.LookUpField(dbName) - if !field.PrimaryKey || !updatingValue.CanAddr() || stmt.Dest != stmt.Model { - if v, ok := selectColumns[field.DBName]; (ok && v) || (!ok && (!restricted || (!stmt.SkipHooks && field.AutoUpdateTime > 0))) { - value, isZero := field.ValueOf(updatingValue) - if !stmt.SkipHooks && field.AutoUpdateTime > 0 { - if field.AutoUpdateTime == schema.UnixNanosecond { - value = stmt.DB.NowFunc().UnixNano() - } else if field.AutoUpdateTime == schema.UnixMillisecond { - value = stmt.DB.NowFunc().UnixNano() / 1e6 - } else if field.GORMDataType == schema.Time { - value = stmt.DB.NowFunc() - } else { - value = stmt.DB.NowFunc().Unix() + if field := updatingSchema.LookUpField(dbName); field != nil && field.Updatable { + if !field.PrimaryKey || !updatingValue.CanAddr() || stmt.Dest != stmt.Model { + if v, ok := selectColumns[field.DBName]; (ok && v) || (!ok && (!restricted || (!stmt.SkipHooks && field.AutoUpdateTime > 0))) { + value, isZero := field.ValueOf(updatingValue) + if !stmt.SkipHooks && field.AutoUpdateTime > 0 { + if field.AutoUpdateTime == schema.UnixNanosecond { + value = stmt.DB.NowFunc().UnixNano() + } else if field.AutoUpdateTime == schema.UnixMillisecond { + value = stmt.DB.NowFunc().UnixNano() / 1e6 + } else if field.GORMDataType == schema.Time { + value = stmt.DB.NowFunc() + } else { + value = stmt.DB.NowFunc().Unix() + } + isZero = false } - isZero = false - } - if ok || !isZero { - set = append(set, clause.Assignment{Column: clause.Column{Name: field.DBName}, Value: value}) - assignValue(field, value) + if ok || !isZero { + set = append(set, clause.Assignment{Column: clause.Column{Name: field.DBName}, Value: value}) + assignValue(field, value) + } + } + } else { + if value, isZero := field.ValueOf(updatingValue); !isZero { + stmt.AddClause(clause.Where{Exprs: []clause.Expression{clause.Eq{Column: field.DBName, Value: value}}}) } - } - } else { - if value, isZero := field.ValueOf(updatingValue); !isZero { - stmt.AddClause(clause.Where{Exprs: []clause.Expression{clause.Eq{Column: field.DBName, Value: value}}}) } } } diff --git a/tests/update_test.go b/tests/update_test.go index 2a747ce5..9e5b630e 100644 --- a/tests/update_test.go +++ b/tests/update_test.go @@ -651,14 +651,16 @@ func TestSave(t *testing.T) { } user3.Name = "save3_" - DB.Model(User{Model: user3.Model}).Save(&user3) + if err := DB.Model(User{Model: user3.Model}).Save(&user3).Error; err != nil { + t.Fatalf("failed to save user, got %v", err) + } var result2 User if err := DB.First(&result2, "name = ?", "save3_").Error; err != nil || result2.ID != user3.ID { - t.Fatalf("failed to find updated user") + t.Fatalf("failed to find updated user, got %v", err) } - DB.Debug().Model(User{Model: user3.Model}).Save(&struct { + if err := DB.Model(User{Model: user3.Model}).Save(&struct { gorm.Model Placeholder string Name string @@ -666,7 +668,9 @@ func TestSave(t *testing.T) { Model: user3.Model, Placeholder: "placeholder", Name: "save3__", - }) + }).Error; err != nil { + t.Fatalf("failed to update user, got %v", err) + } var result3 User if err := DB.First(&result3, "name = ?", "save3__").Error; err != nil || result3.ID != user3.ID { From a16db07945e5f5acf348649debd2130dfcfeeb92 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Tue, 7 Sep 2021 21:21:44 +0800 Subject: [PATCH 50/51] Refactor Join ON --- callbacks/query.go | 69 +++++++++++++++++++++++---------------------- chainable_api.go | 4 ++- statement.go | 2 +- tests/joins_test.go | 5 ++-- 4 files changed, 42 insertions(+), 38 deletions(-) diff --git a/callbacks/query.go b/callbacks/query.go index a4093c63..1cfd618c 100644 --- a/callbacks/query.go +++ b/callbacks/query.go @@ -125,47 +125,48 @@ func BuildQuerySQL(db *gorm.DB) { }) } - if join.On != nil { - primaryFields := make([]clause.Column, len(relation.FieldSchema.PrimaryFieldDBNames)) - for idx, ref := range relation.FieldSchema.PrimaryFieldDBNames { - primaryFields[idx] = clause.Column{Table: tableAliasName, Name: ref} - } - - exprs := db.Statement.BuildCondition("(?) = (?)", primaryFields, join.On) - joins = append(joins, clause.Join{ - Type: clause.LeftJoin, - Table: clause.Table{Name: relation.FieldSchema.Table, Alias: tableAliasName}, - ON: clause.Where{Exprs: exprs}, - }) - } else { - exprs := make([]clause.Expression, len(relation.References)) - for idx, ref := range relation.References { - if ref.OwnPrimaryKey { + exprs := make([]clause.Expression, len(relation.References)) + for idx, ref := range relation.References { + if ref.OwnPrimaryKey { + exprs[idx] = clause.Eq{ + Column: clause.Column{Table: clause.CurrentTable, Name: ref.PrimaryKey.DBName}, + Value: clause.Column{Table: tableAliasName, Name: ref.ForeignKey.DBName}, + } + } else { + if ref.PrimaryValue == "" { exprs[idx] = clause.Eq{ - Column: clause.Column{Table: clause.CurrentTable, Name: ref.PrimaryKey.DBName}, - Value: clause.Column{Table: tableAliasName, Name: ref.ForeignKey.DBName}, + Column: clause.Column{Table: clause.CurrentTable, Name: ref.ForeignKey.DBName}, + Value: clause.Column{Table: tableAliasName, Name: ref.PrimaryKey.DBName}, } } else { - if ref.PrimaryValue == "" { - exprs[idx] = clause.Eq{ - Column: clause.Column{Table: clause.CurrentTable, Name: ref.ForeignKey.DBName}, - Value: clause.Column{Table: tableAliasName, Name: ref.PrimaryKey.DBName}, - } - } else { - exprs[idx] = clause.Eq{ - Column: clause.Column{Table: tableAliasName, Name: ref.ForeignKey.DBName}, - Value: ref.PrimaryValue, - } + exprs[idx] = clause.Eq{ + Column: clause.Column{Table: tableAliasName, Name: ref.ForeignKey.DBName}, + Value: ref.PrimaryValue, } } } - - joins = append(joins, clause.Join{ - Type: clause.LeftJoin, - Table: clause.Table{Name: relation.FieldSchema.Table, Alias: tableAliasName}, - ON: clause.Where{Exprs: exprs}, - }) } + + if join.On != nil { + onStmt := gorm.Statement{Table: tableAliasName, DB: db} + join.On.Build(&onStmt) + onSQL := onStmt.SQL.String() + vars := onStmt.Vars + for idx, v := range onStmt.Vars { + bindvar := strings.Builder{} + onStmt.Vars = vars[0 : idx+1] + db.Dialector.BindVarTo(&bindvar, &onStmt, v) + onSQL = strings.Replace(onSQL, bindvar.String(), "?", 1) + } + + exprs = append(exprs, clause.Expr{SQL: onSQL, Vars: vars}) + } + + joins = append(joins, clause.Join{ + Type: clause.LeftJoin, + Table: clause.Table{Name: relation.FieldSchema.Table, Alias: tableAliasName}, + ON: clause.Where{Exprs: exprs}, + }) } else { joins = append(joins, clause.Join{ Expression: clause.NamedExpr{SQL: join.Name, Vars: join.Conds}, diff --git a/chainable_api.go b/chainable_api.go index 8fd7ee3c..01ab2597 100644 --- a/chainable_api.go +++ b/chainable_api.go @@ -177,7 +177,9 @@ func (db *DB) Joins(query string, args ...interface{}) (tx *DB) { if len(args) > 0 { if db, ok := args[0].(*DB); ok { - tx.Statement.Joins = append(tx.Statement.Joins, join{Name: query, Conds: args[1:], On: db}) + if where, ok := db.Statement.Clauses["WHERE"].Expression.(clause.Where); ok { + tx.Statement.Joins = append(tx.Statement.Joins, join{Name: query, Conds: args[1:], On: &where}) + } return } } diff --git a/statement.go b/statement.go index b21b8854..38363443 100644 --- a/statement.go +++ b/statement.go @@ -50,7 +50,7 @@ type Statement struct { type join struct { Name string Conds []interface{} - On interface{} + On *clause.Where } // StatementModifier statement modifier interface diff --git a/tests/joins_test.go b/tests/joins_test.go index 21c73c19..e560f38a 100644 --- a/tests/joins_test.go +++ b/tests/joins_test.go @@ -109,14 +109,15 @@ func TestJoinOn(t *testing.T) { DB.Save(&user) var user1 User - onQuery := DB.Select("id").Where("user_id = users.id AND name = ?", "joins-on_pet_1").Model(&Pet{}) + onQuery := DB.Where(&Pet{Name: "joins-on_pet_1"}) if err := DB.Joins("NamedPet", onQuery).Where("users.name = ?", user.Name).First(&user1).Error; err != nil { t.Fatalf("Failed to load with joins on, got error: %v", err) } + AssertEqual(t, user1.NamedPet.Name, "joins-on_pet_1") - onQuery2 := DB.Select("id").Where("user_id = users.id AND name = ?", "joins-on_pet_2").Model(&Pet{}) + onQuery2 := DB.Where(&Pet{Name: "joins-on_pet_2"}) var user2 User if err := DB.Joins("NamedPet", onQuery2).Where("users.name = ?", user.Name).First(&user2).Error; err != nil { t.Fatalf("Failed to load with joins on, got error: %v", err) From 04f049c1dac757c6fb93df863a9585b98fc8661b Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Thu, 9 Sep 2021 11:22:55 +0800 Subject: [PATCH 51/51] Add tests for RowsAffected --- tests/delete_test.go | 4 ++-- tests/update_test.go | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/delete_test.go b/tests/delete_test.go index abe85b0e..f62cc606 100644 --- a/tests/delete_test.go +++ b/tests/delete_test.go @@ -22,8 +22,8 @@ func TestDelete(t *testing.T) { } } - if err := DB.Delete(&users[1]).Error; err != nil { - t.Errorf("errors happened when delete: %v", err) + if res := DB.Delete(&users[1]); res.Error != nil || res.RowsAffected != 1 { + t.Errorf("errors happened when delete: %v, affected: %v", res.Error, res.RowsAffected) } var result User diff --git a/tests/update_test.go b/tests/update_test.go index 9e5b630e..631d0d6d 100644 --- a/tests/update_test.go +++ b/tests/update_test.go @@ -69,8 +69,10 @@ func TestUpdate(t *testing.T) { } values := map[string]interface{}{"Active": true, "age": 5} - if err := DB.Model(user).Updates(values).Error; err != nil { - t.Errorf("errors happened when update: %v", err) + if res := DB.Model(user).Updates(values); res.Error != nil { + t.Errorf("errors happened when update: %v", res.Error) + } else if res.RowsAffected != 1 { + t.Errorf("rows affected should be 1, but got : %v", res.RowsAffected) } else if user.Age != 5 { t.Errorf("Age should equals to 5, but got %v", user.Age) } else if user.Active != true { @@ -131,7 +133,10 @@ func TestUpdates(t *testing.T) { lastUpdatedAt := users[0].UpdatedAt // update with map - DB.Model(users[0]).Updates(map[string]interface{}{"name": "updates_01_newname", "age": 100}) + if res := DB.Model(users[0]).Updates(map[string]interface{}{"name": "updates_01_newname", "age": 100}); res.Error != nil || res.RowsAffected != 1 { + t.Errorf("Failed to update users") + } + if users[0].Name != "updates_01_newname" || users[0].Age != 100 { t.Errorf("Record should be updated also with map") }