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

Struct tag to skip nil values #174

Open
1 of 3 tasks
randallmlough opened this issue Oct 17, 2019 · 4 comments
Open
1 of 3 tasks

Struct tag to skip nil values #174

randallmlough opened this issue Oct 17, 2019 · 4 comments

Comments

@randallmlough
Copy link

randallmlough commented Oct 17, 2019

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()
		}
	}
}
@davae1an
Copy link

Im also looking for this feature. Something like a omitempty tag

@randallmlough
Copy link
Author

Im also looking for this feature. Something like a omitempty tag

I second this. Omitempty seems like the obvious choice for this

@doug-martin
Copy link
Owner

doug-martin commented Oct 31, 2019

@randallmlough sorry I haven't gotten around to looking at this yet, Ill take a look tonight.

(edit) tomorrow forgot tonight was halloween!

@mrg0lden
Copy link

Is there any update on this? it's a very needed feature, especially for UPDATE statements..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants