-
Notifications
You must be signed in to change notification settings - Fork 219
Description
Is your feature request related to a problem? Please describe.
I'm looking to see if there's a tag available to skip a field if that field value is nil.
My use case: I use "request" types to limit the scope of what can be modified in a data set. I heavily use this style around updating, which allows you to skip the update of a value/column if it's nil, but update the value if it's a real value including an empty string (ex. user wants to remove their phone number)
// standard type, no pointers
UserCreateRequest struct {
Name string `db:"name" validate:"required"`
Email string `db:"email" validate:"required"`
Password string `db:"password" validate:"required"`
Age int `db:"age"`
Phone string `db:"phone"`
}
// pointer types to allow updating of only real values. Empty string "" or 0 for example is considered a real value.
UserUpdateRequest struct {
Name *string `db:"name"`
Age *int `db:"age"`
Phone *string `db:"phone"`
}
// Example request struct
// struct is typically filled in the unmarshalling process, but for example purposes I have it more explicit
ur := &UserUpdateRequest{}
phone := ""
ur.Phone = phone
stmt, args, _ := db.Update("tests").Set(ur).Where(goqu.Ex{"id":1}).Prepared(true).ToSql()
// Output:
// stmt: UPDATE "tests" SET "phone"=$1 WHERE ("id" = $2)
// args: []interface{"",1}
Describe the solution you'd like
maybe a new tag that checks for and skips nil values? ie. db:"skipIfNil"
Describe alternatives you've considered
I saw this already, but doesn't address the request as far as I can tell.
https://godoc.org/github.com/doug-martin/goqu#example-UpdateDataset-Set-WithNilEmbeddedPointer
Dialect
- postgres
- mysql
- sqlite3
I've loosely implemented this already with the below code, but it has some limitations. You may be able to include it more seamlessly with the functionality you already have.
// flattenStruct takes in a struct and will reduce it to a map.
// If the type is a pointer and it is nil or if has an ignore tag "-", the key/value pair will not be added to the map.
// it's best to use pointer types on update request structs since only real values will remain in final map.
func flattenStruct(i interface{}) interface{} {
v := reflect.ValueOf(i)
if v.Kind() == reflect.Interface {
v = v.Elem()
}
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
switch v.Kind() {
case reflect.Map:
return i
case reflect.Struct:
m := make(map[string]interface{})
mapRecursion(v, "", m)
return m
case reflect.Slice:
mm := make([]map[string]interface{}, v.Len())
for i := 0; i < v.Len(); i++ {
vv := v.Index(i)
m := make(map[string]interface{})
mapRecursion(vv, "", m)
mm[i] = m
}
return mm
}
return map[string]interface{}{}
}
func mapRecursion(v reflect.Value, mapKey string, m map[string]interface{}) {
switch v.Kind() {
case reflect.Interface:
if !v.IsNil() {
mapRecursion(v.Elem(), mapKey, m)
}
case reflect.Ptr:
if !v.IsNil() {
mapRecursion(v.Elem(), mapKey, m)
}
case reflect.Struct:
switch t := v.Interface().(type) {
case time.Time:
if !t.IsZero() {
m[strings.CamelCase(mapKey)] = v.Interface()
}
case driver.Valuer:
m[strings.CamelCase(mapKey)] = v.Interface()
default:
for i := 0; i < v.NumField(); i++ {
val := v.Field(i)
tf := v.Type().Field(i)
q := tf.Tag.Get("db")
if q == "-" {
continue
}
mapRecursion(val, tf.Name, m)
}
}
case reflect.Map:
iter := v.MapRange()
for iter.Next() {
k := iter.Key()
v := iter.Value()
mapRecursion(v, k.String(), m)
}
case reflect.Slice:
if !v.CanInterface() {
return
}
switch t := v.Interface().(type) {
case []int:
ii := make(pq.Int64Array, v.Len())
for i := 0; i < v.Len(); i++ {
ii[i] = int64(t[i])
}
mapRecursion(reflect.ValueOf(ii), mapKey, m)
case []int64:
mapRecursion(reflect.ValueOf(pq.Int64Array(t)), mapKey, m)
case []float64:
mapRecursion(reflect.ValueOf(pq.Float64Array(t)), mapKey, m)
case []string:
mapRecursion(reflect.ValueOf(pq.StringArray(t)), mapKey, m)
case []bool:
mapRecursion(reflect.ValueOf(pq.BoolArray(t)), mapKey, m)
case pq.Int64Array, pq.StringArray, pq.BoolArray, pq.Float64Array:
m[strings.CamelCase(mapKey)] = t
}
default:
if v.CanInterface() && mapKey != "" {
m[strings.CamelCase(mapKey)] = v.Interface()
}
}
}