This commit is contained in:
Michał Matczuk
2017-07-25 12:25:59 +02:00
parent 37524a4485
commit f3b13bf31b
4 changed files with 271 additions and 63 deletions

View File

@@ -69,9 +69,6 @@ func TestExample(t *testing.T) {
q.Release()
}
// TODO
// tx.NamedExec("INSERT INTO person (first_name, last_name, email) VALUES (:first_name, :last_name, :email)", &Person{"Jane", "Citizen", "jane.citzen@gocqlx_test.com"})
// Query the database, storing results in a []Person (wrapped in []interface{})
{
people := []Person{}
@@ -109,4 +106,35 @@ func TestExample(t *testing.T) {
// gocqlx_test.Place{Country:"United States", City:"New York", TelCode:1}
// gocqlx_test.Place{Country:"Singapore", City:"", TelCode:65}
}
// Named queries, using `:name` as the bindvar
{
stmt, names, err := gocqlx.CompileNamedQuery([]byte("INSERT INTO person (first_name, last_name, email) VALUES (:first_name, :last_name, :email)"))
if err != nil {
t.Fatal("compile:", err)
}
q := gocqlx.Queryx{
Query: session.Query(stmt),
Names: names,
}
if err := q.BindStruct(&Person{
"Jane",
"Citizen",
[]string{"jane.citzen@gocqlx_test.com"},
}); err != nil {
t.Fatal("bind:", err)
}
mustExec(q.Query)
if err := q.BindMap(map[string]interface{}{
"first_name": "Bin",
"last_name": "Smuth",
"email": []string{"bensmith@allblacks.nz"},
}); err != nil {
t.Fatal("bind:", err)
}
mustExec(q.Query)
}
}

View File

@@ -1,60 +0,0 @@
package gocqlx
import "testing"
func TestCompileQuery(t *testing.T) {
table := []struct {
Q, R string
V []string
}{
// basic test for named parameters, invalid char ',' terminating
{
Q: `INSERT INTO foo (a,b,c,d) VALUES (:name, :age, :first, :last)`,
R: `INSERT INTO foo (a,b,c,d) VALUES (?, ?, ?, ?)`,
V: []string{"name", "age", "first", "last"},
},
// This query tests a named parameter ending the string as well as numbers
{
Q: `SELECT * FROM a WHERE first_name=:name1 AND last_name=:name2`,
R: `SELECT * FROM a WHERE first_name=? AND last_name=?`,
V: []string{"name1", "name2"},
},
{
Q: `SELECT "::foo" FROM a WHERE first_name=:name1 AND last_name=:name2`,
R: `SELECT ":foo" FROM a WHERE first_name=? AND last_name=?`,
V: []string{"name1", "name2"},
},
{
Q: `SELECT 'a::b::c' || first_name, '::::ABC::_::' FROM person WHERE first_name=:first_name AND last_name=:last_name`,
R: `SELECT 'a:b:c' || first_name, '::ABC:_:' FROM person WHERE first_name=? AND last_name=?`,
V: []string{"first_name", "last_name"},
},
/* This unicode awareness test sadly fails, because of our byte-wise worldview.
* We could certainly iterate by Rune instead, though it's a great deal slower,
* it's probably the RightWay(tm)
{
Q: `INSERT INTO foo (a,b,c,d) VALUES (:あ, :b, :キコ, :名前)`,
R: `INSERT INTO foo (a,b,c,d) VALUES (?, ?, ?, ?)`,
},
*/
}
for _, test := range table {
qr, names, err := CompileNamedQuery([]byte(test.Q))
if err != nil {
t.Error(err)
}
if qr != test.R {
t.Errorf("expected %s, got %s", test.R, qr)
}
if len(names) != len(test.V) {
t.Errorf("expected %#v, got %#v", test.V, names)
} else {
for i, name := range names {
if name != test.V[i] {
t.Errorf("expected %dth name to be %s, got %s", i+1, test.V[i], name)
}
}
}
}
}

View File

@@ -3,8 +3,13 @@ package gocqlx
import (
"bytes"
"errors"
"fmt"
"reflect"
"strconv"
"unicode"
"github.com/gocql/gocql"
"github.com/jmoiron/sqlx/reflectx"
)
// Allow digits and letters in bind params; additionally runes are
@@ -72,3 +77,73 @@ func CompileNamedQuery(qs []byte) (stmt string, names []string, err error) {
return string(rebound), names, err
}
// Queryx is a wrapper around gocql.Query which adds struct binding capabilities.
type Queryx struct {
*gocql.Query
Names []string
Mapper *reflectx.Mapper
}
// BindStruct binds query named parameters using mapper.
func (q Queryx) BindStruct(arg interface{}) error {
m := q.Mapper
if m == nil {
m = DefaultMapper
}
arglist, err := bindStructArgs(q.Names, arg, m)
if err != nil {
return err
}
q.Bind(arglist...)
return nil
}
func bindStructArgs(names []string, arg interface{}, m *reflectx.Mapper) ([]interface{}, error) {
arglist := make([]interface{}, 0, len(names))
// grab the indirected value of arg
v := reflect.ValueOf(arg)
for v = reflect.ValueOf(arg); v.Kind() == reflect.Ptr; {
v = v.Elem()
}
fields := m.TraversalsByName(v.Type(), names)
for i, t := range fields {
if len(t) == 0 {
return arglist, fmt.Errorf("could not find name %s in %#v", names[i], arg)
}
val := reflectx.FieldByIndexesReadOnly(v, t)
arglist = append(arglist, val.Interface())
}
return arglist, nil
}
// BindMap binds query named parameters using map.
func (q Queryx) BindMap(arg map[string]interface{}) error {
arglist, err := bindMapArgs(q.Names, arg)
if err != nil {
return err
}
q.Bind(arglist...)
return nil
}
func bindMapArgs(names []string, arg map[string]interface{}) ([]interface{}, error) {
arglist := make([]interface{}, 0, len(names))
for _, name := range names {
val, ok := arg[name]
if !ok {
return arglist, fmt.Errorf("could not find name %s in %#v", name, arg)
}
arglist = append(arglist, val)
}
return arglist, nil
}

165
queryx_test.go Normal file
View File

@@ -0,0 +1,165 @@
package gocqlx
import (
"testing"
"github.com/gocql/gocql"
"github.com/google/go-cmp/cmp"
)
func TestCompileQuery(t *testing.T) {
table := []struct {
Q, R string
V []string
}{
// basic test for named parameters, invalid char ',' terminating
{
Q: `INSERT INTO foo (a,b,c,d) VALUES (:name, :age, :first, :last)`,
R: `INSERT INTO foo (a,b,c,d) VALUES (?, ?, ?, ?)`,
V: []string{"name", "age", "first", "last"},
},
// This query tests a named parameter ending the string as well as numbers
{
Q: `SELECT * FROM a WHERE first_name=:name1 AND last_name=:name2`,
R: `SELECT * FROM a WHERE first_name=? AND last_name=?`,
V: []string{"name1", "name2"},
},
{
Q: `SELECT "::foo" FROM a WHERE first_name=:name1 AND last_name=:name2`,
R: `SELECT ":foo" FROM a WHERE first_name=? AND last_name=?`,
V: []string{"name1", "name2"},
},
{
Q: `SELECT 'a::b::c' || first_name, '::::ABC::_::' FROM person WHERE first_name=:first_name AND last_name=:last_name`,
R: `SELECT 'a:b:c' || first_name, '::ABC:_:' FROM person WHERE first_name=? AND last_name=?`,
V: []string{"first_name", "last_name"},
},
/* This unicode awareness test sadly fails, because of our byte-wise worldview.
* We could certainly iterate by Rune instead, though it's a great deal slower,
* it's probably the RightWay(tm)
{
Q: `INSERT INTO foo (a,b,c,d) VALUES (:あ, :b, :キコ, :名前)`,
R: `INSERT INTO foo (a,b,c,d) VALUES (?, ?, ?, ?)`,
},
*/
}
for _, test := range table {
qr, names, err := CompileNamedQuery([]byte(test.Q))
if err != nil {
t.Error(err)
}
if qr != test.R {
t.Error("expected", test.R, "got", qr)
}
if diff := cmp.Diff(names, test.V); diff != "" {
t.Error("names mismatch", diff)
}
}
}
func BenchmarkCompileNamedQuery(b *testing.B) {
q1 := `INSERT INTO foo (a, b, c, d) VALUES (:name, :age, :first, :last)`
b.ResetTimer()
for i := 0; i < b.N; i++ {
CompileNamedQuery([]byte(q1))
}
}
func TestBindStruct(t *testing.T) {
v := &struct {
Name string
Age int
First string
Last string
}{
Name: "name",
Age: 30,
First: "first",
Last: "last",
}
t.Run("simple", func(t *testing.T) {
names := []string{"name", "age", "first", "last"}
args, err := bindStructArgs(names, v, DefaultMapper)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(args, []interface{}{"name", 30, "first", "last"}); diff != "" {
t.Error("args mismatch", diff)
}
})
t.Run("error", func(t *testing.T) {
names := []string{"name", "first", "not_found"}
_, err := bindStructArgs(names, v, DefaultMapper)
if err == nil {
t.Fatal("unexpected error")
}
})
}
func BenchmarkBindStruct(b *testing.B) {
q := Queryx{
Query: &gocql.Query{},
Names: []string{"name", "age", "first", "last"},
}
type t struct {
Name string
Age int
First string
Last string
}
am := t{"Jason Moiron", 30, "Jason", "Moiron"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
q.BindStruct(am)
}
}
func TestBindMap(t *testing.T) {
v := map[string]interface{}{
"name": "name",
"age": 30,
"first": "first",
"last": "last",
}
t.Run("simple", func(t *testing.T) {
names := []string{"name", "age", "first", "last"}
args, err := bindMapArgs(names, v)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(args, []interface{}{"name", 30, "first", "last"}); diff != "" {
t.Error("args mismatch", diff)
}
})
t.Run("error", func(t *testing.T) {
names := []string{"name", "first", "not_found"}
_, err := bindMapArgs(names, v)
if err == nil {
t.Fatal("unexpected error")
}
})
}
func BenchmarkBindMap(b *testing.B) {
q := Queryx{
Query: &gocql.Query{},
Names: []string{"name", "age", "first", "last"},
}
am := map[string]interface{}{
"name": "Jason Moiron",
"age": 30,
"first": "Jason",
"last": "Moiron",
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
q.BindMap(am)
}
}