From e3050b4c04a38ea4052386b19af65123a3e45385 Mon Sep 17 00:00:00 2001 From: doug-martin Date: Fri, 26 Jul 2019 13:04:37 -0500 Subject: [PATCH] Support column DEFAULT when inserting/updating via struct #27 --- HISTORY.md | 4 ++ docs/inserting.md | 42 +++++++++++++++ docs/updating.md | 18 +++++++ exp/insert.go | 6 ++- exp/literal.go | 5 ++ exp/update.go | 6 ++- expressions.go | 2 +- insert_dataset_example_test.go | 28 ++++++++-- insert_dataset_test.go | 34 ++++++++++++ internal/util/reflect.go | 47 +++++++++++----- internal/util/reflect_test.go | 97 ++++++++++++++++++++++++++++++++-- update_dataset_example_test.go | 20 +++++++ update_dataset_test.go | 32 +++++++++++ 13 files changed, 317 insertions(+), 24 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 56ca9c8f..1ba2a7c5 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,7 @@ +## v8.1.0 + +* [ADDED] Support column DEFAULT when inserting/updating via struct [#27](https://github.com/doug-martin/goqu/issues/27) + ## v8.0.1 * [ADDED] Multi table update support for `mysql` and `postgres` [#60](https://github.com/doug-martin/goqu/issues/60) diff --git a/docs/inserting.md b/docs/inserting.md index 0aabd3b1..c81cebe7 100644 --- a/docs/inserting.md +++ b/docs/inserting.md @@ -154,6 +154,48 @@ Output: INSERT INTO "user" ("first_name", "last_name") VALUES ('Greg', 'Farley'), ('Jimmy', 'Stewart'), ('Jeff', 'Jeffers') [] ``` +You can skip fields in a struct by using the `skipinsert` tag + +```go +type User struct { + FirstName string `db:"first_name" goqu:"skipinsert"` + LastName string `db:"last_name"` +} +ds := goqu.Insert("user").Rows( + User{FirstName: "Greg", LastName: "Farley"}, + User{FirstName: "Jimmy", LastName: "Stewart"}, + User{FirstName: "Jeff", LastName: "Jeffers"}, +) +insertSQL, args, _ := ds.ToSQL() +fmt.Println(insertSQL, args) +``` + +Output: +``` +INSERT INTO "user" ("last_name") VALUES ('Farley'), ('Stewart'), ('Jeffers') [] +``` + +If you want to use the database `DEFAULT` when the struct field is a zero value you can use the `defaultifempty` tag. + +```go +type User struct { + FirstName string `db:"first_name" goqu:"defaultifempty"` + LastName string `db:"last_name"` +} +ds := goqu.Insert("user").Rows( + User{LastName: "Farley"}, + User{FirstName: "Jimmy", LastName: "Stewart"}, + User{LastName: "Jeffers"}, +) +insertSQL, args, _ := ds.ToSQL() +fmt.Println(insertSQL, args) +``` + +Output: +``` +INSERT INTO "user" ("first_name", "last_name") VALUES (DEFAULT, 'Farley'), ('Jimmy', 'Stewart'), (DEFAULT, 'Jeffers') [] +``` + **Insert `map[string]interface{}`** diff --git a/docs/updating.md b/docs/updating.md index da7bcfbe..1c5fba54 100644 --- a/docs/updating.md +++ b/docs/updating.md @@ -153,6 +153,24 @@ Output: UPDATE "items" SET "address"='111 Test Addr' [] ``` +If you want to use the database `DEFAULT` when the struct field is a zero value you can use the `defaultifempty` tag. + +```go +type item struct { + Address string `db:"address"` + Name string `db:"name" goqu:"defaultifempty"` +} +sql, args, _ := goqu.Update("items").Set( + item{Address: "111 Test Addr"}, +).ToSQL() +fmt.Println(sql, args) +``` + +Output: +``` +UPDATE "items" SET "address"='111 Test Addr',"name"=DEFAULT [] +``` + **[Set with Map](https://godoc.org/github.com/doug-martin/goqu/#UpdateDataset.Set)** diff --git a/exp/insert.go b/exp/insert.go index b43a5746..6799bbf8 100644 --- a/exp/insert.go +++ b/exp/insert.go @@ -150,7 +150,11 @@ func getFieldsValues(value reflect.Value) (rowCols, rowVals []interface{}, err e if f.ShouldInsert { v := value.FieldByIndex(f.FieldIndex) rowCols = append(rowCols, col) - rowVals = append(rowVals, v.Interface()) + if f.DefaultIfEmpty && util.IsEmptyValue(v) { + rowVals = append(rowVals, Default()) + } else { + rowVals = append(rowVals, v.Interface()) + } } } } diff --git a/exp/literal.go b/exp/literal.go index ea58178e..68bb9063 100644 --- a/exp/literal.go +++ b/exp/literal.go @@ -22,6 +22,11 @@ func Star() LiteralExpression { return NewLiteralExpression("*") } +// Returns a literal for the 'DEFAULT' +func Default() LiteralExpression { + return NewLiteralExpression("DEFAULT") +} + func (l literal) Clone() Expression { return NewLiteralExpression(l.literal, l.args...) } diff --git a/exp/update.go b/exp/update.go index 626904ba..25b3734e 100644 --- a/exp/update.go +++ b/exp/update.go @@ -50,7 +50,11 @@ func getUpdateExpressionsStruct(value reflect.Value) (updates []UpdateExpression f := cm[col] if f.ShouldUpdate { v := value.FieldByIndex(f.FieldIndex) - updates = append(updates, ParseIdentifier(col).Set(v.Interface())) + setV := v.Interface() + if f.DefaultIfEmpty && util.IsEmptyValue(v) { + setV = Default() + } + updates = append(updates, ParseIdentifier(col).Set(setV)) } } return updates, nil diff --git a/expressions.go b/expressions.go index 097efc14..62dc1ac7 100644 --- a/expressions.go +++ b/expressions.go @@ -209,5 +209,5 @@ func Star() exp.LiteralExpression { return exp.Star() } // Returns a literal for DEFAULT sql keyword func Default() exp.LiteralExpression { - return L("DEFAULT") + return exp.Default() } diff --git a/insert_dataset_example_test.go b/insert_dataset_example_test.go index e41fc33f..93237351 100644 --- a/insert_dataset_example_test.go +++ b/insert_dataset_example_test.go @@ -367,25 +367,43 @@ func ExampleInsertDataset_Rows_withGoquSkipInsertTag() { fmt.Println(insertSQL, args) insertSQL, args, _ = goqu.Insert("items"). + Rows([]item{ + {Name: "Test1", Address: "111 Test Addr"}, + {Name: "Test2", Address: "112 Test Addr"}, + }). + ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "items" ("address") VALUES ('111 Test Addr'), ('112 Test Addr') [] + // INSERT INTO "items" ("address") VALUES ('111 Test Addr'), ('112 Test Addr') [] +} + +func ExampleInsertDataset_Rows_withGoquDefaultIfEmptyTag() { + type item struct { + ID uint32 `goqu:"skipinsert"` + Address string + Name string `goqu:"defaultifempty"` + } + insertSQL, args, _ := goqu.Insert("items"). Rows( item{Name: "Test1", Address: "111 Test Addr"}, - item{Name: "Test2", Address: "112 Test Addr"}, + item{Address: "112 Test Addr"}, ). ToSQL() fmt.Println(insertSQL, args) insertSQL, args, _ = goqu.Insert("items"). Rows([]item{ - {Name: "Test1", Address: "111 Test Addr"}, + {Address: "111 Test Addr"}, {Name: "Test2", Address: "112 Test Addr"}, }). ToSQL() fmt.Println(insertSQL, args) // Output: - // INSERT INTO "items" ("address") VALUES ('111 Test Addr'), ('112 Test Addr') [] - // INSERT INTO "items" ("address") VALUES ('111 Test Addr'), ('112 Test Addr') [] - // INSERT INTO "items" ("address") VALUES ('111 Test Addr'), ('112 Test Addr') [] + // INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', 'Test1'), ('112 Test Addr', DEFAULT) [] + // INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', DEFAULT), ('112 Test Addr', 'Test2') [] } func ExampleInsertDataset_ClearOnConflict() { diff --git a/insert_dataset_test.go b/insert_dataset_test.go index 8ec5dc0d..6dd9e9ea 100644 --- a/insert_dataset_test.go +++ b/insert_dataset_test.go @@ -600,6 +600,40 @@ func (ids *insertDatasetSuite) TestRows_ToSQLWithGoquSkipInsertTagSQL() { assert.Equal(t, `INSERT INTO "items" ("address", "name") VALUES (?, ?), (?, ?), (?, ?), (?, ?)`, insertSQL) } +func (ids *insertDatasetSuite) TestRows_ToSQLWithGoquDefaultIfEmptyTag() { + type item struct { + ID uint32 `db:"id" goqu:"skipinsert"` + Address string `db:"address" goqu:"defaultifempty"` + Name string `db:"name" goqu:"defaultifempty"` + Bool bool `db:"bool" goqu:"skipinsert,defaultifempty"` + } + ds := Insert("items") + + ds1 := ds.Rows(item{Name: "Test", Address: "111 Test Addr"}) + + insertSQL, args, err := ds1.ToSQL() + ids.NoError(err) + ids.Empty(args) + ids.Equal(insertSQL, `INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', 'Test')`) + + insertSQL, args, err = ds1.Prepared(true).ToSQL() + ids.NoError(err) + ids.Equal([]interface{}{"111 Test Addr", "Test"}, args) + ids.Equal(insertSQL, `INSERT INTO "items" ("address", "name") VALUES (?, ?)`) + + ds1 = ds.Rows(item{}) + + insertSQL, args, err = ds1.ToSQL() + ids.NoError(err) + ids.Empty(args) + ids.Equal(insertSQL, `INSERT INTO "items" ("address", "name") VALUES (DEFAULT, DEFAULT)`) + + insertSQL, args, err = ds1.Prepared(true).ToSQL() + ids.NoError(err) + ids.Empty(args) + ids.Equal(insertSQL, `INSERT INTO "items" ("address", "name") VALUES (DEFAULT, DEFAULT)`) +} + func (ids *insertDatasetSuite) TestRows_ToSQLWithDefaultValues() { t := ids.T() ds := Insert("items") diff --git a/internal/util/reflect.go b/internal/util/reflect.go index d1c5b6b9..a2963d6a 100644 --- a/internal/util/reflect.go +++ b/internal/util/reflect.go @@ -12,18 +12,20 @@ import ( type ( ColumnData struct { - ColumnName string - FieldIndex []int - ShouldInsert bool - ShouldUpdate bool - GoType reflect.Type + ColumnName string + FieldIndex []int + ShouldInsert bool + ShouldUpdate bool + DefaultIfEmpty bool + GoType reflect.Type } ColumnMap map[string]ColumnData ) const ( - skipUpdateTagName = "skipupdate" - skipInsertTagName = "skipinsert" + skipUpdateTagName = "skipupdate" + skipInsertTagName = "skipinsert" + defaultIfEmptyTagName = "defaultifempty" ) func IsUint(k reflect.Kind) bool { @@ -71,6 +73,26 @@ func IsPointer(k reflect.Kind) bool { return k == reflect.Ptr } +func IsEmptyValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + case reflect.Invalid: + return true + } + return false +} + var structMapCache = make(map[interface{}]ColumnMap) var structMapCacheLock = sync.Mutex{} @@ -189,11 +211,12 @@ func createColumnMap(t reflect.Type, fieldIndex []int) ColumnMap { goquTag := tag.New("goqu", f.Tag) if !dbTag.Equals("-") { cm[columnName] = ColumnData{ - ColumnName: columnName, - ShouldInsert: !goquTag.Contains(skipInsertTagName), - ShouldUpdate: !goquTag.Contains(skipUpdateTagName), - FieldIndex: append(fieldIndex, f.Index...), - GoType: f.Type, + ColumnName: columnName, + ShouldInsert: !goquTag.Contains(skipInsertTagName), + ShouldUpdate: !goquTag.Contains(skipUpdateTagName), + DefaultIfEmpty: goquTag.Contains(defaultIfEmptyTagName), + FieldIndex: append(fieldIndex, f.Index...), + GoType: f.Type, } } } diff --git a/internal/util/reflect_test.go b/internal/util/reflect_test.go index 7470c948..b1b722d7 100644 --- a/internal/util/reflect_test.go +++ b/internal/util/reflect_test.go @@ -49,6 +49,40 @@ var ( } ) +type ( + TestInterface interface { + A() string + } + TestInterfaceImpl struct { + str string + } + TestStruct struct { + arr [0]string + slc []string + mp map[string]interface{} + str string + bl bool + i int + i8 int8 + i16 int16 + i32 int32 + i64 int64 + ui uint + ui8 uint8 + ui16 uint16 + ui32 uint32 + ui64 uint64 + f32 float32 + f64 float64 + intr TestInterface + ptr *sql.NullString + } +) + +func (t TestInterfaceImpl) A() string { + return t.str +} + type reflectTest struct { suite.Suite } @@ -296,6 +330,52 @@ func (rt *reflectTest) TestIsPointer() { } } +func (rt *reflectTest) TestIsEmptyValue_emptyValues() { + ts := TestStruct{} + rt.True(IsEmptyValue(reflect.ValueOf(ts.arr))) + rt.True(IsEmptyValue(reflect.ValueOf(ts.slc))) + rt.True(IsEmptyValue(reflect.ValueOf(ts.mp))) + rt.True(IsEmptyValue(reflect.ValueOf(ts.str))) + rt.True(IsEmptyValue(reflect.ValueOf(ts.bl))) + rt.True(IsEmptyValue(reflect.ValueOf(ts.i))) + rt.True(IsEmptyValue(reflect.ValueOf(ts.i8))) + rt.True(IsEmptyValue(reflect.ValueOf(ts.i16))) + rt.True(IsEmptyValue(reflect.ValueOf(ts.i32))) + rt.True(IsEmptyValue(reflect.ValueOf(ts.i64))) + rt.True(IsEmptyValue(reflect.ValueOf(ts.ui))) + rt.True(IsEmptyValue(reflect.ValueOf(ts.ui8))) + rt.True(IsEmptyValue(reflect.ValueOf(ts.ui16))) + rt.True(IsEmptyValue(reflect.ValueOf(ts.ui32))) + rt.True(IsEmptyValue(reflect.ValueOf(ts.ui64))) + rt.True(IsEmptyValue(reflect.ValueOf(ts.f32))) + rt.True(IsEmptyValue(reflect.ValueOf(ts.f64))) + rt.True(IsEmptyValue(reflect.ValueOf(ts.intr))) + rt.True(IsEmptyValue(reflect.ValueOf(ts.ptr))) +} + +func (rt *reflectTest) TestIsEmptyValue_validValues() { + ts := TestStruct{intr: TestInterfaceImpl{"hello"}} + rt.False(IsEmptyValue(reflect.ValueOf([1]string{"a"}))) + rt.False(IsEmptyValue(reflect.ValueOf([]string{"a"}))) + rt.False(IsEmptyValue(reflect.ValueOf(map[string]interface{}{"a": true}))) + rt.False(IsEmptyValue(reflect.ValueOf("str"))) + rt.False(IsEmptyValue(reflect.ValueOf(true))) + rt.False(IsEmptyValue(reflect.ValueOf(int(1)))) + rt.False(IsEmptyValue(reflect.ValueOf(int8(1)))) + rt.False(IsEmptyValue(reflect.ValueOf(int16(1)))) + rt.False(IsEmptyValue(reflect.ValueOf(int32(1)))) + rt.False(IsEmptyValue(reflect.ValueOf(int64(1)))) + rt.False(IsEmptyValue(reflect.ValueOf(uint(1)))) + rt.False(IsEmptyValue(reflect.ValueOf(uint8(1)))) + rt.False(IsEmptyValue(reflect.ValueOf(uint16(1)))) + rt.False(IsEmptyValue(reflect.ValueOf(uint32(1)))) + rt.False(IsEmptyValue(reflect.ValueOf(uint64(1)))) + rt.False(IsEmptyValue(reflect.ValueOf(float32(0.1)))) + rt.False(IsEmptyValue(reflect.ValueOf(float64(0.2)))) + rt.False(IsEmptyValue(reflect.ValueOf(ts.intr))) + rt.False(IsEmptyValue(reflect.ValueOf(&TestStruct{str: "a"}))) +} + func (rt *reflectTest) TestColumnRename() { t := rt.T() // different key names are used each time to circumvent the caching that happens @@ -736,16 +816,25 @@ func (rt *reflectTest) TestGetColumnMap_withStructGoquTags() { Str string `goqu:"skipinsert,skipupdate"` Int int64 `goqu:"skipinsert"` Bool bool `goqu:"skipupdate"` + Empty bool `goqu:"defaultifempty"` Valuer *sql.NullString } var ts TestStruct cm, err := GetColumnMap(&ts) assert.NoError(t, err) assert.Equal(t, ColumnMap{ - "str": {ColumnName: "str", FieldIndex: []int{0}, ShouldInsert: false, ShouldUpdate: false, GoType: reflect.TypeOf("")}, - "int": {ColumnName: "int", FieldIndex: []int{1}, ShouldInsert: false, ShouldUpdate: true, GoType: reflect.TypeOf(int64(1))}, - "bool": {ColumnName: "bool", FieldIndex: []int{2}, ShouldInsert: true, ShouldUpdate: false, GoType: reflect.TypeOf(true)}, - "valuer": {ColumnName: "valuer", FieldIndex: []int{3}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(&sql.NullString{})}, + "str": {ColumnName: "str", FieldIndex: []int{0}, ShouldInsert: false, ShouldUpdate: false, GoType: reflect.TypeOf("")}, + "int": {ColumnName: "int", FieldIndex: []int{1}, ShouldInsert: false, ShouldUpdate: true, GoType: reflect.TypeOf(int64(1))}, + "bool": {ColumnName: "bool", FieldIndex: []int{2}, ShouldInsert: true, ShouldUpdate: false, GoType: reflect.TypeOf(true)}, + "empty": { + ColumnName: "empty", + FieldIndex: []int{3}, + ShouldInsert: true, + ShouldUpdate: true, + DefaultIfEmpty: true, + GoType: reflect.TypeOf(true), + }, + "valuer": {ColumnName: "valuer", FieldIndex: []int{4}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(&sql.NullString{})}, }, cm) } diff --git a/update_dataset_example_test.go b/update_dataset_example_test.go index e875283a..d6c14914 100644 --- a/update_dataset_example_test.go +++ b/update_dataset_example_test.go @@ -500,6 +500,26 @@ func ExampleUpdateDataset_Set_withSkipUpdateTag() { // UPDATE "items" SET "address"='111 Test Addr' [] } +func ExampleUpdateDataset_Set_withDefaultIfEmptyTag() { + type item struct { + Address string `db:"address"` + Name string `db:"name" goqu:"defaultifempty"` + } + sql, args, _ := goqu.Update("items").Set( + item{Address: "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = goqu.Update("items").Set( + item{Name: "Bob Yukon", Address: "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + // Output: + // UPDATE "items" SET "address"='111 Test Addr',"name"=DEFAULT [] + // UPDATE "items" SET "address"='111 Test Addr',"name"='Bob Yukon' [] +} + func ExampleUpdateDataset_Set_withNoTags() { type item struct { Address string diff --git a/update_dataset_test.go b/update_dataset_test.go index 82ac08c9..6cd46070 100644 --- a/update_dataset_test.go +++ b/update_dataset_test.go @@ -361,6 +361,38 @@ func (uds *updateDatasetSuite) TestSet_ToSQLWithSkipupdateTag() { assert.Equal(t, `UPDATE "items" SET "name"=?`, updateSQL) } +func (uds *updateDatasetSuite) TestSet_ToSQLWithDefaultIfEmptyTag() { + type item struct { + Address string `db:"address" goqu:"skipupdate, defaultifempty"` + Name string `db:"name" goqu:"defaultifempty"` + Alias *string `db:"alias" goqu:"defaultifempty"` + } + ds := Update("items").Set(item{Name: "Test", Address: "111 Test Addr"}) + + updateSQL, args, err := ds.ToSQL() + uds.NoError(err) + uds.Empty(args) + uds.Equal(`UPDATE "items" SET "alias"=DEFAULT,"name"='Test'`, updateSQL) + + updateSQL, args, err = ds.Prepared(true).ToSQL() + uds.NoError(err) + uds.Equal([]interface{}{"Test"}, args) + uds.Equal(`UPDATE "items" SET "alias"=DEFAULT,"name"=?`, updateSQL) + + var alias = "" + ds = ds.Set(item{Alias: &alias}) + + updateSQL, args, err = ds.ToSQL() + uds.NoError(err) + uds.Empty(args) + uds.Equal(`UPDATE "items" SET "alias"='',"name"=DEFAULT`, updateSQL) + + updateSQL, args, err = ds.Prepared(true).ToSQL() + uds.NoError(err) + uds.Equal([]interface{}{""}, args) + uds.Equal(`UPDATE "items" SET "alias"=?,"name"=DEFAULT`, updateSQL) +} + func (uds *updateDatasetSuite) TestFrom() { ds := Update("test") dsc := ds.GetClauses()