Skip to content

Struct tag to skip nil values #174

@randallmlough

Description

@randallmlough

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()
		}
	}
}

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions