Skip to content

Commit

Permalink
change NamedQuery and NamedExec from using maps to structs, and retai…
Browse files Browse the repository at this point in the history
…n old map based interface on NamedQueryMap and NamedExecMap. It would be trivial to write one function to do both, but it would be equally trivial for other people to do this if they require it. I'd rather not build an interface that layers reflection upon reflection for small conveniences
  • Loading branch information
jmoiron committed Jun 9, 2013
1 parent b3d958b commit dce7d8f
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 18 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,20 @@ func main() {

// Named queries, using `:name` as the bindvar. Automatic bindvar support
// which takes into account the dbtype based on the driverName on sqlx.Open/Connect
_, err = db.NamedExec(`INSERT INTO person (first_name,last_name,email) VALUES (:first,:last,:email)`,
_, err = db.NamedExecMap(`INSERT INTO person (first_name,last_name,email) VALUES (:first,:last,:email)`,
map[string]interface{}{
"first": "Bin",
"last": "Smuth",
"email": "bensmith@allblacks.nz",
})

// Selects Mr. Smith from the database
rows, err := db.NamedQuery(`SELECT * FROM person WHERE first_name=:fn`, map[string]interface{}{"fn": "Bin"})
rows, err := db.NamedQueryMap(`SELECT * FROM person WHERE first_name=:fn`, map[string]interface{}{"fn": "Bin"})

// Named queries can also use structs. Their bind names follow the same rules
// as the name -> db mapping, so struct fields are lowercased and the `db` tag
// is taken into consideration.
rows, err := db.NamedQuery(`SELECT * FROM person WHERE first_name=:first_name`, jason)

}

Expand Down
31 changes: 30 additions & 1 deletion bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sqlx
import (
"bytes"
"errors"
"reflect"
"strconv"
"unicode"
)
Expand Down Expand Up @@ -51,6 +52,34 @@ func Rebind(bindType int, query string) string {
return string(rqb)
}

// Bind a named parameter query with fields from a struct argument
// Use of reflect here makes this
func BindStruct(bindType int, query string, arg interface{}) (string, []interface{}, error) {
arglist := make([]interface{}, 0, 5)
t, err := BaseStructType(reflect.TypeOf(arg))
if err != nil {
return "", arglist, err
}

// resolve this type into a map of fields to field positions
fm, err := getFieldmap(t)
if err != nil {
return "", arglist, err
}

argmap := map[string]interface{}{}

v := reflect.ValueOf(arg)
for v = reflect.ValueOf(arg); v.Kind() == reflect.Ptr; {
v = v.Elem()
}
for key, val := range fm {
argmap[key] = v.Field(val).Interface()
}

return BindMap(bindType, query, argmap)
}

// Bind a named parameter query with a map of arguments to a regular positional
// bindvar query and return arguments for the new query in a slice.
func BindMap(bindType int, query string, args map[string]interface{}) (string, []interface{}, error) {
Expand Down Expand Up @@ -78,7 +107,7 @@ func BindMap(bindType int, query string, args map[string]interface{}) (string, [
}
inName = true
name = []byte{}
} else if inName && unicode.IsLetter(rune(b)) && i != last {
} else if inName && (unicode.IsLetter(rune(b)) || b == '_') && i != last {
// append the rune to the name if we are in a name and not on the last rune
name = append(name, b)
} else if inName {
Expand Down
9 changes: 8 additions & 1 deletion doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,15 @@
// The addition of a set of "Select" functions, which combine Query and
// StructScan and have "f" and "v" error handling variantes like Exec.
//
// The addition of a "Get" function, which is to "QueryRow" what "Select" is
// to "Query", and will return a special Row that can StructScan.
//
// The addition of Named Queries, accessible via either struct arguments or
// via map arguments
//
// A "LoadFile" convenience function which executes the queries in a file.
//
// Panicing variants of Connect and Begin: MustConnect, MustBegin.
// A "Connect" function, which combines "Open" and "Ping", and panicing variants
// of Connect and Begin: MustConnect, MustBegin.
//
package sqlx
78 changes: 66 additions & 12 deletions sqlx.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,17 @@ type Execer interface {
Exec(query string, args ...interface{}) (sql.Result, error)
}

// An interface for something which can bind queries (Tx, DB)
type Binder interface {
DriverName() string
Rebind(string) string
BindMap(string, map[string]interface{}) (string, []interface{}, error)
BindStruct(string, interface{}) (string, []interface{}, error)
}

// A union interface which can bind, query, and exec (Tx, DB)
type Ext interface {
Binder
Queryer
Execer
}
Expand Down Expand Up @@ -119,14 +126,30 @@ func (db *DB) BindMap(query string, argmap map[string]interface{}) (string, []in
return BindMap(BindType(db.driverName), query, argmap)
}

// Binds a named query to a new query using positional bindvars and a slice
// of args corresponding to those positions.
func (db *DB) BindStruct(query string, arg interface{}) (string, []interface{}, error) {
return BindStruct(BindType(db.driverName), query, arg)
}

// Issue a named query using this DB.
func (db *DB) NamedQuery(query string, argmap map[string]interface{}) (*Rows, error) {
return NamedQuery(db, query, argmap)
func (db *DB) NamedQueryMap(query string, argmap map[string]interface{}) (*Rows, error) {
return NamedQueryMap(db, query, argmap)
}

// Exec a named query using this DB.
func (db *DB) NamedExec(query string, argmap map[string]interface{}) (sql.Result, error) {
return NamedExec(db, query, argmap)
func (db *DB) NamedExecMap(query string, argmap map[string]interface{}) (sql.Result, error) {
return NamedExecMap(db, query, argmap)
}

// Issue a named query using this DB.
func (db *DB) NamedQuery(query string, arg interface{}) (*Rows, error) {
return NamedQuery(db, query, arg)
}

// Exec a named query using this DB.
func (db *DB) NamedExec(query string, arg interface{}) (sql.Result, error) {
return NamedExec(db, query, arg)
}

// Call Select using this db to issue the query.
Expand Down Expand Up @@ -222,6 +245,12 @@ func (tx *Tx) BindMap(query string, argmap map[string]interface{}) (string, []in
return BindMap(BindType(tx.driverName), query, argmap)
}

// Binds a named query to a new query using positional bindvars and a slice
// of args corresponding to those positions.
func (tx *Tx) BindStruct(query string, arg interface{}) (string, []interface{}, error) {
return BindStruct(BindType(tx.driverName), query, arg)
}

// Issue a named query using thi stransaction.
func (tx *Tx) NamedQuery(query string, argmap map[string]interface{}) (*Rows, error) {
return NamedQuery(tx, query, argmap)
Expand Down Expand Up @@ -616,6 +645,10 @@ func BaseStructType(t reflect.Type) (reflect.Type, error) {
// Create a fieldmap for a given type and return its fieldmap (or error)
func getFieldmap(t reflect.Type) (fm fieldmap, err error) {
// if we have a fieldmap cached, return it
t, err = BaseStructType(t)
if err != nil {
return nil, err
}
fm, ok := fieldmapCache[t]
if ok {
return fm, nil
Expand All @@ -625,6 +658,7 @@ func getFieldmap(t reflect.Type) (fm fieldmap, err error) {

var f reflect.StructField
var name string

for i := 0; i < t.NumField(); i++ {
f = t.Field(i)
name = strings.ToLower(f.Name)
Expand Down Expand Up @@ -764,22 +798,42 @@ func StructScan(rows *sql.Rows, dest interface{}) error {
return nil
}

// Issue a named query using the struct BindStruct to get a query executable
// by the driver and then run Queryx on the result. May return an error
// from the binding or from the execution itself. Usable on DB and Tx.
func NamedQuery(e Ext, query string, arg interface{}) (*Rows, error) {
q, args, err := e.BindStruct(query, arg)
if err != nil {
return nil, err
}
return e.Queryx(q, args...)
}

// Like NamedQuery, but use Exec instead of Queryx.
func NamedExec(e Ext, query string, arg interface{}) (sql.Result, error) {
q, args, err := e.BindStruct(query, arg)
if err != nil {
return nil, err
}
return e.Exec(q, args...)
}

// Issue a named query. Runs BindMap to get a query executable by the driver
// and then runs Queryx on the result. May return an error from the binding
// or from the query execution itself. Usable on any `Binder`, which are sqlx.DB,
// sqlx.Tx.
func NamedQuery(b Binder, query string, argmap map[string]interface{}) (*Rows, error) {
q, args, err := b.BindMap(query, argmap)
// or from the query execution itself. Usable on DB and Tx.
func NamedQueryMap(e Ext, query string, argmap map[string]interface{}) (*Rows, error) {
q, args, err := e.BindMap(query, argmap)
if err != nil {
return nil, err
}
return b.Queryx(q, args...)
return e.Queryx(q, args...)
}

func NamedExec(b Binder, query string, argmap map[string]interface{}) (sql.Result, error) {
q, args, err := b.BindMap(query, argmap)
// Like NamedQuery, but use Exec instead of Queryx.
func NamedExecMap(e Ext, query string, argmap map[string]interface{}) (sql.Result, error) {
q, args, err := e.BindMap(query, argmap)
if err != nil {
return nil, err
}
return b.Exec(q, args...)
return e.Exec(q, args...)
}
83 changes: 81 additions & 2 deletions sqlx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ func TestUsage(t *testing.T) {
}

// test advanced querying
_, err = db.NamedExec("INSERT INTO person (first_name, last_name, email) VALUES (:first, :last, :email)", map[string]interface{}{
_, err = db.NamedExecMap("INSERT INTO person (first_name, last_name, email) VALUES (:first, :last, :email)", map[string]interface{}{
"first": "Bin",
"last": "Smuth",
"email": "bensmith@allblacks.nz",
Expand All @@ -295,7 +295,7 @@ func TestUsage(t *testing.T) {
}

// ensure that if the named param happens right at the end it still works
rows, err = db.NamedQuery("SELECT * FROM person WHERE first_name=:first", map[string]interface{}{"first": "Bin"})
rows, err = db.NamedQueryMap("SELECT * FROM person WHERE first_name=:first", map[string]interface{}{"first": "Bin"})
if err != nil {
t.Fatal(err)
}
Expand All @@ -314,6 +314,34 @@ func TestUsage(t *testing.T) {
}
}

ben.FirstName = "Ben"
ben.LastName = "Smith"

// Insert via a named query using the struct
_, err = db.NamedExec("INSERT INTO person (first_name, last_name, email) VALUES (:first_name, :last_name, :email)", ben)

if err != nil {
t.Fatal(err)
}

rows, err = db.NamedQuery("SELECT * FROM person WHERE first_name=:first_name", ben)
if err != nil {
t.Fatal(err)
}
for rows.Next() {
err = rows.StructScan(ben)
if err != nil {
t.Fatal(err)
}
if ben.FirstName != "Ben" {
t.Fatal("Expected first name of `Ben`, got " + ben.FirstName)
}
if ben.LastName != "Smith" {
t.Fatal("Expected first name of `Smith`, got " + ben.LastName)
}

}

}

if TestPostgres {
Expand Down Expand Up @@ -376,6 +404,57 @@ func TestBindMap(t *testing.T) {
}
}

func TestBindStruct(t *testing.T) {
q1 := `INSERT INTO foo (a, b, c, d) VALUES (:name, :age, :first, :last)`
type tt struct {
Name string
Age int
First string
Last string
}
am := tt{"Jason Moiron", 30, "Jason", "Moiron"}

bq, args, _ := BindStruct(QUESTION, q1, am)
expect := `INSERT INTO foo (a, b, c, d) VALUES (?, ?, ?, ?)`
if bq != expect {
t.Errorf("Interpolation of query failed: got `%v`, expected `%v`\n", bq, expect)
}

if args[0].(string) != "Jason Moiron" {
t.Errorf("Expected `Jason Moiron`, got %v\n", args[0])
}

if args[1].(int) != 30 {
t.Errorf("Expected 30, got %v\n", args[1])
}

if args[2].(string) != "Jason" {
t.Errorf("Expected Jason, got %v\n", args[2])
}

if args[3].(string) != "Moiron" {
t.Errorf("Expected Moiron, got %v\n", args[3])
}

}

func BenchmarkBindStruct(b *testing.B) {
b.StopTimer()
q1 := `INSERT INTO foo (a, b, c, d) VALUES (:name, :age, :first, :last)`
type t struct {
Name string
Age int
First string
Last string
}
am := t{"Jason Moiron", 30, "Jason", "Moiron"}
b.StartTimer()
for i := 0; i < b.N; i++ {
BindStruct(DOLLAR, q1, am)
//bindMap(QUESTION, q1, am)
}
}

func BenchmarkBindMap(b *testing.B) {
b.StopTimer()
q1 := `INSERT INTO foo (a, b, c, d) VALUES (:name, :age, :first, :last)`
Expand Down

0 comments on commit dce7d8f

Please sign in to comment.