diff --git a/docs/inserting.md b/docs/inserting.md index e6f5fa28..5e07a363 100644 --- a/docs/inserting.md +++ b/docs/inserting.md @@ -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. diff --git a/docs/updating.md b/docs/updating.md index e0d7dcf1..d22cbf7d 100644 --- a/docs/updating.md +++ b/docs/updating.md @@ -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 diff --git a/exp/record.go b/exp/record.go index 80b99ae0..9214c560 100644 --- a/exp/record.go +++ b/exp/record.go @@ -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) + } } } } @@ -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() } } diff --git a/insert_dataset_example_test.go b/insert_dataset_example_test.go index fc09b911..755b3d69 100644 --- a/insert_dataset_example_test.go +++ b/insert_dataset_example_test.go @@ -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"` diff --git a/internal/util/column_map.go b/internal/util/column_map.go index b8269341..8cbebe67 100644 --- a/internal/util/column_map.go +++ b/internal/util/column_map.go @@ -15,6 +15,8 @@ type ( ShouldInsert bool ShouldUpdate bool DefaultIfEmpty bool + OmitNil bool + OmitEmpty bool GoType reflect.Type } ColumnMap map[string]ColumnData @@ -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, } diff --git a/internal/util/reflect.go b/internal/util/reflect.go index bc18b8bd..82683395 100644 --- a/internal/util/reflect.go +++ b/internal/util/reflect.go @@ -13,6 +13,8 @@ const ( skipUpdateTagName = "skipupdate" skipInsertTagName = "skipinsert" defaultIfEmptyTagName = "defaultifempty" + omitNilTagName = "omitnil" + omitEmptyTagName = "omitempty" ) var scannerType = reflect.TypeOf((*sql.Scanner)(nil)).Elem() @@ -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{} diff --git a/internal/util/reflect_test.go b/internal/util/reflect_test.go index e461add7..fc576d5f 100644 --- a/internal/util/reflect_test.go +++ b/internal/util/reflect_test.go @@ -342,6 +342,8 @@ func (rt *reflectTest) TestIsEmptyValue_emptyValues() { rt.True(util.IsEmptyValue(reflect.ValueOf(ts.f64))) rt.True(util.IsEmptyValue(reflect.ValueOf(ts.intr))) rt.True(util.IsEmptyValue(reflect.ValueOf(ts.ptr))) + rt.True(util.IsEmptyValue(reflect.ValueOf(ts))) + rt.True(util.IsNil(reflect.ValueOf(nil))) } func (rt *reflectTest) TestIsEmptyValue_validValues() { @@ -365,6 +367,34 @@ func (rt *reflectTest) TestIsEmptyValue_validValues() { rt.False(util.IsEmptyValue(reflect.ValueOf(float64(0.2)))) rt.False(util.IsEmptyValue(reflect.ValueOf(ts.intr))) rt.False(util.IsEmptyValue(reflect.ValueOf(&TestStruct{str: "a"}))) + rt.False(util.IsEmptyValue(reflect.ValueOf(ts))) +} + +func (rt *reflectTest) TestIsNil() { + ts := TestStruct{} + rt.False(util.IsNil(reflect.ValueOf(ts.arr))) + rt.True(util.IsNil(reflect.ValueOf(ts.slc))) + rt.False(util.IsEmptyValue(reflect.ValueOf([]string{"a"}))) + rt.True(util.IsNil(reflect.ValueOf(ts.mp))) + rt.False(util.IsEmptyValue(reflect.ValueOf(map[string]interface{}{"a": true}))) + rt.False(util.IsNil(reflect.ValueOf(ts.str))) + rt.False(util.IsNil(reflect.ValueOf(ts.bl))) + rt.False(util.IsNil(reflect.ValueOf(ts.i))) + rt.False(util.IsNil(reflect.ValueOf(ts.i8))) + rt.False(util.IsNil(reflect.ValueOf(ts.i16))) + rt.False(util.IsNil(reflect.ValueOf(ts.i32))) + rt.False(util.IsNil(reflect.ValueOf(ts.i64))) + rt.False(util.IsNil(reflect.ValueOf(ts.ui))) + rt.False(util.IsNil(reflect.ValueOf(ts.ui8))) + rt.False(util.IsNil(reflect.ValueOf(ts.ui16))) + rt.False(util.IsNil(reflect.ValueOf(ts.ui32))) + rt.False(util.IsNil(reflect.ValueOf(ts.ui64))) + rt.False(util.IsNil(reflect.ValueOf(ts.f32))) + rt.False(util.IsNil(reflect.ValueOf(ts.f64))) + rt.True(util.IsNil(reflect.ValueOf(ts.intr))) + rt.True(util.IsNil(reflect.ValueOf(ts.ptr))) + rt.False(util.IsNil(reflect.ValueOf(ts))) + rt.True(util.IsNil(reflect.ValueOf(nil))) } func (rt *reflectTest) TestColumnRename() { @@ -681,11 +711,13 @@ func (rt *reflectTest) TestGetColumnMap_withStruct() { func (rt *reflectTest) TestGetColumnMap_withStructGoquTags() { type TestStruct struct { - Str string `goqu:"skipinsert,skipupdate"` - Int int64 `goqu:"skipinsert"` - Bool bool `goqu:"skipupdate"` - Empty bool `goqu:"defaultifempty"` - Valuer *sql.NullString + Str string `goqu:"skipinsert,skipupdate"` + Int int64 `goqu:"skipinsert"` + Bool bool `goqu:"skipupdate"` + Empty bool `goqu:"defaultifempty"` + OmitNil bool `goqu:"omitnil"` + OmitEmpty bool `goqu:"omitempty"` + Valuer *sql.NullString } var ts TestStruct cm, err := util.GetColumnMap(&ts) @@ -702,7 +734,23 @@ func (rt *reflectTest) TestGetColumnMap_withStructGoquTags() { DefaultIfEmpty: true, GoType: reflect.TypeOf(true), }, - "valuer": {ColumnName: "valuer", FieldIndex: []int{4}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(&sql.NullString{})}, + "omitnil": { + ColumnName: "omitnil", + FieldIndex: []int{4}, + ShouldInsert: true, + ShouldUpdate: true, + OmitNil: true, + GoType: reflect.TypeOf(true), + }, + "omitempty": { + ColumnName: "omitempty", + FieldIndex: []int{5}, + ShouldInsert: true, + ShouldUpdate: true, + OmitEmpty: true, + GoType: reflect.TypeOf(true), + }, + "valuer": {ColumnName: "valuer", FieldIndex: []int{6}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(&sql.NullString{})}, }, cm) } diff --git a/update_dataset_example_test.go b/update_dataset_example_test.go index 49852fa1..096dfa38 100644 --- a/update_dataset_example_test.go +++ b/update_dataset_example_test.go @@ -2,6 +2,7 @@ package goqu_test import ( + dbsql "database/sql" "fmt" "github.com/doug-martin/goqu/v9" @@ -22,6 +23,83 @@ func ExampleUpdate_withStruct() { // UPDATE "items" SET "address"='111 Test Addr',"name"='Test' [] } +func ExampleUpdate_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 := "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"='' [] +} + +func ExampleUpdate_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 := "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' [] +} + +func ExampleUpdate_withOmitEmptyTag_valuer() { + type item struct { + FirstName dbsql.NullString `db:"first_name" goqu:"omitempty"` + MiddleName dbsql.NullString `db:"middle_name" goqu:"omitempty"` + LastName dbsql.NullString `db:"last_name" goqu:"omitempty"` + Address1 *dbsql.NullString `db:"address1" goqu:"omitempty"` + Address2 *dbsql.NullString `db:"address2" goqu:"omitempty"` + Address3 *dbsql.NullString `db:"address3" goqu:"omitempty"` + Address4 *dbsql.NullString `db:"address4" goqu:"omitempty"` + } + query, args, _ := goqu.Update("items").Set( + item{ + FirstName: dbsql.NullString{Valid: true, String: "Test First Name"}, + MiddleName: dbsql.NullString{Valid: true, String: ""}, + LastName: dbsql.NullString{}, // will omit zero valuer struct + Address1: &dbsql.NullString{Valid: true, String: "Test Address 1"}, + Address2: &dbsql.NullString{Valid: true, String: ""}, + Address3: &dbsql.NullString{}, + Address4: nil, // will omit nil pointer + }, + ).ToSQL() + fmt.Println(query, args) + + // Output: + // UPDATE "items" SET "address1"='Test Address 1',"address2"='',"address3"=NULL,"first_name"='Test First Name',"middle_name"='' [] +} + func ExampleUpdate_withGoquRecord() { sql, args, _ := goqu.Update("items").Set( goqu.Record{"name": "Test", "address": "111 Test Addr"},