From eac6d1bdb9f4b1e04b663dbc8b211f1ffd9217cf Mon Sep 17 00:00:00 2001 From: EricZhou Date: Wed, 24 Jun 2020 16:20:12 +0800 Subject: [PATCH 01/20] issue --- .github/labeler.yml | 6 ++++++ .github/workflows/issue.yml | 15 +++++++++++++++ .github/workflows/issue_stale.yml | 19 +++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/issue.yml create mode 100644 .github/workflows/issue_stale.yml diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 00000000..d96bafa0 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,6 @@ +# Add/remove 'critical' label if issue contains the words 'urgent' or 'critical' +HasGormPlaygroundTestCase: + - '(github.com/go-gorm/playground/pull/\d)' + +NoTestCase: + - '(change this to your link)' diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml new file mode 100644 index 00000000..0759782c --- /dev/null +++ b/.github/workflows/issue.yml @@ -0,0 +1,15 @@ +name: "Issue-Labeler" +on: + issues: + types: [opened, edited] + +jobs: + triage: + runs-on: ubuntu-latest + steps: + - uses: github/issue-labeler@v2.0 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + configuration-path: ".github/labeler.yml" + not-before: "2020-01-15T02:54:32Z" + enable-versioned-regex: 0 \ No newline at end of file diff --git a/.github/workflows/issue_stale.yml b/.github/workflows/issue_stale.yml new file mode 100644 index 00000000..fadfb522 --- /dev/null +++ b/.github/workflows/issue_stale.yml @@ -0,0 +1,19 @@ +name: Issue cleanup +on: + schedule: + - cron: '0 1 * * *' # At 01:00, everyday +jobs: + triage_issues: + name: Issue triage + runs-on: ubuntu-latest + steps: + - name: Find old issues and mark them stale + uses: Krizzu/issue-triage-action@v1.0.0 + with: + ghToken: ${{ secrets.GITHUB_TOKEN }} + staleAfter: 7 + closeAfter: 14 + staleLabel: "STALE 📺" + staleComment: "This issue is %DAYS_OLD% days old, marking as stale! cc: @%AUTHOR%" + closeComment: "Issue last updated %DAYS_OLD% days ago! Closing down!" + showLogs: true \ No newline at end of file From 630f4fe03f9d2fd93ed3dcc0ec248c8c76c05cd5 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Wed, 24 Jun 2020 16:43:53 +0800 Subject: [PATCH 02/20] Create join table with ReorderModels --- migrator/migrator.go | 37 +++++++++----------------------- tests/multi_primary_keys_test.go | 27 +++++++++++++++++++---- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/migrator/migrator.go b/migrator/migrator.go index c8fe17ab..799bf433 100644 --- a/migrator/migrator.go +++ b/migrator/migrator.go @@ -116,20 +116,6 @@ func (m Migrator) AutoMigrate(values ...interface{}) error { } } } - - // create join table - if rel.JoinTable != nil { - joinValue := reflect.New(rel.JoinTable.ModelType).Interface() - if !tx.Migrator().HasTable(rel.JoinTable.Table) { - defer func(table string, joinValue interface{}) { - errr = tx.Table(table).Migrator().CreateTable(joinValue) - }(rel.JoinTable.Table, joinValue) - } else { - defer func(table string, joinValue interface{}) { - errr = tx.Table(table).Migrator().AutoMigrate(joinValue) - }(rel.JoinTable.Table, joinValue) - } - } } return nil }); err != nil { @@ -193,16 +179,6 @@ func (m Migrator) CreateTable(values ...interface{}) error { } } } - - // create join table - if rel.JoinTable != nil { - joinValue := reflect.New(rel.JoinTable.ModelType).Interface() - if !tx.Migrator().HasTable(rel.JoinTable.Table) { - defer func(table string, joinValue interface{}) { - errr = tx.Table(table).Migrator().CreateTable(joinValue) - }(rel.JoinTable.Table, joinValue) - } - } } for _, chk := range stmt.Schema.ParseCheckConstraints() { @@ -551,9 +527,10 @@ func (m Migrator) ReorderModels(values []interface{}, autoAdd bool) (results []i orderedModelNamesMap = map[string]bool{} valuesMap = map[string]Dependency{} insertIntoOrderedList func(name string) + parseDependence func(value interface{}, addToList bool) ) - parseDependence := func(value interface{}, addToList bool) { + parseDependence = func(value interface{}, addToList bool) { dep := Dependency{ Statement: &gorm.Statement{DB: m.DB, Dest: value}, } @@ -564,8 +541,14 @@ func (m Migrator) ReorderModels(values []interface{}, autoAdd bool) (results []i dep.Depends = append(dep.Depends, c.ReferenceSchema) } - if rel.JoinTable != nil && rel.Schema != rel.FieldSchema { - dep.Depends = append(dep.Depends, rel.FieldSchema) + if rel.JoinTable != nil { + if rel.Schema != rel.FieldSchema { + dep.Depends = append(dep.Depends, rel.FieldSchema) + } + // append join value + defer func(joinValue interface{}) { + parseDependence(joinValue, autoAdd) + }(reflect.New(rel.JoinTable.ModelType).Interface()) } } diff --git a/tests/multi_primary_keys_test.go b/tests/multi_primary_keys_test.go index 05267bbb..617010c5 100644 --- a/tests/multi_primary_keys_test.go +++ b/tests/multi_primary_keys_test.go @@ -4,6 +4,8 @@ import ( "reflect" "sort" "testing" + + "gorm.io/gorm" ) type Blog struct { @@ -11,7 +13,7 @@ type Blog struct { Locale string `gorm:"primary_key"` Subject string Body string - Tags []Tag `gorm:"many2many:blog_tags;"` + Tags []Tag `gorm:"many2many:blogs_tags;"` SharedTags []Tag `gorm:"many2many:shared_blog_tags;ForeignKey:id;References:id"` LocaleTags []Tag `gorm:"many2many:locale_blog_tags;ForeignKey:id,locale;References:id"` } @@ -38,7 +40,16 @@ func TestManyToManyWithMultiPrimaryKeys(t *testing.T) { t.Skip("skip sqlite, sqlserver due to it doesn't support multiple primary keys with auto increment") } - DB.Migrator().DropTable(&Blog{}, &Tag{}, "blog_tags") + if name := DB.Dialector.Name(); name == "postgres" { + stmt := gorm.Statement{DB: DB} + stmt.Parse(&Blog{}) + stmt.Schema.LookUpField("ID").Unique = true + stmt.Parse(&Tag{}) + stmt.Schema.LookUpField("ID").Unique = true + // postgers only allow unique constraint matching given keys + } + + DB.Migrator().DropTable(&Blog{}, &Tag{}, "blog_tags", "locale_blog_tags", "shared_blog_tags") if err := DB.AutoMigrate(&Blog{}, &Tag{}); err != nil { t.Fatalf("Failed to auto migrate, got error: %v", err) } @@ -127,7 +138,11 @@ func TestManyToManyWithCustomizedForeignKeys(t *testing.T) { t.Skip("skip sqlite, sqlserver due to it doesn't support multiple primary keys with auto increment") } - DB.Migrator().DropTable(&Blog{}, &Tag{}, "blog_tags") + if name := DB.Dialector.Name(); name == "postgres" { + t.Skip("skip postgers due to it only allow unique constraint matching given keys") + } + + DB.Migrator().DropTable(&Blog{}, &Tag{}, "blog_tags", "locale_blog_tags", "shared_blog_tags") if err := DB.AutoMigrate(&Blog{}, &Tag{}); err != nil { t.Fatalf("Failed to auto migrate, got error: %v", err) } @@ -248,7 +263,11 @@ func TestManyToManyWithCustomizedForeignKeys2(t *testing.T) { t.Skip("skip sqlite, sqlserver due to it doesn't support multiple primary keys with auto increment") } - DB.Migrator().DropTable(&Blog{}, &Tag{}, "blog_tags") + if name := DB.Dialector.Name(); name == "postgres" { + t.Skip("skip postgers due to it only allow unique constraint matching given keys") + } + + DB.Migrator().DropTable(&Blog{}, &Tag{}, "blog_tags", "locale_blog_tags", "shared_blog_tags") if err := DB.AutoMigrate(&Blog{}, &Tag{}); err != nil { t.Fatalf("Failed to auto migrate, got error: %v", err) } From 6b92bca6648ebb9137339b7347ae82ac8a462754 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Wed, 24 Jun 2020 19:09:19 +0800 Subject: [PATCH 03/20] Update test script --- tests/main_test.go | 4 ++++ tests/tests_all.sh | 14 ++++---------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/tests/main_test.go b/tests/main_test.go index 9d933caf..5b8c7dbb 100644 --- a/tests/main_test.go +++ b/tests/main_test.go @@ -7,6 +7,10 @@ import ( ) func TestExceptionsWithInvalidSql(t *testing.T) { + if name := DB.Dialector.Name(); name == "sqlserver" { + t.Skip("skip sqlserver due to it will raise data race for invalid sql") + } + var columns []string if DB.Where("sdsd.zaaa = ?", "sd;;;aa").Pluck("aaa", &columns).Error == nil { t.Errorf("Should got error with invalid SQL") diff --git a/tests/tests_all.sh b/tests/tests_all.sh index 47f25401..e87ff045 100755 --- a/tests/tests_all.sh +++ b/tests/tests_all.sh @@ -19,27 +19,21 @@ for dialect in "${dialects[@]}" ; do then echo "testing ${dialect}..." - race="" - if [ "$GORM_DIALECT" = "sqlserver" ] - then - race="-race" - fi - if [ "$GORM_VERBOSE" = "" ] then - GORM_DIALECT=${dialect} go test $race -count=1 ./... + GORM_DIALECT=${dialect} go test -race -count=1 ./... if [ -d tests ] then cd tests - GORM_DIALECT=${dialect} go test $race -count=1 ./... + GORM_DIALECT=${dialect} go test -race -count=1 ./... cd .. fi else - GORM_DIALECT=${dialect} go test $race -count=1 -v ./... + GORM_DIALECT=${dialect} go test -race -count=1 -v ./... if [ -d tests ] then cd tests - GORM_DIALECT=${dialect} go test $race -count=1 -v ./... + GORM_DIALECT=${dialect} go test -race -count=1 -v ./... cd .. fi fi From 19f56ddc2a212019a950c6ef81e55950342b713a Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Wed, 24 Jun 2020 20:19:28 +0800 Subject: [PATCH 04/20] Upgrade default mysql driver --- tests/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/go.mod b/tests/go.mod index abe32cd6..f4d93ecb 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -6,7 +6,7 @@ require ( github.com/google/uuid v1.1.1 github.com/jinzhu/now v1.1.1 github.com/lib/pq v1.6.0 - gorm.io/driver/mysql v0.2.3 + gorm.io/driver/mysql v0.2.6 gorm.io/driver/postgres v0.2.3 gorm.io/driver/sqlite v1.0.7 gorm.io/driver/sqlserver v0.2.2 From 4cbd99aa94d04292ac369fd9abe3b1a78d6d7fe6 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Thu, 25 Jun 2020 06:38:07 +0800 Subject: [PATCH 05/20] Add default value test --- tests/default_value_test.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/default_value_test.go diff --git a/tests/default_value_test.go b/tests/default_value_test.go new file mode 100644 index 00000000..52292cf7 --- /dev/null +++ b/tests/default_value_test.go @@ -0,0 +1,37 @@ +package tests_test + +import ( + "testing" + + "gorm.io/gorm" +) + +func TestDefaultValue(t *testing.T) { + type Harumph struct { + gorm.Model + Email string `gorm:"not null;"` + Name string `gorm:"not null;default:foo"` + Name2 string `gorm:"not null;default:'foo'"` + Age int `gorm:"default:18"` + } + + DB.Migrator().DropTable(&Harumph{}) + + if err := DB.AutoMigrate(&Harumph{}); err != nil { + t.Fatalf("Failed to migrate with default value, got error: %v", err) + } + + var harumph = Harumph{Email: "hello@gorm.io"} + if err := DB.Create(&harumph).Error; err != nil { + t.Fatalf("Failed to create data with default value, got error: %v", err) + } else if harumph.Name != "foo" || harumph.Name2 != "foo" || harumph.Age != 18 { + t.Fatalf("Failed to create data with default value, got: %+v", harumph) + } + + var result Harumph + if err := DB.First(&result, "email = ?", "hello@gorm.io").Error; err != nil { + t.Fatalf("Failed to find created data, got error: %v", err) + } else if result.Name != "foo" || result.Name2 != "foo" || result.Age != 18 { + t.Fatalf("Failed to find created data with default data, got %+v", result) + } +} From dcdcc6fedc9e55ca6ebec4e8676cbdb238fc955f Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Thu, 25 Jun 2020 08:00:10 +0800 Subject: [PATCH 06/20] Fix create with default value --- migrator/migrator.go | 8 ++++---- tests/default_value_test.go | 13 +++++++------ tests/go.mod | 2 ++ 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/migrator/migrator.go b/migrator/migrator.go index 799bf433..9c4ce2d5 100644 --- a/migrator/migrator.go +++ b/migrator/migrator.go @@ -65,10 +65,10 @@ func (m Migrator) FullDataTypeOf(field *schema.Field) (expr clause.Expr) { } if field.HasDefaultValue && field.DefaultValue != "" { - if field.DataType == schema.String && field.DefaultValueInterface != nil { - defaultStmt := &gorm.Statement{Vars: []interface{}{field.DefaultValue}} - m.Dialector.BindVarTo(defaultStmt, defaultStmt, field.DefaultValue) - expr.SQL += " DEFAULT " + m.Dialector.Explain(defaultStmt.SQL.String(), field.DefaultValue) + if field.DefaultValueInterface != nil { + defaultStmt := &gorm.Statement{Vars: []interface{}{field.DefaultValueInterface}} + m.Dialector.BindVarTo(defaultStmt, defaultStmt, field.DefaultValueInterface) + expr.SQL += " DEFAULT " + m.Dialector.Explain(defaultStmt.SQL.String(), field.DefaultValueInterface) } else { expr.SQL += " DEFAULT " + field.DefaultValue } diff --git a/tests/default_value_test.go b/tests/default_value_test.go index 52292cf7..28a456d3 100644 --- a/tests/default_value_test.go +++ b/tests/default_value_test.go @@ -9,10 +9,11 @@ import ( func TestDefaultValue(t *testing.T) { type Harumph struct { gorm.Model - Email string `gorm:"not null;"` - Name string `gorm:"not null;default:foo"` - Name2 string `gorm:"not null;default:'foo'"` - Age int `gorm:"default:18"` + Email string `gorm:"not null;index:,unique"` + Name string `gorm:"not null;default:'foo'"` + Name2 string `gorm:"not null;default:'foo'"` + Age int `gorm:"default:18"` + Enabled bool `gorm:"default:true"` } DB.Migrator().DropTable(&Harumph{}) @@ -24,14 +25,14 @@ func TestDefaultValue(t *testing.T) { var harumph = Harumph{Email: "hello@gorm.io"} if err := DB.Create(&harumph).Error; err != nil { t.Fatalf("Failed to create data with default value, got error: %v", err) - } else if harumph.Name != "foo" || harumph.Name2 != "foo" || harumph.Age != 18 { + } else if harumph.Name != "foo" || harumph.Name2 != "foo" || harumph.Age != 18 || !harumph.Enabled { t.Fatalf("Failed to create data with default value, got: %+v", harumph) } var result Harumph if err := DB.First(&result, "email = ?", "hello@gorm.io").Error; err != nil { t.Fatalf("Failed to find created data, got error: %v", err) - } else if result.Name != "foo" || result.Name2 != "foo" || result.Age != 18 { + } else if result.Name != "foo" || result.Name2 != "foo" || result.Age != 18 || !result.Enabled { t.Fatalf("Failed to find created data with default data, got %+v", result) } } diff --git a/tests/go.mod b/tests/go.mod index f4d93ecb..d43ee8f1 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -14,3 +14,5 @@ require ( ) replace gorm.io/gorm => ../ + +replace gorm.io/driver/sqlserver => /Users/jinzhu/Projects/sqlserver From c888560a0e9971b174f7232cb847d3dc38229575 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Thu, 25 Jun 2020 08:08:37 +0800 Subject: [PATCH 07/20] Fix go.mod --- tests/default_value_test.go | 2 +- tests/go.mod | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/default_value_test.go b/tests/default_value_test.go index 28a456d3..7a7790bc 100644 --- a/tests/default_value_test.go +++ b/tests/default_value_test.go @@ -11,7 +11,7 @@ func TestDefaultValue(t *testing.T) { gorm.Model Email string `gorm:"not null;index:,unique"` Name string `gorm:"not null;default:'foo'"` - Name2 string `gorm:"not null;default:'foo'"` + Name2 string `gorm:"size:233;not null;default:'foo'"` Age int `gorm:"default:18"` Enabled bool `gorm:"default:true"` } diff --git a/tests/go.mod b/tests/go.mod index d43ee8f1..955bafe2 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -9,10 +9,8 @@ require ( gorm.io/driver/mysql v0.2.6 gorm.io/driver/postgres v0.2.3 gorm.io/driver/sqlite v1.0.7 - gorm.io/driver/sqlserver v0.2.2 + gorm.io/driver/sqlserver v0.2.3 gorm.io/gorm v0.2.9 ) replace gorm.io/gorm => ../ - -replace gorm.io/driver/sqlserver => /Users/jinzhu/Projects/sqlserver From af632199cf92c8609975a48a66a8be976a077d96 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Thu, 25 Jun 2020 22:48:10 +0800 Subject: [PATCH 08/20] Test set string field's default value to blank string --- migrator/migrator.go | 2 +- tests/default_value_test.go | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/migrator/migrator.go b/migrator/migrator.go index 9c4ce2d5..5edd800e 100644 --- a/migrator/migrator.go +++ b/migrator/migrator.go @@ -64,7 +64,7 @@ func (m Migrator) FullDataTypeOf(field *schema.Field) (expr clause.Expr) { expr.SQL += " UNIQUE" } - if field.HasDefaultValue && field.DefaultValue != "" { + if field.HasDefaultValue && (field.DefaultValueInterface != nil || field.DefaultValue != "") { if field.DefaultValueInterface != nil { defaultStmt := &gorm.Statement{Vars: []interface{}{field.DefaultValueInterface}} m.Dialector.BindVarTo(defaultStmt, defaultStmt, field.DefaultValueInterface) diff --git a/tests/default_value_test.go b/tests/default_value_test.go index 7a7790bc..ea496d60 100644 --- a/tests/default_value_test.go +++ b/tests/default_value_test.go @@ -12,6 +12,7 @@ func TestDefaultValue(t *testing.T) { Email string `gorm:"not null;index:,unique"` Name string `gorm:"not null;default:'foo'"` Name2 string `gorm:"size:233;not null;default:'foo'"` + Name3 string `gorm:"size:233;not null;default:''"` Age int `gorm:"default:18"` Enabled bool `gorm:"default:true"` } @@ -25,14 +26,14 @@ func TestDefaultValue(t *testing.T) { var harumph = Harumph{Email: "hello@gorm.io"} if err := DB.Create(&harumph).Error; err != nil { t.Fatalf("Failed to create data with default value, got error: %v", err) - } else if harumph.Name != "foo" || harumph.Name2 != "foo" || harumph.Age != 18 || !harumph.Enabled { + } else if harumph.Name != "foo" || harumph.Name2 != "foo" || harumph.Name3 != "" || harumph.Age != 18 || !harumph.Enabled { t.Fatalf("Failed to create data with default value, got: %+v", harumph) } var result Harumph if err := DB.First(&result, "email = ?", "hello@gorm.io").Error; err != nil { t.Fatalf("Failed to find created data, got error: %v", err) - } else if result.Name != "foo" || result.Name2 != "foo" || result.Age != 18 || !result.Enabled { + } else if result.Name != "foo" || result.Name2 != "foo" || result.Name3 != "" || result.Age != 18 || !result.Enabled { t.Fatalf("Failed to find created data with default data, got %+v", result) } } From 81f4fafae4c6a4237d8ad25d1b55340652d0c066 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Thu, 25 Jun 2020 23:37:49 +0800 Subject: [PATCH 09/20] Test group by with multiple columns --- tests/group_by_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/group_by_test.go b/tests/group_by_test.go index cb4c4f43..b08f48f1 100644 --- a/tests/group_by_test.go +++ b/tests/group_by_test.go @@ -11,6 +11,7 @@ func TestGroupBy(t *testing.T) { Name: "groupby", Age: 10, Birthday: Now(), + Active: true, }, { Name: "groupby", Age: 20, @@ -19,6 +20,7 @@ func TestGroupBy(t *testing.T) { Name: "groupby", Age: 30, Birthday: Now(), + Active: true, }, { Name: "groupby1", Age: 110, @@ -27,10 +29,12 @@ func TestGroupBy(t *testing.T) { Name: "groupby1", Age: 220, Birthday: Now(), + Active: true, }, { Name: "groupby1", Age: 330, Birthday: Now(), + Active: true, }} if err := DB.Create(&users).Error; err != nil { @@ -54,4 +58,13 @@ func TestGroupBy(t *testing.T) { if name != "groupby1" || total != 660 { t.Errorf("name should be groupby, but got %v, total should be 660, but got %v", name, total) } + + var active bool + if err := DB.Model(&User{}).Select("name, active, sum(age)").Where("name = ? and active = ?", "groupby", true).Group("name").Group("active").Row().Scan(&name, &active, &total); err != nil { + t.Errorf("no error should happen, but got %v", err) + } + + if name != "groupby" || active != true || total != 40 { + t.Errorf("group by two columns, name %v, age %v, active: %v", name, total, active) + } } From a550a058823234587dc53a815e158be2c9355424 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Fri, 26 Jun 2020 07:26:45 +0800 Subject: [PATCH 10/20] Set db type after autotime --- schema/field.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/schema/field.go b/schema/field.go index a8328367..f02968fa 100644 --- a/schema/field.go +++ b/schema/field.go @@ -223,15 +223,6 @@ func (schema *Schema) ParseField(fieldStruct reflect.StructField) *Field { field.DataType = DataType(dataTyper.GormDataType()) } - if val, ok := field.TagSettings["TYPE"]; ok { - switch DataType(strings.ToLower(val)) { - case Bool, Int, Uint, Float, String, Time, Bytes: - field.DataType = DataType(strings.ToLower(val)) - default: - field.DataType = DataType(val) - } - } - if v, ok := field.TagSettings["AUTOCREATETIME"]; ok || (field.Name == "CreatedAt" && (field.DataType == Time || field.DataType == Int || field.DataType == Uint)) { if strings.ToUpper(v) == "NANO" { field.AutoCreateTime = UnixNanosecond @@ -248,6 +239,15 @@ func (schema *Schema) ParseField(fieldStruct reflect.StructField) *Field { } } + if val, ok := field.TagSettings["TYPE"]; ok { + switch DataType(strings.ToLower(val)) { + case Bool, Int, Uint, Float, String, Time, Bytes: + field.DataType = DataType(strings.ToLower(val)) + default: + field.DataType = DataType(val) + } + } + if field.Size == 0 { switch reflect.Indirect(fieldValue).Kind() { case reflect.Int, reflect.Int64, reflect.Uint, reflect.Uint64, reflect.Float64: From d5d31b38a7442f44da356cc413ad4afb30fa1abb Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Fri, 26 Jun 2020 08:39:18 +0800 Subject: [PATCH 11/20] Test group with table name --- tests/go.mod | 8 ++++---- tests/group_by_test.go | 8 ++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/go.mod b/tests/go.mod index 955bafe2..c467f34b 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -6,10 +6,10 @@ require ( github.com/google/uuid v1.1.1 github.com/jinzhu/now v1.1.1 github.com/lib/pq v1.6.0 - gorm.io/driver/mysql v0.2.6 - gorm.io/driver/postgres v0.2.3 - gorm.io/driver/sqlite v1.0.7 - gorm.io/driver/sqlserver v0.2.3 + gorm.io/driver/mysql v0.2.7 + gorm.io/driver/postgres v0.2.4 + gorm.io/driver/sqlite v1.0.8 + gorm.io/driver/sqlserver v0.2.4 gorm.io/gorm v0.2.9 ) diff --git a/tests/group_by_test.go b/tests/group_by_test.go index b08f48f1..6d0ed39c 100644 --- a/tests/group_by_test.go +++ b/tests/group_by_test.go @@ -51,6 +51,14 @@ func TestGroupBy(t *testing.T) { t.Errorf("name should be groupby, but got %v, total should be 60, but got %v", name, total) } + if err := DB.Model(&User{}).Select("name, sum(age)").Where("name = ?", "groupby").Group("users.name").Row().Scan(&name, &total); err != nil { + t.Errorf("no error should happen, but got %v", err) + } + + if name != "groupby" || total != 60 { + t.Errorf("name should be groupby, but got %v, total should be 60, but got %v", name, total) + } + if err := DB.Model(&User{}).Select("name, sum(age) as total").Where("name LIKE ?", "groupby%").Group("name").Having("name = ?", "groupby1").Row().Scan(&name, &total); err != nil { t.Errorf("no error should happen, but got %v", err) } From eeee014500669387fb0442ebbed1556a04bad8c5 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sat, 27 Jun 2020 08:04:12 +0800 Subject: [PATCH 12/20] Only query with readable fields --- statement.go | 24 ++++++++++++++---------- tests/customize_field_test.go | 8 ++++++++ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/statement.go b/statement.go index 7cc01bb8..e902b739 100644 --- a/statement.go +++ b/statement.go @@ -271,22 +271,26 @@ func (stmt *Statement) BuildCondition(query interface{}, args ...interface{}) (c switch reflectValue.Kind() { case reflect.Struct: for _, field := range s.Fields { - if v, isZero := field.ValueOf(reflectValue); !isZero { - if field.DBName == "" { - conds = append(conds, clause.Eq{Column: clause.Column{Table: s.Table, Name: field.Name}, Value: v}) - } else { - conds = append(conds, clause.Eq{Column: clause.Column{Table: s.Table, Name: field.DBName}, Value: v}) + if field.Readable { + if v, isZero := field.ValueOf(reflectValue); !isZero { + if field.DBName == "" { + conds = append(conds, clause.Eq{Column: clause.Column{Table: s.Table, Name: field.Name}, Value: v}) + } else { + conds = append(conds, clause.Eq{Column: clause.Column{Table: s.Table, Name: field.DBName}, Value: v}) + } } } } case reflect.Slice, reflect.Array: for i := 0; i < reflectValue.Len(); i++ { for _, field := range s.Fields { - if v, isZero := field.ValueOf(reflectValue.Index(i)); !isZero { - if field.DBName == "" { - conds = append(conds, clause.Eq{Column: clause.Column{Table: s.Table, Name: field.Name}, Value: v}) - } else { - conds = append(conds, clause.Eq{Column: clause.Column{Table: s.Table, Name: field.DBName}, Value: v}) + if field.Readable { + if v, isZero := field.ValueOf(reflectValue.Index(i)); !isZero { + if field.DBName == "" { + conds = append(conds, clause.Eq{Column: clause.Column{Table: s.Table, Name: field.Name}, Value: v}) + } else { + conds = append(conds, clause.Eq{Column: clause.Column{Table: s.Table, Name: field.DBName}, Value: v}) + } } } } diff --git a/tests/customize_field_test.go b/tests/customize_field_test.go index 910fa6ae..9c6ab948 100644 --- a/tests/customize_field_test.go +++ b/tests/customize_field_test.go @@ -134,10 +134,18 @@ func TestCustomizeField(t *testing.T) { t.Fatalf("invalid updated result: %#v", result2) } + if err := DB.Where(CustomizeFieldStruct{Name: create.Name, FieldReadonly: create.FieldReadonly, FieldIgnore: create.FieldIgnore}).First(&CustomizeFieldStruct{}).Error; err == nil { + t.Fatalf("Should failed to find result") + } + if err := DB.Table("customize_field_structs").Where("1 = 1").UpdateColumn("field_readonly", "readonly").Error; err != nil { t.Fatalf("failed to update field_readonly column") } + if err := DB.Where(CustomizeFieldStruct{Name: create.Name, FieldReadonly: "readonly", FieldIgnore: create.FieldIgnore}).First(&CustomizeFieldStruct{}).Error; err != nil { + t.Fatalf("Should find result") + } + var result3 CustomizeFieldStruct DB.Find(&result3, "name = ?", "create") From e308b103c02b05d5b0ab5b8a6f1ea70321d9f757 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Tue, 30 Jun 2020 07:29:15 +0800 Subject: [PATCH 13/20] SingularTable for JoinTable --- schema/naming.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/schema/naming.go b/schema/naming.go index d2a4919f..9b7c9471 100644 --- a/schema/naming.go +++ b/schema/naming.go @@ -41,6 +41,9 @@ func (ns NamingStrategy) ColumnName(table, column string) string { // JoinTableName convert string to join table name func (ns NamingStrategy) JoinTableName(str string) string { + if ns.SingularTable { + return ns.TablePrefix + toDBName(str) + } return ns.TablePrefix + inflection.Plural(toDBName(str)) } From 66dcd7e3cae8998f4c22a642299d1f4e7175c148 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Tue, 30 Jun 2020 16:53:54 +0800 Subject: [PATCH 14/20] Add SetColumn, Changed method --- callbacks/associations.go | 4 +- callbacks/create.go | 2 +- callbacks/helper.go | 58 +------------------ callbacks/update.go | 2 +- errors.go | 2 + statement.go | 117 ++++++++++++++++++++++++++++++++++++++ tests/hooks_test.go | 81 ++++++++++++++++++++++++++ utils/utils.go | 15 +++++ 8 files changed, 221 insertions(+), 60 deletions(-) diff --git a/callbacks/associations.go b/callbacks/associations.go index 3ff0f4b0..bcb6c414 100644 --- a/callbacks/associations.go +++ b/callbacks/associations.go @@ -11,7 +11,7 @@ import ( func SaveBeforeAssociations(db *gorm.DB) { if db.Error == nil && db.Statement.Schema != nil { - selectColumns, restricted := SelectAndOmitColumns(db.Statement, true, false) + selectColumns, restricted := db.Statement.SelectAndOmitColumns(true, false) // Save Belongs To associations for _, rel := range db.Statement.Schema.Relationships.BelongsTo { @@ -90,7 +90,7 @@ func SaveBeforeAssociations(db *gorm.DB) { func SaveAfterAssociations(db *gorm.DB) { if db.Error == nil && db.Statement.Schema != nil { - selectColumns, restricted := SelectAndOmitColumns(db.Statement, true, false) + selectColumns, restricted := db.Statement.SelectAndOmitColumns(true, false) // Save Has One associations for _, rel := range db.Statement.Schema.Relationships.HasOne { diff --git a/callbacks/create.go b/callbacks/create.go index 283d3fd1..eecb80a1 100644 --- a/callbacks/create.go +++ b/callbacks/create.go @@ -218,7 +218,7 @@ func ConvertToCreateValues(stmt *gorm.Statement) (values clause.Values) { values = ConvertSliceOfMapToValuesForCreate(stmt, value) default: var ( - selectColumns, restricted = SelectAndOmitColumns(stmt, true, false) + selectColumns, restricted = stmt.SelectAndOmitColumns(true, false) curTime = stmt.DB.NowFunc() isZero bool ) diff --git a/callbacks/helper.go b/callbacks/helper.go index 3b0cca16..1b06e0b7 100644 --- a/callbacks/helper.go +++ b/callbacks/helper.go @@ -7,64 +7,10 @@ import ( "gorm.io/gorm/clause" ) -// SelectAndOmitColumns get select and omit columns, select -> true, omit -> false -func SelectAndOmitColumns(stmt *gorm.Statement, requireCreate, requireUpdate bool) (map[string]bool, bool) { - results := map[string]bool{} - notRestricted := false - - // select columns - for _, column := range stmt.Selects { - if column == "*" { - notRestricted = true - for _, dbName := range stmt.Schema.DBNames { - results[dbName] = true - } - } else if column == clause.Associations { - for _, rel := range stmt.Schema.Relationships.Relations { - results[rel.Name] = true - } - } else if field := stmt.Schema.LookUpField(column); field != nil && field.DBName != "" { - results[field.DBName] = true - } else { - results[column] = true - } - } - - // omit columns - for _, omit := range stmt.Omits { - if omit == clause.Associations { - for _, rel := range stmt.Schema.Relationships.Relations { - results[rel.Name] = false - } - } else if field := stmt.Schema.LookUpField(omit); field != nil && field.DBName != "" { - results[field.DBName] = false - } else { - results[omit] = false - } - } - - if stmt.Schema != nil { - for _, field := range stmt.Schema.Fields { - name := field.DBName - if name == "" { - name = field.Name - } - - if requireCreate && !field.Creatable { - results[name] = false - } else if requireUpdate && !field.Updatable { - results[name] = false - } - } - } - - return results, !notRestricted && len(stmt.Selects) > 0 -} - // ConvertMapToValuesForCreate convert map to values func ConvertMapToValuesForCreate(stmt *gorm.Statement, mapValue map[string]interface{}) (values clause.Values) { columns := make([]string, 0, len(mapValue)) - selectColumns, restricted := SelectAndOmitColumns(stmt, true, false) + selectColumns, restricted := stmt.SelectAndOmitColumns(true, false) var keys []string for k := range mapValue { @@ -91,7 +37,7 @@ func ConvertSliceOfMapToValuesForCreate(stmt *gorm.Statement, mapValues []map[st var ( columns = []string{} result = map[string][]interface{}{} - selectColumns, restricted = SelectAndOmitColumns(stmt, true, false) + selectColumns, restricted = stmt.SelectAndOmitColumns(true, false) ) for idx, mapValue := range mapValues { diff --git a/callbacks/update.go b/callbacks/update.go index 1ea77552..f84e933c 100644 --- a/callbacks/update.go +++ b/callbacks/update.go @@ -110,7 +110,7 @@ func AfterUpdate(db *gorm.DB) { // ConvertToAssignments convert to update assignments func ConvertToAssignments(stmt *gorm.Statement) (set clause.Set) { var ( - selectColumns, restricted = SelectAndOmitColumns(stmt, false, true) + selectColumns, restricted = stmt.SelectAndOmitColumns(false, true) assignValue func(field *schema.Field, value interface{}) ) diff --git a/errors.go b/errors.go index b41eefae..e1b58835 100644 --- a/errors.go +++ b/errors.go @@ -29,4 +29,6 @@ var ( ErrUnsupportedDriver = errors.New("unsupported driver") // ErrRegistered registered ErrRegistered = errors.New("registered") + // ErrInvalidField invalid field + ErrInvalidField = errors.New("invalid field") ) diff --git a/statement.go b/statement.go index e902b739..164ddbd7 100644 --- a/statement.go +++ b/statement.go @@ -12,6 +12,7 @@ import ( "gorm.io/gorm/clause" "gorm.io/gorm/schema" + "gorm.io/gorm/utils" ) // Statement statement @@ -370,3 +371,119 @@ func (stmt *Statement) clone() *Statement { return newStmt } + +// Helpers +// SetColumn set column's value +func (stmt *Statement) SetColumn(name string, value interface{}) { + if v, ok := stmt.Dest.(map[string]interface{}); ok { + v[name] = value + } else if stmt.Schema != nil { + if field := stmt.Schema.LookUpField(name); field != nil { + field.Set(stmt.ReflectValue, value) + } else { + stmt.AddError(ErrInvalidField) + } + } else { + stmt.AddError(ErrInvalidField) + } +} + +// Changed check model changed or not when updating +func (stmt *Statement) Changed(fields ...string) bool { + modelValue := reflect.ValueOf(stmt.Model) + for modelValue.Kind() == reflect.Ptr { + modelValue = modelValue.Elem() + } + + selectColumns, restricted := stmt.SelectAndOmitColumns(false, true) + changed := func(field *schema.Field) bool { + fieldValue, isZero := field.ValueOf(modelValue) + if v, ok := selectColumns[field.DBName]; (ok && v) || (!ok && !restricted) { + if v, ok := stmt.Dest.(map[string]interface{}); ok { + if fv, ok := v[field.Name]; ok { + return !utils.AssertEqual(fv, fieldValue) + } else if fv, ok := v[field.DBName]; ok { + return !utils.AssertEqual(fv, fieldValue) + } else if isZero { + return true + } + } else { + changedValue, _ := field.ValueOf(stmt.ReflectValue) + return !utils.AssertEqual(changedValue, fieldValue) + } + } + return false + } + + if len(fields) == 0 { + for _, field := range stmt.Schema.FieldsByDBName { + if changed(field) { + return true + } + } + } else { + for _, name := range fields { + if field := stmt.Schema.LookUpField(name); field != nil { + if changed(field) { + return true + } + } + } + } + + return false +} + +// SelectAndOmitColumns get select and omit columns, select -> true, omit -> false +func (stmt *Statement) SelectAndOmitColumns(requireCreate, requireUpdate bool) (map[string]bool, bool) { + results := map[string]bool{} + notRestricted := false + + // select columns + for _, column := range stmt.Selects { + if column == "*" { + notRestricted = true + for _, dbName := range stmt.Schema.DBNames { + results[dbName] = true + } + } else if column == clause.Associations { + for _, rel := range stmt.Schema.Relationships.Relations { + results[rel.Name] = true + } + } else if field := stmt.Schema.LookUpField(column); field != nil && field.DBName != "" { + results[field.DBName] = true + } else { + results[column] = true + } + } + + // omit columns + for _, omit := range stmt.Omits { + if omit == clause.Associations { + for _, rel := range stmt.Schema.Relationships.Relations { + results[rel.Name] = false + } + } else if field := stmt.Schema.LookUpField(omit); field != nil && field.DBName != "" { + results[field.DBName] = false + } else { + results[omit] = false + } + } + + if stmt.Schema != nil { + for _, field := range stmt.Schema.Fields { + name := field.DBName + if name == "" { + name = field.Name + } + + if requireCreate && !field.Creatable { + results[name] = false + } else if requireUpdate && !field.Updatable { + results[name] = false + } + } + } + + return results, !notRestricted && len(stmt.Selects) > 0 +} diff --git a/tests/hooks_test.go b/tests/hooks_test.go index c74e8f10..8f8c60f5 100644 --- a/tests/hooks_test.go +++ b/tests/hooks_test.go @@ -285,3 +285,84 @@ func TestUseDBInHooks(t *testing.T) { t.Errorf("Admin product's price should not be changed, expects: %v, got %v", 600, result4.Price) } } + +type Product3 struct { + gorm.Model + Name string + Code string + Price int64 + Owner string +} + +func (s Product3) BeforeCreate(tx *gorm.DB) (err error) { + tx.Statement.SetColumn("Price", s.Price+100) + return nil +} + +func (s Product3) BeforeUpdate(tx *gorm.DB) (err error) { + if tx.Statement.Changed() { + tx.Statement.SetColumn("Price", s.Price+10) + } + + if tx.Statement.Changed("Code") { + s.Price += 20 + tx.Statement.SetColumn("Price", s.Price+30) + } + return nil +} + +func TestSetColumn(t *testing.T) { + DB.Migrator().DropTable(&Product3{}) + DB.AutoMigrate(&Product3{}) + + product := Product3{Name: "Product", Price: 0} + DB.Create(&product) + + if product.Price != 100 { + t.Errorf("invalid price after create, got %+v", product) + } + + DB.Model(&product).Select("code", "price").Updates(map[string]interface{}{"code": "L1212"}) + + if product.Price != 150 || product.Code != "L1212" { + t.Errorf("invalid data after update, got %+v", product) + } + + // Code not changed, price should not change + DB.Model(&product).Updates(map[string]interface{}{"Name": "Product New"}) + + if product.Name != "Product New" || product.Price != 160 || product.Code != "L1212" { + t.Errorf("invalid data after update, got %+v", product) + } + + // Code changed, but not selected, price should not change + DB.Model(&product).Select("Name", "Price").Updates(map[string]interface{}{"Name": "Product New2", "code": "L1213"}) + + if product.Name != "Product New2" || product.Price != 170 || product.Code != "L1212" { + t.Errorf("invalid data after update, got %+v", product) + } + + // Code changed, price should changed + DB.Model(&product).Select("Name", "Code", "Price").Updates(map[string]interface{}{"Name": "Product New3", "code": "L1213"}) + + if product.Name != "Product New3" || product.Price != 220 || product.Code != "L1213" { + t.Errorf("invalid data after update, got %+v", product) + } + + var result Product3 + DB.First(&result, product.ID) + + AssertEqual(t, result, product) + + // Code changed, price not selected, price should not change + DB.Model(&product).Select("code").Updates(map[string]interface{}{"name": "L1214"}) + + if product.Price != 220 || product.Code != "L1213" { + t.Errorf("invalid data after update, got %+v", product) + } + + var result2 Product3 + DB.First(&result2, product.ID) + + AssertEqual(t, result2, product) +} diff --git a/utils/utils.go b/utils/utils.go index 81d2dc34..9bf00683 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -68,3 +68,18 @@ func ToStringKey(values ...interface{}) string { return strings.Join(results, "_") } + +func AssertEqual(src, dst interface{}) bool { + if !reflect.DeepEqual(src, dst) { + if valuer, ok := src.(driver.Valuer); ok { + src, _ = valuer.Value() + } + + if valuer, ok := dst.(driver.Valuer); ok { + dst, _ = valuer.Value() + } + + return reflect.DeepEqual(src, dst) + } + return true +} From 3e4dbde920e3fe88a56a97429ab8146408d18da6 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Tue, 30 Jun 2020 22:47:21 +0800 Subject: [PATCH 15/20] Test Hooks For Slice --- callbacks/callmethod.go | 4 +++- statement.go | 17 +++++++++++---- tests/hooks_test.go | 48 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/callbacks/callmethod.go b/callbacks/callmethod.go index a0e9b0e7..0160f354 100644 --- a/callbacks/callmethod.go +++ b/callbacks/callmethod.go @@ -11,8 +11,10 @@ func callMethod(db *gorm.DB, fc func(value interface{}, tx *gorm.DB) bool) { if called := fc(db.Statement.Dest, tx); !called { switch db.Statement.ReflectValue.Kind() { case reflect.Slice, reflect.Array: + db.Statement.CurDestIndex = 0 for i := 0; i < db.Statement.ReflectValue.Len(); i++ { - fc(db.Statement.ReflectValue.Index(i).Addr().Interface(), tx) + fc(reflect.Indirect(db.Statement.ReflectValue.Index(i)).Addr().Interface(), tx) + db.Statement.CurDestIndex++ } case reflect.Struct: fc(db.Statement.ReflectValue.Addr().Interface(), tx) diff --git a/statement.go b/statement.go index 164ddbd7..e65a064f 100644 --- a/statement.go +++ b/statement.go @@ -38,6 +38,7 @@ type Statement struct { SQL strings.Builder Vars []interface{} NamedVars []sql.NamedArg + CurDestIndex int attrs []interface{} assigns []interface{} } @@ -379,7 +380,12 @@ func (stmt *Statement) SetColumn(name string, value interface{}) { v[name] = value } else if stmt.Schema != nil { if field := stmt.Schema.LookUpField(name); field != nil { - field.Set(stmt.ReflectValue, value) + switch stmt.ReflectValue.Kind() { + case reflect.Slice, reflect.Array: + field.Set(stmt.ReflectValue.Index(stmt.CurDestIndex), value) + case reflect.Struct: + field.Set(stmt.ReflectValue, value) + } } else { stmt.AddError(ErrInvalidField) } @@ -395,17 +401,20 @@ func (stmt *Statement) Changed(fields ...string) bool { modelValue = modelValue.Elem() } + switch modelValue.Kind() { + case reflect.Slice, reflect.Array: + modelValue = stmt.ReflectValue.Index(stmt.CurDestIndex) + } + selectColumns, restricted := stmt.SelectAndOmitColumns(false, true) changed := func(field *schema.Field) bool { - fieldValue, isZero := field.ValueOf(modelValue) + fieldValue, _ := field.ValueOf(modelValue) if v, ok := selectColumns[field.DBName]; (ok && v) || (!ok && !restricted) { if v, ok := stmt.Dest.(map[string]interface{}); ok { if fv, ok := v[field.Name]; ok { return !utils.AssertEqual(fv, fieldValue) } else if fv, ok := v[field.DBName]; ok { return !utils.AssertEqual(fv, fieldValue) - } else if isZero { - return true } } else { changedValue, _ := field.ValueOf(stmt.ReflectValue) diff --git a/tests/hooks_test.go b/tests/hooks_test.go index 8f8c60f5..ed5ee746 100644 --- a/tests/hooks_test.go +++ b/tests/hooks_test.go @@ -366,3 +366,51 @@ func TestSetColumn(t *testing.T) { AssertEqual(t, result2, product) } + +func TestHooksForSlice(t *testing.T) { + products := []*Product3{ + {Name: "Product-1", Price: 100}, + {Name: "Product-2", Price: 200}, + {Name: "Product-3", Price: 300}, + } + + DB.Create(&products) + + for idx, value := range []int64{200, 300, 400} { + if products[idx].Price != value { + t.Errorf("invalid price for product #%v, expects: %v, got %v", idx, value, products[idx].Price) + } + } + + DB.Model(&products).Update("Name", "product-name") + + // will set all product's price to last product's price + 10 + for idx, value := range []int64{410, 410, 410} { + if products[idx].Price != value { + t.Errorf("invalid price for product #%v, expects: %v, got %v", idx, value, products[idx].Price) + } + } + + products2 := []Product3{ + {Name: "Product-1", Price: 100}, + {Name: "Product-2", Price: 200}, + {Name: "Product-3", Price: 300}, + } + + DB.Create(&products2) + + for idx, value := range []int64{200, 300, 400} { + if products2[idx].Price != value { + t.Errorf("invalid price for product #%v, expects: %v, got %v", idx, value, products2[idx].Price) + } + } + + DB.Model(&products2).Update("Name", "product-name") + + // will set all product's price to last product's price + 10 + for idx, value := range []int64{410, 410, 410} { + if products2[idx].Price != value { + t.Errorf("invalid price for product #%v, expects: %v, got %v", idx, value, products2[idx].Price) + } + } +} From 7aaac3a580d5c0a4b28853c8f53d8feb0327530f Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Tue, 30 Jun 2020 23:06:48 +0800 Subject: [PATCH 16/20] Allow to use sql function in Group, Pluck --- chainable_api.go | 4 +++- finisher_api.go | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/chainable_api.go b/chainable_api.go index dbd783fd..e2ba44cc 100644 --- a/chainable_api.go +++ b/chainable_api.go @@ -162,8 +162,10 @@ func (db *DB) Joins(query string, args ...interface{}) (tx *DB) { // Group specify the group method on the find func (db *DB) Group(name string) (tx *DB) { tx = db.getInstance() + + fields := strings.FieldsFunc(name, utils.IsChar) tx.Statement.AddClause(clause.GroupBy{ - Columns: []clause.Column{{Name: name}}, + Columns: []clause.Column{{Name: name, Raw: len(fields) != 1}}, }) return } diff --git a/finisher_api.go b/finisher_api.go index 6d961811..af040106 100644 --- a/finisher_api.go +++ b/finisher_api.go @@ -8,6 +8,7 @@ import ( "strings" "gorm.io/gorm/clause" + "gorm.io/gorm/utils" ) // Create insert the value into database @@ -325,9 +326,10 @@ func (db *DB) Pluck(column string, dest interface{}) (tx *DB) { tx.AddError(ErrorModelValueRequired) } + fields := strings.FieldsFunc(column, utils.IsChar) tx.Statement.AddClauseIfNotExists(clause.Select{ Distinct: tx.Statement.Distinct, - Columns: []clause.Column{{Name: column}}, + Columns: []clause.Column{{Name: column, Raw: len(fields) != 1}}, }) tx.Statement.Dest = dest tx.callbacks.Query().Execute(tx) From 9d7df71332b26949d6d61eff94ad416c0984d7f3 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Wed, 1 Jul 2020 08:56:21 +0800 Subject: [PATCH 17/20] Query with smaller struct --- callbacks/query.go | 12 +++++++++++- scan.go | 24 +++++++++++++++++------- tests/query_test.go | 23 ++++++++++++++++++++++- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/callbacks/query.go b/callbacks/query.go index 27d53a4d..4b7f5bd5 100644 --- a/callbacks/query.go +++ b/callbacks/query.go @@ -40,7 +40,7 @@ func BuildQuerySQL(db *gorm.DB) { db.Statement.SQL.Grow(100) clauseSelect := clause.Select{Distinct: db.Statement.Distinct} - if db.Statement.ReflectValue.Kind() == reflect.Struct { + if db.Statement.ReflectValue.Kind() == reflect.Struct && db.Statement.ReflectValue.Type() == db.Statement.Schema.ModelType { var conds []clause.Expression for _, primaryField := range db.Statement.Schema.PrimaryFields { if v, isZero := primaryField.ValueOf(db.Statement.ReflectValue); !isZero { @@ -64,6 +64,16 @@ func BuildQuerySQL(db *gorm.DB) { clauseSelect.Columns[idx] = clause.Column{Name: name, Raw: true} } } + } else if db.Statement.Schema != nil && db.Statement.ReflectValue.IsValid() && db.Statement.ReflectValue.Type() != db.Statement.Schema.ModelType { + stmt := gorm.Statement{DB: db} + // smaller struct + if err := stmt.Parse(db.Statement.Dest); err == nil { + clauseSelect.Columns = make([]clause.Column, len(stmt.Schema.DBNames)) + + for idx, dbName := range stmt.Schema.DBNames { + clauseSelect.Columns[idx] = clause.Column{Name: dbName} + } + } } // inline joins diff --git a/scan.go b/scan.go index 2d227ec2..0b199029 100644 --- a/scan.go +++ b/scan.go @@ -69,6 +69,8 @@ func Scan(rows *sql.Rows, db *DB, initialized bool) { db.AddError(rows.Scan(dest)) } default: + Schema := db.Statement.Schema + switch db.Statement.ReflectValue.Kind() { case reflect.Slice, reflect.Array: var ( @@ -84,16 +86,20 @@ func Scan(rows *sql.Rows, db *DB, initialized bool) { db.Statement.ReflectValue.Set(reflect.MakeSlice(db.Statement.ReflectValue.Type(), 0, 0)) - if db.Statement.Schema != nil { + if Schema != nil { + if reflectValueType != Schema.ModelType && reflectValueType.Kind() == reflect.Struct { + Schema, _ = schema.Parse(db.Statement.Dest, db.cacheStore, db.NamingStrategy) + } + for idx, column := range columns { - if field := db.Statement.Schema.LookUpField(column); field != nil && field.Readable { + if field := Schema.LookUpField(column); field != nil && field.Readable { fields[idx] = field } else if names := strings.Split(column, "__"); len(names) > 1 { if len(joinFields) == 0 { joinFields = make([][2]*schema.Field, len(columns)) } - if rel, ok := db.Statement.Schema.Relationships.Relations[names[0]]; ok { + if rel, ok := Schema.Relationships.Relations[names[0]]; ok { if field := rel.FieldSchema.LookUpField(strings.Join(names[1:], "__")); field != nil && field.Readable { fields[idx] = field joinFields[idx] = [2]*schema.Field{rel.Field, field} @@ -151,12 +157,16 @@ func Scan(rows *sql.Rows, db *DB, initialized bool) { } } case reflect.Struct: + if db.Statement.ReflectValue.Type() != Schema.ModelType { + Schema, _ = schema.Parse(db.Statement.Dest, db.cacheStore, db.NamingStrategy) + } + if initialized || rows.Next() { for idx, column := range columns { - if field := db.Statement.Schema.LookUpField(column); field != nil && field.Readable { + if field := Schema.LookUpField(column); field != nil && field.Readable { values[idx] = reflect.New(reflect.PtrTo(field.IndirectFieldType)).Interface() } else if names := strings.Split(column, "__"); len(names) > 1 { - if rel, ok := db.Statement.Schema.Relationships.Relations[names[0]]; ok { + if rel, ok := Schema.Relationships.Relations[names[0]]; ok { if field := rel.FieldSchema.LookUpField(strings.Join(names[1:], "__")); field != nil && field.Readable { values[idx] = reflect.New(reflect.PtrTo(field.IndirectFieldType)).Interface() continue @@ -172,10 +182,10 @@ func Scan(rows *sql.Rows, db *DB, initialized bool) { db.AddError(rows.Scan(values...)) for idx, column := range columns { - if field := db.Statement.Schema.LookUpField(column); field != nil && field.Readable { + if field := Schema.LookUpField(column); field != nil && field.Readable { field.Set(db.Statement.ReflectValue, values[idx]) } else if names := strings.Split(column, "__"); len(names) > 1 { - if rel, ok := db.Statement.Schema.Relationships.Relations[names[0]]; ok { + if rel, ok := Schema.Relationships.Relations[names[0]]; ok { relValue := rel.Field.ReflectValueOf(db.Statement.ReflectValue) if field := rel.FieldSchema.LookUpField(strings.Join(names[1:], "__")); field != nil && field.Readable { value := reflect.ValueOf(values[idx]).Elem() diff --git a/tests/query_test.go b/tests/query_test.go index de65b63b..7973fd51 100644 --- a/tests/query_test.go +++ b/tests/query_test.go @@ -3,6 +3,7 @@ package tests_test import ( "fmt" "reflect" + "regexp" "sort" "strconv" "testing" @@ -144,8 +145,8 @@ func TestFillSmallerStruct(t *testing.T) { user := User{Name: "SmallerUser", Age: 100} DB.Save(&user) type SimpleUser struct { - Name string ID int64 + Name string UpdatedAt time.Time CreatedAt time.Time } @@ -156,6 +157,26 @@ func TestFillSmallerStruct(t *testing.T) { } AssertObjEqual(t, user, simpleUser, "Name", "ID", "UpdatedAt", "CreatedAt") + + var simpleUser2 SimpleUser + if err := DB.Model(&User{}).Select("id").First(&simpleUser2, user.ID).Error; err != nil { + t.Fatalf("Failed to query smaller user, got error %v", err) + } + + AssertObjEqual(t, user, simpleUser2, "ID") + + var simpleUsers []SimpleUser + if err := DB.Model(&User{}).Select("id").Find(&simpleUsers, user.ID).Error; err != nil || len(simpleUsers) != 1 { + t.Fatalf("Failed to query smaller user, got error %v", err) + } + + AssertObjEqual(t, user, simpleUsers[0], "ID") + + result := DB.Session(&gorm.Session{DryRun: true}).Model(&User{}).Find(&simpleUsers, user.ID) + + if !regexp.MustCompile("SELECT .*id.*name.*updated_at.*created_at.* FROM .*users").MatchString(result.Statement.SQL.String()) { + t.Fatalf("SQL should include selected names, but got %v", result.Statement.SQL.String()) + } } func TestPluck(t *testing.T) { From d342f4122af9a14b2d4aa768af759ea6a0c56d7a Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Wed, 1 Jul 2020 10:19:52 +0800 Subject: [PATCH 18/20] Better support Count in chain --- finisher_api.go | 2 ++ tests/count_test.go | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/finisher_api.go b/finisher_api.go index af040106..25c56e49 100644 --- a/finisher_api.go +++ b/finisher_api.go @@ -269,6 +269,7 @@ func (db *DB) Count(count *int64) (tx *DB) { if len(tx.Statement.Selects) == 0 { tx.Statement.AddClause(clause.Select{Expression: clause.Expr{SQL: "count(1)"}}) + defer tx.Statement.AddClause(clause.Select{}) } else if !strings.Contains(strings.ToLower(tx.Statement.Selects[0]), "count(") { expr := clause.Expr{SQL: "count(1)"} @@ -281,6 +282,7 @@ func (db *DB) Count(count *int64) (tx *DB) { } tx.Statement.AddClause(clause.Select{Expression: expr}) + defer tx.Statement.AddClause(clause.Select{}) } tx.Statement.Dest = count diff --git a/tests/count_test.go b/tests/count_test.go index 0662ae5c..826d6a36 100644 --- a/tests/count_test.go +++ b/tests/count_test.go @@ -27,6 +27,14 @@ func TestCount(t *testing.T) { t.Errorf("Count() method should get correct value, expect: %v, got %v", count, len(users)) } + if err := DB.Model(&User{}).Where("name = ?", user1.Name).Or("name = ?", user3.Name).Count(&count).Find(&users).Error; err != nil { + t.Errorf(fmt.Sprintf("Count should work, but got err %v", err)) + } + + if count != int64(len(users)) { + t.Errorf("Count() method should get correct value, expect: %v, got %v", count, len(users)) + } + DB.Model(&User{}).Where("name = ?", user1.Name).Count(&count1).Or("name in ?", []string{user2.Name, user3.Name}).Count(&count2) if count1 != 1 || count2 != 3 { t.Errorf("multiple count in chain should works") From 65d6c19d73e5574d5d6024b2a3fe6008962c6300 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Wed, 1 Jul 2020 11:47:46 +0800 Subject: [PATCH 19/20] Test multiple index tags --- schema/index_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/schema/index_test.go b/schema/index_test.go index 384e902b..71a70a8c 100644 --- a/schema/index_test.go +++ b/schema/index_test.go @@ -16,7 +16,7 @@ type UserIndex struct { Name5 int64 `gorm:"index:,class:FULLTEXT,comment:hello \\, world,where:age > 10"` Name6 int64 `gorm:"index:profile,comment:hello \\, world,where:age > 10"` Age int64 `gorm:"index:profile,expression:ABS(age)"` - OID int64 `gorm:"index:idx_id"` + OID int64 `gorm:"index:idx_id;index:idx_oid,unique"` MemberNumber string `gorm:"index:idx_id"` } @@ -70,6 +70,11 @@ func TestParseIndex(t *testing.T) { Name: "idx_id", Fields: []schema.IndexOption{{}, {}}, }, + "idx_oid": { + Name: "idx_oid", + Class: "UNIQUE", + Fields: []schema.IndexOption{{}}, + }, } indices := user.ParseIndexes() From 322c6a36ee92dd8ab375cc9eda5fb267db131c5b Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Wed, 1 Jul 2020 19:50:24 +0800 Subject: [PATCH 20/20] Fix .github config --- .github/ISSUE_TEMPLATE.md | 5 -- .github/PULL_REQUEST_TEMPLATE.md | 11 --- .github/labeler.yml | 6 -- .github/labels.json | 139 ++++++++++++++++++++++++++++++ .github/workflows/issue.yml | 15 ---- .github/workflows/issue_stale.yml | 19 ---- .github/workflows/labeler.yml | 19 ++++ .github/workflows/stale.yml | 21 +++++ 8 files changed, 179 insertions(+), 56 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md delete mode 100644 .github/labeler.yml create mode 100644 .github/labels.json delete mode 100644 .github/workflows/issue.yml delete mode 100644 .github/workflows/issue_stale.yml create mode 100644 .github/workflows/labeler.yml create mode 100644 .github/workflows/stale.yml diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index ac311633..00000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,5 +0,0 @@ -Your issue may already be reported! Please search on the [issue track](https://github.com/go-gorm/gorm/issues) before creating one. - -To report a bug, your issue *have to* include an [GORM playground pull request link](https://github.com/go-gorm/playground), for general questions, please delete below line. - -## GORM Playground Link: https://github.com/go-gorm/playground/pull/1 (change this to your link) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 930ff176..00000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,11 +0,0 @@ -Make sure these boxes checked before submitting your pull request. - -- [] Do only one thing -- [] No API-breaking changes -- [] New code/logic commented & tested (important) - -For significant changes like big bug fixes, new features, please open an issue to make an agreement on an implementation design/plan first before starting it. - -### What did this pull request do? - -### Use Case diff --git a/.github/labeler.yml b/.github/labeler.yml deleted file mode 100644 index d96bafa0..00000000 --- a/.github/labeler.yml +++ /dev/null @@ -1,6 +0,0 @@ -# Add/remove 'critical' label if issue contains the words 'urgent' or 'critical' -HasGormPlaygroundTestCase: - - '(github.com/go-gorm/playground/pull/\d)' - -NoTestCase: - - '(change this to your link)' diff --git a/.github/labels.json b/.github/labels.json new file mode 100644 index 00000000..8b1ce849 --- /dev/null +++ b/.github/labels.json @@ -0,0 +1,139 @@ +{ + "labels": { + "critical": { + "name": "type:critical", + "colour": "#E84137", + "description": "critical questions" + }, + "question": { + "name": "type:question", + "colour": "#EDEDED", + "description": "general questions" + }, + "with_playground": { + "name": "type:with reproduction steps", + "colour": "#00ff00", + "description": "with reproduction steps" + }, + "without_playground": { + "name": "type:missing reproduction steps", + "colour": "#CF2E1F", + "description": "missing reproduction steps" + }, + "has_pr": { + "name": "type:has pull request", + "colour": "#43952A", + "description": "has pull request" + }, + "not_tested": { + "name": "type:not tested", + "colour": "#CF2E1F", + "description": "not tested" + }, + "tested": { + "name": "type:tested", + "colour": "#00ff00", + "description": "tested" + }, + "breaking_change": { + "name": "type:breaking change", + "colour": "#CF2E1F", + "description": "breaking change" + } + }, + "issue": { + "with_playground": { + "requires": 1, + "conditions": [ + { + "type": "descriptionMatches", + "pattern": "/github.com\/go-gorm\/playground\/pull\/\\d\\d+/s" + } + ] + }, + "critical": { + "requires": 1, + "conditions": [ + { + "type": "descriptionMatches", + "pattern": "/(critical|urgent)/i" + }, + { + "type": "titleMatches", + "pattern": "/(critical|urgent)/i" + } + ] + }, + "question": { + "requires": 1, + "conditions": [ + { + "type": "titleMatches", + "pattern": "/question/i" + }, + { + "type": "descriptionMatches", + "pattern": "/question/i" + } + ] + }, + "without_playground": { + "requires": 5, + "conditions": [ + { + "type": "descriptionMatches", + "pattern": "/^((?!github.com\/go-gorm\/playground\/pull\/\\d\\d+).)*$/s" + }, + { + "type": "titleMatches", + "pattern": "/^((?!question).)*$/s" + }, + { + "type": "descriptionMatches", + "pattern": "/^((?!question).)*$/is" + }, + { + "type": "titleMatches", + "pattern": "/^((?!critical|urgent).)*$/s" + }, + { + "type": "descriptionMatches", + "pattern": "/^((?!critical|urgent).)*$/s" + } + ] + } + }, + "pr": { + "critical": { + "requires": 1, + "conditions": [ + { + "type": "descriptionMatches", + "pattern": "/(critical|urgent)/i" + }, + { + "type": "titleMatches", + "pattern": "/(critical|urgent)/i" + } + ] + }, + "not_tested": { + "requires": 1, + "conditions": [ + { + "type": "descriptionMatches", + "pattern": "/\\[\\] Tested/" + } + ] + }, + "breaking_change": { + "requires": 1, + "conditions": [ + { + "type": "descriptionMatches", + "pattern": "/\\[\\] Non breaking API changes/" + } + ] + } + } +} diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml deleted file mode 100644 index 0759782c..00000000 --- a/.github/workflows/issue.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: "Issue-Labeler" -on: - issues: - types: [opened, edited] - -jobs: - triage: - runs-on: ubuntu-latest - steps: - - uses: github/issue-labeler@v2.0 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" - configuration-path: ".github/labeler.yml" - not-before: "2020-01-15T02:54:32Z" - enable-versioned-regex: 0 \ No newline at end of file diff --git a/.github/workflows/issue_stale.yml b/.github/workflows/issue_stale.yml deleted file mode 100644 index fadfb522..00000000 --- a/.github/workflows/issue_stale.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Issue cleanup -on: - schedule: - - cron: '0 1 * * *' # At 01:00, everyday -jobs: - triage_issues: - name: Issue triage - runs-on: ubuntu-latest - steps: - - name: Find old issues and mark them stale - uses: Krizzu/issue-triage-action@v1.0.0 - with: - ghToken: ${{ secrets.GITHUB_TOKEN }} - staleAfter: 7 - closeAfter: 14 - staleLabel: "STALE 📺" - staleComment: "This issue is %DAYS_OLD% days old, marking as stale! cc: @%AUTHOR%" - closeComment: "Issue last updated %DAYS_OLD% days ago! Closing down!" - showLogs: true \ No newline at end of file diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 00000000..1490730b --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,19 @@ +name: "Issue Labeler" +on: + issues: + types: [opened, edited, reopened] + pull_request: + types: [opened, edited, reopened, ready_for_review, synchronize] + +jobs: + triage: + runs-on: ubuntu-latest + name: Label issues and pull requests + steps: + - name: check out + uses: actions/checkout@v2 + + - name: labeler + uses: jinzhu/super-labeler-action@develop + with: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..6fb714ca --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,21 @@ +name: "Close Missing Playground issues" +on: + schedule: + - cron: "*/10 * * * *" + +jobs: + stale: + runs-on: ubuntu-latest + env: + ACTIONS_STEP_DEBUG: true + steps: + - name: Close Stale Issues + uses: actions/stale@v3.0.7 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: "This issue has been automatically marked as stale as it missing playground pull request link, 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." + stale-issue-label: "status:stale" + days-before-stale: 0 + days-before-close: 2 + remove-stale-when-updated: true + only-labels: "type:missing reproduction steps"