queryx
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
165
queryx_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user