Skip to content

Commit

Permalink
Named: missing field support for ::names/::vales/::name=::value
Browse files Browse the repository at this point in the history
  • Loading branch information
jfbus committed Oct 5, 2015
1 parent eb7c766 commit 4abbe79
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 5 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ sqlbind.Named("SELECT /* {comment} */ * FROM {table_prefix}example WHERE name=:n

Braces inside quotes are ignored : `"{value}"` will not be modified.

## JSON and missing fields [TODO]
## JSON and missing fields

In a REST API, `PATCH` update calls may update only certain fields. When using structs with plain types, it is impossible to differentiate between empty fields `{"name":""}`, null fields : `{"name": null}` and missing fields : `{}`.

Expand Down
26 changes: 24 additions & 2 deletions fieldmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,35 @@ func names(arg interface{}) []string {
return []string(names)
} else if v := reflect.Indirect(reflect.ValueOf(arg)); v.Type().Kind() == reflect.Struct {
if names, found := fieldMap.names[v.Type()]; found {
return names
return filterMissing(names, v)
}
return buildNames(v.Type())
return filterMissing(buildNames(v.Type()), v)
}
return []string{}
}

type WillUpdater interface {
WillUpdate() bool
}

func filterMissing(names []string, v reflect.Value) []string {
n := make([]string, 0, len(names))
for _, name := range names {
fv, ok := field(v, name)
if !ok {
continue
}
if i, ok := fv.Interface().(WillUpdater); ok && i.WillUpdate() == false {
continue
}
if fv.Kind() == reflect.Ptr && fv.IsNil() {
continue
}
n = append(n, name)
}
return n
}

// TODO : remove missing fields from names
// func missing()

Expand Down
83 changes: 83 additions & 0 deletions named_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,89 @@ func TestNoTag(t *testing.T) {
}, tc, "struct/notag")
}

type Missing bool

func (m Missing) WillUpdate() bool {
return !bool(m)
}

func TestMissing(t *testing.T) {
type testStructMissing struct {
Foo Missing
Bar *string
Baz string
}
barbar := "barbar"
tt := testStructMissing{
Foo: false,
Bar: &barbar,
Baz: "bazbar",
}
doTest(t, tt, []testCase{
{
src: `INSERT INTO example (::names) VALUES(::values)`,
mySQL: `INSERT INTO example (Bar, Baz, Foo) VALUES(?, ?, ?)`,
pgSQL: `INSERT INTO example (Bar, Baz, Foo) VALUES($1, $2, $3)`,
args: []interface{}{tt.Bar, "bazbar", Missing(false)},
},
{
src: `UPDATE example SET ::name=::value`,
mySQL: `UPDATE example SET Bar=?, Baz=?, Foo=?`,
pgSQL: `UPDATE example SET Bar=$1, Baz=$2, Foo=$3`,
args: []interface{}{tt.Bar, "bazbar", Missing(false)},
},
{
src: `SELECT * FROM foo WHERE foo=:Foo AND bar=:Bar`,
mySQL: `SELECT * FROM foo WHERE foo=? AND bar=?`,
pgSQL: `SELECT * FROM foo WHERE foo=$1 AND bar=$2`,
args: []interface{}{Missing(false), tt.Bar},
},
}, "struct/missing/none")
tt.Foo = true
doTest(t, tt, []testCase{
{
src: `INSERT INTO example (::names) VALUES(::values)`,
mySQL: `INSERT INTO example (Bar, Baz) VALUES(?, ?)`,
pgSQL: `INSERT INTO example (Bar, Baz) VALUES($1, $2)`,
args: []interface{}{tt.Bar, "bazbar"},
},
{
src: `UPDATE example SET ::name=::value`,
mySQL: `UPDATE example SET Bar=?, Baz=?`,
pgSQL: `UPDATE example SET Bar=$1, Baz=$2`,
args: []interface{}{tt.Bar, "bazbar"},
},
{
src: `SELECT * FROM foo WHERE foo=:Foo AND bar=:Bar`,
mySQL: `SELECT * FROM foo WHERE foo=? AND bar=?`,
pgSQL: `SELECT * FROM foo WHERE foo=$1 AND bar=$2`,
args: []interface{}{Missing(true), tt.Bar},
},
}, "struct/missing/struct")
tt.Foo = false
tt.Bar = nil
doTest(t, tt, []testCase{
{
src: `INSERT INTO example (::names) VALUES(::values)`,
mySQL: `INSERT INTO example (Baz, Foo) VALUES(?, ?)`,
pgSQL: `INSERT INTO example (Baz, Foo) VALUES($1, $2)`,
args: []interface{}{"bazbar", Missing(false)},
},
{
src: `UPDATE example SET ::name=::value`,
mySQL: `UPDATE example SET Baz=?, Foo=?`,
pgSQL: `UPDATE example SET Baz=$1, Foo=$2`,
args: []interface{}{"bazbar", Missing(false)},
},
{
src: `SELECT * FROM foo WHERE foo=:Foo AND bar=:Bar`,
mySQL: `SELECT * FROM foo WHERE foo=? AND bar=?`,
pgSQL: `SELECT * FROM foo WHERE foo=$1 AND bar=$2`,
args: []interface{}{Missing(false), tt.Bar},
},
}, "struct/missing/ptr")
}

func TestErrors(t *testing.T) {
_, _, err := Named("{var}", map[string]interface{}{}, Variables("var"))
if err == nil {
Expand Down
4 changes: 2 additions & 2 deletions sqlbind.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@
//
// JSON and missing fields
//
// [Not yet implemented]
//
// In a REST API, PATCH update calls may update only certain fields. When using structs with plain types, it is impossible to differentiate between empty fields {"name":""}, null fields : {"name": null} and missing fields : {}.
//
// Using pointers, one can differentiate between empty fields and null/missing fields, but not between null and missing fields. In this case, nil values are usually considered missing.
Expand All @@ -66,6 +64,8 @@
//
// * jsontypes.ROString will never be expanded
//
// More generally, all struct that implement `WillUpdate() bool` will be managed by sqlbind.
//
// Result struct binding
//
// type Example struct {
Expand Down

0 comments on commit 4abbe79

Please sign in to comment.