From f3b13bf31bd89332e1f524fcd101a97d6d8310f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Matczuk?= Date: Tue, 25 Jul 2017 12:25:59 +0200 Subject: [PATCH] queryx --- example_test.go | 34 ++++++++- named_test.go | 60 --------------- named.go => queryx.go | 75 +++++++++++++++++++ queryx_test.go | 165 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 271 insertions(+), 63 deletions(-) delete mode 100644 named_test.go rename named.go => queryx.go (59%) create mode 100644 queryx_test.go diff --git a/example_test.go b/example_test.go index eaaa649..b8738e5 100644 --- a/example_test.go +++ b/example_test.go @@ -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) + } } diff --git a/named_test.go b/named_test.go deleted file mode 100644 index 0494ff1..0000000 --- a/named_test.go +++ /dev/null @@ -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) - } - } - } - } -} diff --git a/named.go b/queryx.go similarity index 59% rename from named.go rename to queryx.go index d2b3c75..9587144 100644 --- a/named.go +++ b/queryx.go @@ -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 +} diff --git a/queryx_test.go b/queryx_test.go new file mode 100644 index 0000000..f0be41c --- /dev/null +++ b/queryx_test.go @@ -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) + } +}