Skip to content

Commit

Permalink
addition of rebind and bindMap functions to be exposed by some other …
Browse files Browse the repository at this point in the history
…interface at a later date (named parameter WIP)
  • Loading branch information
jmoiron committed Jun 7, 2013
1 parent fb6eb3c commit f6fbcb1
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 4 deletions.
109 changes: 109 additions & 0 deletions bind.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package sqlx

import (
"bytes"
"errors"
"strconv"
"unicode"
)

const (
UNKNOWN = iota
QUESTION
DOLLAR
)

func bindType(driverName string) int {
switch driverName {
case "postgres":
return DOLLAR
case "mysql":
return QUESTION
case "sqlite":
return QUESTION
}
return UNKNOWN
}

func rebind(bindType int, query string) string {
if bindType == QUESTION {
return query
}

qb := []byte(query)
// Add space enough for 10 params before we have to allocate
rqb := make([]byte, 0, len(qb)+10)
j := 1
for _, b := range qb {
if b == '?' {
rqb = append(rqb, '$')
for _, b := range strconv.Itoa(j) {
rqb = append(rqb, byte(b))
}
j++
} else {
rqb = append(rqb, b)
}
}

return string(rqb)
}

// Bind named parameters to a query string and list of arguments, in order.
// If the
func bindMap(query string, args map[string]interface{}) (string, []interface{}, error) {
arglist := make([]interface{}, 0, 5)
// In all likelihood, the rebound query will be shorter
qb := []byte(query)
rebound := make([]byte, 0, len(qb))

var name []byte
inName := false

for i, b := range qb {
if b == ':' {
if inName {
err := errors.New("Unexpected `:` while reading named param at " + strconv.Itoa(i))
return "", arglist, err
}
inName = true
name = []byte{}
} else if inName && unicode.IsLetter(rune(b)) {
name = append(name, b)
} else if inName {
inName = false
sname := string(name)
val, ok := args[sname]
if !ok {
err := errors.New("Could not find name `" + sname + "` in args")
return "", arglist, err
}
arglist = append(arglist, val)
rebound = append(rebound, '?')
rebound = append(rebound, b)
} else {
rebound = append(rebound, b)
}
}
return string(rebound), arglist, nil
}

func rebindBuff(bindType int, query string) string {
if bindType == QUESTION {
return query
}

b := make([]byte, 0, len(query))
rqb := bytes.NewBuffer(b)
j := 1
for _, r := range query {
if r == '?' {
rqb.WriteString("$" + strconv.Itoa(j))
j++
} else {
rqb.WriteRune(r)
}
}

return rqb.String()
}
41 changes: 37 additions & 4 deletions sqlx.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ type Execer interface {
Exec(query string, args ...interface{}) (sql.Result, error)
}

type dbext interface {
Queryer
Execer
DriverName() string
}

// An interface for something which can Prepare sql statements (Tx, DB)
type Preparer interface {
Prepare(query string) (*sql.Stmt, error)
Expand Down Expand Up @@ -80,12 +86,19 @@ func (r *Row) Columns() ([]string, error) {
}

// An sqlx wrapper around database/sql's DB with extra functionality
type DB struct{ sql.DB }
type DB struct {
sql.DB
driverName string
}

func (db *DB) DriverName() string {
return db.driverName
}

// Same as database/sql's Open, but returns an *sqlx.DB instead.
func Open(driverName, dataSourceName string) (*DB, error) {
db, err := sql.Open(driverName, dataSourceName)
return &DB{*db}, err
return &DB{*db, driverName}, err
}

// Call Select using this db to issue the query.
Expand Down Expand Up @@ -116,7 +129,7 @@ func (db *DB) MustBegin() *Tx {
// Beginx is the same as Begin, but returns an *sqlx.Tx instead of an *sql.Tx
func (db *DB) Beginx() (*Tx, error) {
tx, err := db.DB.Begin()
return &Tx{*tx}, err
return &Tx{*tx, db.driverName}, err
}

// Queryx is the same as Query, but returns an *sqlx.Rows instead of *sql.Rows
Expand Down Expand Up @@ -162,7 +175,14 @@ func (db *DB) Preparex(query string) (*Stmt, error) {
}

// An sqlx wrapper around database/sql's Tx with extra functionality
type Tx struct{ sql.Tx }
type Tx struct {
sql.Tx
driverName string
}

func (tx *Tx) DriverName() string {
return tx.driverName
}

// Call LoadFile using this transaction to issue the Exec.
func (tx *Tx) LoadFile(path string) (*sql.Result, error) {
Expand Down Expand Up @@ -691,3 +711,16 @@ func StructScan(rows *sql.Rows, dest interface{}) error {

return nil
}

/*
func compileNamedQuery(driverName, query string, args map[string]interface{}) (string, []interface{}, error) {
return "", []interface{}{}, nil
}
// Issue a NamedQuery against a queryer.
func NamedQuery(db *dbext, query string, args map[string]interface{}) (*sql.Rows, error) {
qs, a, err := compileNamedQuery(query, args)
return nil, nil
}
*/
73 changes: 73 additions & 0 deletions sqlx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,3 +307,76 @@ func TestUsage(t *testing.T) {
RunTest(mysqldb, t, "mysql")
}
}

func TestRebind(t *testing.T) {
q1 := `INSERT INTO foo (a, b, c, d, e, f, g, h, i) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
q2 := `INSERT INTO foo (a, b, c) VALUES (?, ?, "foo"), ("Hi", ?, ?)`

s1 := rebind(DOLLAR, q1)
s2 := rebind(DOLLAR, q2)

if s1 != `INSERT INTO foo (a, b, c, d, e, f, g, h, i) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)` {
t.Errorf("q1 failed")
}

if s2 != `INSERT INTO foo (a, b, c) VALUES ($1, $2, "foo"), ("Hi", $3, $4)` {
t.Errorf("q2 failed")
}
}

func TestBindMap(t *testing.T) {
// Test that it works..
q1 := `INSERT INTO foo (a, b, c, d) VALUES (:name, :age, :first, :last)`
am := map[string]interface{}{
"name": "Jason Moiron",
"age": 30,
"first": "Jason",
"last": "Moiron",
}

bq, args, _ := bindMap(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 BenchmarkRebind(b *testing.B) {
b.StopTimer()
q1 := `INSERT INTO foo (a, b, c, d, e, f, g, h, i) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
q2 := `INSERT INTO foo (a, b, c) VALUES (?, ?, "foo"), ("Hi", ?, ?)`
b.StartTimer()

for i := 0; i < b.N; i++ {
rebind(DOLLAR, q1)
rebind(DOLLAR, q2)
}
}

func BenchmarkRebindBuffer(b *testing.B) {
b.StopTimer()
q1 := `INSERT INTO foo (a, b, c, d, e, f, g, h, i) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
q2 := `INSERT INTO foo (a, b, c) VALUES (?, ?, "foo"), ("Hi", ?, ?)`
b.StartTimer()

for i := 0; i < b.N; i++ {
rebindBuff(DOLLAR, q1)
rebindBuff(DOLLAR, q2)
}
}

0 comments on commit f6fbcb1

Please sign in to comment.