Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: omitempty and omitnil struct tags #309

Merged
merged 6 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions docs/inserting.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,65 @@ Output:
```
INSERT INTO "user" ("last_name") VALUES ('Farley'), ('Stewart'), ('Jeffers') []
```
If you do not want to set the database field when the struct field is a nil pointer you can use the `omitnil` tag.

```go
type item struct {
FirstName string `db:"first_name" goqu:"omitnil"`
LastName string `db:"last_name" goqu:"omitnil"`
Address1 *string `db:"address1" goqu:"omitnil"`
Address2 *string `db:"address2" goqu:"omitnil"`
Address3 *string `db:"address3" goqu:"omitnil"`
}
address1 := "111 Test Addr"
var emptyString string
i := item{
FirstName: "Test First Name",
LastName: "",
Address1: &address1,
Address2: &emptyString,
Address3: nil, // will omit nil pointer
}

insertSQL, args, _ := goqu.Insert("items").Rows(i).ToSQL()
fmt.Println(insertSQL, args)
```

Output:
```
INSERT INTO "items" ("address1", "address2", "first_name", "last_name") VALUES ('111 Test Addr', '', 'Test First Name', '') []
```

If you do not want to set the database field when the struct field is a zero value (including nil pointers) you can use
the `omitempty` tag.

Empty embedded structs implementing the `Valuer` interface (eg. `sql.NullString`) will also be omitted.

```go
type item struct {
FirstName string `db:"first_name" goqu:"omitempty"`
LastName string `db:"last_name" goqu:"omitempty"`
Address1 *string `db:"address1" goqu:"omitempty"`
Address2 *string `db:"address2" goqu:"omitempty"`
Address3 *string `db:"address3" goqu:"omitempty"`
}
address1 := "112 Test Addr"
var emptyString string
i := item{
FirstName: "Test First Name",
LastName: "", // will omit zero field
Address1: &address1,
Address2: &emptyString,
Address3: nil, // will omit nil pointer
}
insertSQL, args, _ := goqu.Insert("items").Rows(i).ToSQL()
fmt.Println(insertSQL, args)
```

Output:
```
INSERT INTO "items" ("address1", "address2", "first_name") VALUES ('112 Test Addr', '', 'Test First Name') []
```

If you want to use the database `DEFAULT` when the struct field is a zero value you can use the `defaultifempty` tag.

Expand Down
62 changes: 62 additions & 0 deletions docs/updating.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,68 @@ Output:
UPDATE "items" SET "address"='111 Test Addr' []
```

If you do not want to update the database field when the struct field is a nil pointer you can use the `omitnil` tag.
This allows a struct of pointers to be used to represent partial updates where nil pointers were not changed.

```go
type item struct {
FirstName string `db:"first_name" goqu:"omitnil"`
LastName string `db:"last_name" goqu:"omitnil"`
Address1 *string `db:"address1" goqu:"omitnil"`
Address2 *string `db:"address2" goqu:"omitnil"`
Address3 *string `db:"address3" goqu:"omitnil"`
}
address1 := "113 Test Addr"
var emptyString string
sql, args, _ := goqu.Update("items").Set(
item{
FirstName: "Test First Name",
LastName: "",
Address1: &address1,
Address2: &emptyString,
Address3: nil, // will omit nil pointer
},
).ToSQL()
fmt.Println(sql, args)
```

Output:
```
UPDATE "items" SET "address1"='113 Test Addr',"address2"='',"first_name"='Test First Name',"last_name"='' []
```

If you do not want to update the database field when the struct field is a zero value (including nil pointers) you can
use the `omitempty` tag.

Empty embedded structs implementing the `Valuer` interface (eg. `sql.NullString`) will also be omitted.

```go
type item struct {
FirstName string `db:"first_name" goqu:"omitempty"`
LastName string `db:"last_name" goqu:"omitempty"`
Address1 *string `db:"address1" goqu:"omitempty"`
Address2 *string `db:"address2" goqu:"omitempty"`
Address3 *string `db:"address3" goqu:"omitempty"`
}
address1 := "114 Test Addr"
var emptyString string
sql, args, _ := goqu.Update("items").Set(
item{
FirstName: "Test First Name",
LastName: "", // will omit zero field
Address1: &address1,
Address2: &emptyString,
Address3: nil, // will omit nil pointer
},
).ToSQL()
fmt.Println(sql, args)
```

Output:
```
UPDATE "items" SET "address1"='114 Test Addr',"address2"='',"first_name"='Test First Name' []
```

If you want to use the database `DEFAULT` when the struct field is a zero value you can use the `defaultifempty` tag.

```go
Expand Down
29 changes: 19 additions & 10 deletions exp/record.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ func NewRecordFromStruct(i interface{}, forInsert, forUpdate bool) (r Record, er
for _, col := range cols {
f := cm[col]
if !shouldSkipField(f, forInsert, forUpdate) {
if ok, fieldVal := getFieldValue(value, f); ok {
r[f.ColumnName] = fieldVal
if fieldValue, isAvailable := util.SafeGetFieldByIndex(value, f.FieldIndex); isAvailable {
if !shouldOmitField(fieldValue, f) {
r[f.ColumnName] = getRecordValue(fieldValue, f)
}
}
}
}
Expand All @@ -46,14 +48,21 @@ func shouldSkipField(f util.ColumnData, forInsert, forUpdate bool) bool {
return shouldSkipInsert || shouldSkipUpdate
}

func getFieldValue(val reflect.Value, f util.ColumnData) (ok bool, fieldVal interface{}) {
if v, isAvailable := util.SafeGetFieldByIndex(val, f.FieldIndex); !isAvailable {
return false, nil
} else if f.DefaultIfEmpty && util.IsEmptyValue(v) {
return true, Default()
} else if v.IsValid() {
return true, v.Interface()
func shouldOmitField(val reflect.Value, f util.ColumnData) bool {
if f.OmitNil && util.IsNil(val) {
return true
} else if f.OmitEmpty && util.IsEmptyValue(val) {
return true
}
return false
}

func getRecordValue(val reflect.Value, f util.ColumnData) interface{} {
if f.DefaultIfEmpty && util.IsEmptyValue(val) {
return Default()
} else if val.IsValid() {
return val.Interface()
} else {
return true, reflect.Zero(f.GoType).Interface()
return reflect.Zero(f.GoType).Interface()
}
}
75 changes: 75 additions & 0 deletions insert_dataset_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,81 @@ func ExampleInsertDataset_Rows_withGoquSkipInsertTag() {
// INSERT INTO "items" ("address") VALUES ('111 Test Addr'), ('112 Test Addr') []
}

func ExampleInsertDataset_Rows_withOmitNilTag() {
type item struct {
FirstName string `db:"first_name" goqu:"omitnil"`
LastName string `db:"last_name" goqu:"omitnil"`
Address1 *string `db:"address1" goqu:"omitnil"`
Address2 *string `db:"address2" goqu:"omitnil"`
Address3 *string `db:"address3" goqu:"omitnil"`
}
address1 := "111 Test Addr"
var emptyString string
i := item{
FirstName: "Test First Name",
LastName: "",
Address1: &address1,
Address2: &emptyString,
Address3: nil, // will omit nil pointer
}

insertSQL, args, _ := goqu.Insert("items").Rows(i).ToSQL()
fmt.Println(insertSQL, args)

// Output:
// INSERT INTO "items" ("address1", "address2", "first_name", "last_name") VALUES ('111 Test Addr', '', 'Test First Name', '') []
}

func ExampleInsertDataset_Rows_withOmitEmptyTag() {
type item struct {
FirstName string `db:"first_name" goqu:"omitempty"`
LastName string `db:"last_name" goqu:"omitempty"`
Address1 *string `db:"address1" goqu:"omitempty"`
Address2 *string `db:"address2" goqu:"omitempty"`
Address3 *string `db:"address3" goqu:"omitempty"`
}
address1 := "112 Test Addr"
var emptyString string
i := item{
FirstName: "Test First Name",
LastName: "", // will omit zero field
Address1: &address1,
Address2: &emptyString,
Address3: nil, // will omit nil pointer
}
insertSQL, args, _ := goqu.Insert("items").Rows(i).ToSQL()
fmt.Println(insertSQL, args)

// Output:
// INSERT INTO "items" ("address1", "address2", "first_name") VALUES ('112 Test Addr', '', 'Test First Name') []
}

func ExampleInsertDataset_Rows_withOmitEmptyTag_Valuer() {
type item struct {
FirstName sql.NullString `db:"first_name" goqu:"omitempty"`
MiddleName sql.NullString `db:"middle_name" goqu:"omitempty"`
LastName sql.NullString `db:"last_name" goqu:"omitempty"`
Address1 *sql.NullString `db:"address1" goqu:"omitempty"`
Address2 *sql.NullString `db:"address2" goqu:"omitempty"`
Address3 *sql.NullString `db:"address3" goqu:"omitempty"`
Address4 *sql.NullString `db:"address4" goqu:"omitempty"`
}
i := item{
FirstName: sql.NullString{Valid: true, String: "Test First Name"},
MiddleName: sql.NullString{Valid: true, String: ""},
LastName: sql.NullString{}, // will omit zero valuer struct
Address1: &sql.NullString{Valid: true, String: "Test Address 1"},
Address2: &sql.NullString{Valid: true, String: ""},
Address3: &sql.NullString{},
Address4: nil, // will omit nil pointer
}
insertSQL, args, _ := goqu.Insert("items").Rows(i).ToSQL()
fmt.Println(insertSQL, args)

// Output:
// INSERT INTO "items" ("address1", "address2", "address3", "first_name", "middle_name") VALUES ('Test Address 1', '', NULL, 'Test First Name', '') []
}

func ExampleInsertDataset_Rows_withGoquDefaultIfEmptyTag() {
type item struct {
ID uint32 `goqu:"skipinsert"`
Expand Down
4 changes: 4 additions & 0 deletions internal/util/column_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ type (
ShouldInsert bool
ShouldUpdate bool
DefaultIfEmpty bool
OmitNil bool
OmitEmpty bool
GoType reflect.Type
}
ColumnMap map[string]ColumnData
Expand Down Expand Up @@ -91,6 +93,8 @@ func newColumnData(f *reflect.StructField, columnName string, fieldIndex []int,
ShouldInsert: !goquTag.Contains(skipInsertTagName),
ShouldUpdate: !goquTag.Contains(skipUpdateTagName),
DefaultIfEmpty: goquTag.Contains(defaultIfEmptyTagName),
OmitNil: goquTag.Contains(omitNilTagName),
OmitEmpty: goquTag.Contains(omitEmptyTagName),
FieldIndex: concatFieldIndexes(fieldIndex, f.Index),
GoType: f.Type,
}
Expand Down
25 changes: 11 additions & 14 deletions internal/util/reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const (
skipUpdateTagName = "skipupdate"
skipInsertTagName = "skipinsert"
defaultIfEmptyTagName = "defaultifempty"
omitNilTagName = "omitnil"
omitEmptyTagName = "omitempty"
)

var scannerType = reflect.TypeOf((*sql.Scanner)(nil)).Elem()
Expand Down Expand Up @@ -62,27 +64,22 @@ func IsPointer(k reflect.Kind) bool {
return k == reflect.Ptr
}

func IsEmptyValue(v reflect.Value) bool {
func IsNil(v reflect.Value) bool {
if !v.IsValid() {
return true
}
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:
case reflect.Ptr, reflect.Interface, reflect.Slice, reflect.Map, reflect.Chan, reflect.Func:
return v.IsNil()
case reflect.Invalid:
return true
default:
return false
}
}

func IsEmptyValue(v reflect.Value) bool {
return !v.IsValid() || v.IsZero()
}

var (
structMapCache = make(map[interface{}]ColumnMap)
structMapCacheLock = sync.Mutex{}
Expand Down
Loading