Skip to content

Commit

Permalink
Support column DEFAULT when inserting/updating via struct #27
Browse files Browse the repository at this point in the history
  • Loading branch information
doug-martin committed Jul 26, 2019
1 parent 4a84b3e commit e3050b4
Show file tree
Hide file tree
Showing 13 changed files with 317 additions and 24 deletions.
4 changes: 4 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
42 changes: 42 additions & 0 deletions docs/inserting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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') []
```

<a name="insert-map"></a>
**Insert `map[string]interface{}`**

Expand Down
18 changes: 18 additions & 0 deletions docs/updating.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 []
```

<a name="set-map"></a>
**[Set with Map](https://godoc.org/github.com/doug-martin/goqu/#UpdateDataset.Set)**

Expand Down
6 changes: 5 additions & 1 deletion exp/insert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions exp/literal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
}
Expand Down
6 changes: 5 additions & 1 deletion exp/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
28 changes: 23 additions & 5 deletions insert_dataset_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
34 changes: 34 additions & 0 deletions insert_dataset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
47 changes: 35 additions & 12 deletions internal/util/reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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{}

Expand Down Expand Up @@ -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,
}
}
}
Expand Down
Loading

0 comments on commit e3050b4

Please sign in to comment.