queryx
This commit is contained in:
@@ -69,9 +69,6 @@ func TestExample(t *testing.T) {
|
|||||||
q.Release()
|
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{})
|
// Query the database, storing results in a []Person (wrapped in []interface{})
|
||||||
{
|
{
|
||||||
people := []Person{}
|
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:"United States", City:"New York", TelCode:1}
|
||||||
// gocqlx_test.Place{Country:"Singapore", City:"", TelCode:65}
|
// 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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/gocql/gocql"
|
||||||
|
"github.com/jmoiron/sqlx/reflectx"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Allow digits and letters in bind params; additionally runes are
|
// 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
|
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