Skip to content

Commit

Permalink
add NamedStmt support, lots more testing around it... still no Map su…
Browse files Browse the repository at this point in the history
…pport
  • Loading branch information
jmoiron committed Feb 20, 2014
1 parent 4f88613 commit 8e788cf
Show file tree
Hide file tree
Showing 3 changed files with 339 additions and 19 deletions.
188 changes: 175 additions & 13 deletions named.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ package sqlx
// * compileNamedQuery - rebind a named query, returning a query and list of names
//
import (
"database/sql"
"errors"
"fmt"
"log"
"reflect"
"strconv"
"unicode"
Expand All @@ -22,32 +24,175 @@ import (
// NamedStmt is a prepared statement that executes named queries. Prepare it
// how you would execute a NamedQuery, but pass in a struct (or map for the map
// variants) when you go to execute.
type namedStmt struct {
Params []string
Query string
Stmt *Stmt
type NamedStmt struct {
Params []string
QueryString string
Stmt *Stmt
}

// BindStruct binds a named parameter query with fields from a struct argument.
// The rules for binding field names to parameter names follow the same
// conventions as for StructScan, including obeying the `db` struct tags.
func BindStruct(bindType int, query string, arg interface{}) (string, []interface{}, error) {
bound, names, err := compileNamedQuery([]byte(query), bindType)
// Close closes the named statement.
func (n *NamedStmt) Close() error {
return n.Stmt.Close()
}

// Exec executes a named statement using the struct passed.
func (n *NamedStmt) Exec(arg interface{}) (sql.Result, error) {
args, err := bindStruct(n.Params, arg)
if err != nil {
return "", []interface{}{}, err
return *new(sql.Result), err
}
return n.Stmt.Exec(args...)
}

// Query executes a named statement using the struct argument, returning rows.
func (n *NamedStmt) Query(arg interface{}) (*sql.Rows, error) {
args, err := bindStruct(n.Params, arg)
if err != nil {
return nil, err
}
return n.Stmt.Query(args...)
}

// QueryRow executes a named statement against the database. Because sqlx cannot
// create a *sql.Row with an error condition pre-set for binding errors, sqlx
// returns a *sqlx.Row instead.
func (n *NamedStmt) QueryRow(arg interface{}) *Row {
args, err := bindStruct(n.Params, arg)
if err != nil {
return &Row{err: err}
}
return n.Stmt.QueryRowx(args...)
}

// Execv execs a NamedStmt with the given arg, printing errors and returning them
func (n *NamedStmt) Execv(arg interface{}) (sql.Result, error) {
res, err := n.Exec(arg)
if err != nil {
log.Println(n.QueryString, res, err)
}
return res, err
}

// Execl execs a NamedStmt with the given arg, logging errors
func (n *NamedStmt) Execl(arg interface{}) sql.Result {
res, err := n.Exec(arg)
if err != nil {
log.Println(n.QueryString, res, err)
}
return res
}

// Execf execs a NamedStmt, using log.fatal to print out errors
func (n *NamedStmt) Execf(arg interface{}) sql.Result {
res, err := n.Exec(arg)
if err != nil {
log.Fatal(n.QueryString, res, err)
}
return res
}

// Execp execs a NamedStmt, panicing on error
func (n *NamedStmt) Execp(arg interface{}) sql.Result {
return n.MustExec(arg)
}

// MustExec execs a NamedStmt, panicing on error
func (n *NamedStmt) MustExec(arg interface{}) sql.Result {
res, err := n.Exec(arg)
if err != nil {
panic(err)
}
return res
}

// Queryx using this NamedStmt
func (n *NamedStmt) Queryx(arg interface{}) (*Rows, error) {
r, err := n.Query(arg)
if err != nil {
return nil, err
}
return &Rows{Rows: *r}, err
}

// QueryRowx this NamedStmt. Because of limitations with QueryRow, this is
// an alias for QueryRow.
func (n *NamedStmt) QueryRowx(arg interface{}) *Row {
return n.QueryRow(arg)
}

// Select using this NamedStmt
func (n *NamedStmt) Select(dest interface{}, arg interface{}) error {
rows, err := n.Query(arg)
if err != nil {
return err
}
// if something happens here, we want to make sure the rows are Closed
defer rows.Close()
return StructScan(rows, dest)
}

// Selectv using this NamedStmt
func (n *NamedStmt) Selectv(dest interface{}, arg interface{}) error {
err := n.Select(dest, arg)
if err != nil {
log.Println(n.QueryString, err)
}
return err
}

// Selectf using this NamedStmt
func (n *NamedStmt) Selectf(dest interface{}, arg interface{}) {
err := n.Select(dest, arg)
if err != nil {
log.Fatal(n.QueryString, err)
}
}

// Get using this NamedStmt
func (n *NamedStmt) Get(dest interface{}, arg interface{}) error {
r := n.QueryRowx(arg)
return r.StructScan(dest)
}

// A union interface of preparer and binder, required to be able to prepare
// named statements (as the bindtype must be determined).
type namedPreparer interface {
Preparer
Binder
}

func prepareNamed(p namedPreparer, query string) (*NamedStmt, error) {
bindType := BindType(p.DriverName())
q, args, err := compileNamedQuery([]byte(query), bindType)
if err != nil {
return nil, err
}
stmt, err := Preparex(p, q)
if err != nil {
return nil, err
}
return &NamedStmt{
QueryString: q,
Params: args,
Stmt: stmt,
}, nil
}

// private interface to generate a list of interfaces from a given struct
// type, given a list of names to pull out of the struct. Used by public
// BindStruct interface.
func bindStruct(names []string, arg interface{}) ([]interface{}, error) {
arglist := make([]interface{}, 0, len(names))

t, err := BaseStructType(reflect.TypeOf(arg))
if err != nil {
return "", arglist, err
return arglist, err
}

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

// grab the indirected value of arg
Expand All @@ -61,11 +206,28 @@ func BindStruct(bindType int, query string, arg interface{}) (string, []interfac
for _, name := range names {
val, ok := fm[name]
if !ok {
return "", arglist, fmt.Errorf("could not find name %s in %v", name, arg)
return arglist, fmt.Errorf("could not find name %s in %v", name, arg)
}
arglist = append(arglist, values[val])
}

return arglist, nil
}

// BindStruct binds a named parameter query with fields from a struct argument.
// The rules for binding field names to parameter names follow the same
// conventions as for StructScan, including obeying the `db` struct tags.
func BindStruct(bindType int, query string, arg interface{}) (string, []interface{}, error) {
bound, names, err := compileNamedQuery([]byte(query), bindType)
if err != nil {
return "", []interface{}{}, err
}

arglist, err := bindStruct(names, arg)
if err != nil {
return "", []interface{}{}, err
}

return bound, arglist, nil
}

Expand Down
150 changes: 149 additions & 1 deletion named_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package sqlx

import "testing"
import (
"database/sql"
"testing"
)

func TestCompileQuery(t *testing.T) {
table := []struct {
Expand Down Expand Up @@ -56,3 +59,148 @@ func TestCompileQuery(t *testing.T) {
}
}
}

type Test struct {
t *testing.T
}

func (t Test) Error(err error, msg ...interface{}) {
if err != nil {
if len(msg) == 0 {
t.t.Error(err)
} else {
t.t.Error(msg...)
}
}
}

func (t Test) Errorf(err error, format string, args ...interface{}) {
if err != nil {
t.t.Errorf(format, args...)
}
}

func TestNamedQueries(t *testing.T) {
RunWithSchema(defaultSchema, t, func(db *DB, t *testing.T) {
loadDefaultFixture(db, t)
test := Test{t}
var ns *NamedStmt
var err error

// Check that invalid preparations fail
ns, err = db.PrepareNamed("SELECT * FROM person WHERE first_name=:first:name")
if err == nil {
t.Error("Expected an error with invalid prepared statement.")
}

ns, err = db.PrepareNamed("invalid sql")
if err == nil {
t.Error("Expected an error with invalid prepared statement.")
}

// Check closing works as anticipated
ns, err = db.PrepareNamed("SELECT * FROM person WHERE first_name=:first_name")
test.Error(err)
err = ns.Close()
test.Error(err)

ns, err = db.PrepareNamed(`
SELECT first_name, last_name, email
FROM person WHERE first_name=:first_name AND email=:email`)
test.Error(err)

// test Queryx w/ uses Query
p := Person{FirstName: "Jason", LastName: "Moiron", Email: "[email protected]"}

rows, err := ns.Queryx(p)
test.Error(err)
for rows.Next() {
var p2 Person
rows.StructScan(&p2)
if p.FirstName != p2.FirstName {
t.Error("got %s, expected %s", p.FirstName, p2.FirstName)
}
if p.LastName != p2.LastName {
t.Error("got %s, expected %s", p.LastName, p2.LastName)
}
if p.Email != p2.Email {
t.Error("got %s, expected %s", p.Email, p2.Email)
}
}

// test Select
people := make([]Person, 0, 5)
err = ns.Select(&people, p)
test.Error(err)

if len(people) != 1 {
t.Errorf("got %d results, expected %d", len(people), 1)
}
if p.FirstName != people[0].FirstName {
t.Error("got %s, expected %s", p.FirstName, people[0].FirstName)
}
if p.LastName != people[0].LastName {
t.Error("got %s, expected %s", p.LastName, people[0].LastName)
}
if p.Email != people[0].Email {
t.Error("got %s, expected %s", p.Email, people[0].Email)
}

// test Exec
ns, err = db.PrepareNamed(`
INSERT INTO person (first_name, last_name, email)
VALUES (:first_name, :last_name, :email)`)
test.Error(err)

js := Person{
FirstName: "Julien",
LastName: "Savea",
Email: "[email protected]",
}
_, err = ns.Exec(js)
test.Error(err)

// Make sure we can pull him out again
p2 := Person{}
db.Get(&p2, db.Rebind("SELECT * FROM person WHERE email=?"), js.Email)
if p2.Email != js.Email {
t.Errorf("expected %s, got %s", js.Email, p2.Email)
}

// test Txn NamedStmts
tx := db.MustBegin()
txns := tx.NamedStmt(ns)

// We're going to add Steven in this txn
sl := Person{
FirstName: "Steven",
LastName: "Luatua",
Email: "[email protected]",
}

_, err = txns.Exec(sl)
test.Error(err)
// then rollback...
tx.Rollback()
// looking for Steven after a rollback should fail
err = db.Get(&p2, db.Rebind("SELECT * FROM person WHERE email=?"), sl.Email)
if err != sql.ErrNoRows {
t.Errorf("expected no rows error, got %v", err)
}

// now do the same, but commit
tx = db.MustBegin()
txns = tx.NamedStmt(ns)
_, err = txns.Exec(sl)
test.Error(err)
tx.Commit()

// looking for Steven after a Commit should succeed
err = db.Get(&p2, db.Rebind("SELECT * FROM person WHERE email=?"), sl.Email)
test.Error(err)
if p2.Email != sl.Email {
t.Errorf("expected %s, got %s", sl.Email, p2.Email)
}

})
}
Loading

0 comments on commit 8e788cf

Please sign in to comment.