Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

queryx: unset empty values #206

Merged
merged 5 commits into from
Nov 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ ifndef GOTEST_CPU
GOTEST_CPU := 1
endif

ifndef GOPATH
GOPATH := $(shell go env GOPATH)
endif

ifndef GOBIN
GOBIN := $(GOPATH)/bin
endif
Expand Down
1 change: 1 addition & 0 deletions benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a ALv2-style
// license that can be found in the LICENSE file.

//go:build all || integration
// +build all integration

package gocqlx_test
Expand Down
1 change: 1 addition & 0 deletions dbutil/rewrite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a ALv2-style
// license that can be found in the LICENSE file.

//go:build all || integration
// +build all integration

package dbutil_test
Expand Down
73 changes: 73 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a ALv2-style
// license that can be found in the LICENSE file.

//go:build all || integration
// +build all integration

package gocqlx_test
Expand All @@ -18,6 +19,7 @@ import (
"github.com/scylladb/gocqlx/v2/qb"
"github.com/scylladb/gocqlx/v2/table"
"golang.org/x/sync/errgroup"
"gopkg.in/inf.v0"
)

// Running examples locally:
Expand Down Expand Up @@ -46,6 +48,7 @@ func TestExample(t *testing.T) {
pagingEfficientFullTableScan(t, session)

lwtLock(t, session)
unsetEmptyValues(t, session)
}

// This example shows how to use query builders and table models to build
Expand Down Expand Up @@ -704,6 +707,76 @@ func lwtLock(t *testing.T, session gocqlx.Session) {
}
}

// This example shows how to reuse the same insert statement with
// partially filled parameters without generating tombstones for empty columns.
func unsetEmptyValues(t *testing.T, session gocqlx.Session) {
err := session.ExecStmt(`CREATE KEYSPACE IF NOT EXISTS examples WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}`)
if err != nil {
t.Fatal("create keyspace:", err)
}

type Operation struct {
ID string
ClientID string
Type string
PaymentID string
Fee *inf.Dec
}
err = session.ExecStmt(`CREATE TABLE IF NOT EXISTS examples.operations (
id text PRIMARY KEY,
client_id text,
type text,
payment_id text,
fee decimal)`)
if err != nil {
t.Fatal("create table:", err)
}

insertOperation := qb.Insert("examples.operations").
Columns("id", "client_id", "type", "payment_id", "fee")

// Insert operation with empty paymentID.
insertQuery := insertOperation.Query(session).
WithBindTransformer(gocqlx.UnsetEmptyTransformer).
BindStruct(Operation{
ID: "1",
ClientID: "42",
Type: "Transfer",
Fee: inf.NewDec(1, 1),
})
if err := insertQuery.ExecRelease(); err != nil {
t.Fatal("ExecRelease() failed:", err)
}

// Set default transformer to avoid setting it for each query.
gocqlx.DefaultBindTransformer = gocqlx.UnsetEmptyTransformer
defer func() {
gocqlx.DefaultBindTransformer = nil
}()

// Insert operation with empty fee.
insertQuery = insertOperation.Query(session).
BindStruct(Operation{
ID: "2",
ClientID: "42",
Type: "Input",
PaymentID: "1",
})
if err := insertQuery.ExecRelease(); err != nil {
t.Fatal("ExecRelease() failed:", err)
}

// Query and displays data.
var ops []*Operation
if err := qb.Select("examples.operations").Query(session).Select(&ops); err != nil {
t.Fatal("Select() failed:", err)
}

for _, op := range ops {
t.Logf("%+v", *op)
}
}

func mustParseUUID(s string) gocql.UUID {
u, err := gocql.ParseUUID(s)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions iterx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a ALv2-style
// license that can be found in the LICENSE file.

//go:build all || integration
// +build all integration

package gocqlx_test
Expand Down
1 change: 1 addition & 0 deletions migrate/migrate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a ALv2-style
// license that can be found in the LICENSE file.

//go:build all || integration
// +build all integration

package migrate_test
Expand Down
42 changes: 30 additions & 12 deletions queryx.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ type Queryx struct {
*gocql.Query
Names []string
Mapper *reflectx.Mapper
err error

tr Transformer
err error
}

// Query creates a new Queryx from gocql.Query using a default mapper.
Expand All @@ -104,13 +106,21 @@ func Query(q *gocql.Query, names []string) *Queryx {
Query: q,
Names: names,
Mapper: DefaultMapper,
tr: DefaultBindTransformer,
}
}

// WithBindTransformer sets the query bind transformer.
// The transformer is called right before binding a value to a named parameter.
func (q *Queryx) WithBindTransformer(tr Transformer) *Queryx {
q.tr = tr
return q
}

// BindStruct binds query named parameters to values from arg using mapper. If
// value cannot be found error is reported.
func (q *Queryx) BindStruct(arg interface{}) *Queryx {
arglist, err := bindStructArgs(q.Names, arg, nil, q.Mapper)
arglist, err := q.bindStructArgs(arg, nil)
if err != nil {
q.err = fmt.Errorf("bind error: %s", err)
} else {
Expand All @@ -125,7 +135,7 @@ func (q *Queryx) BindStruct(arg interface{}) *Queryx {
// using a mapper. If value cannot be found in arg0 it's looked up in arg1
// before reporting an error.
func (q *Queryx) BindStructMap(arg0 interface{}, arg1 map[string]interface{}) *Queryx {
arglist, err := bindStructArgs(q.Names, arg0, arg1, q.Mapper)
arglist, err := q.bindStructArgs(arg0, arg1)
if err != nil {
q.err = fmt.Errorf("bind error: %s", err)
} else {
Expand All @@ -136,27 +146,31 @@ func (q *Queryx) BindStructMap(arg0 interface{}, arg1 map[string]interface{}) *Q
return q
}

func bindStructArgs(names []string, arg0 interface{}, arg1 map[string]interface{}, m *reflectx.Mapper) ([]interface{}, error) {
arglist := make([]interface{}, 0, len(names))
func (q *Queryx) bindStructArgs(arg0 interface{}, arg1 map[string]interface{}) ([]interface{}, error) {
arglist := make([]interface{}, 0, len(q.Names))

// grab the indirected value of arg
v := reflect.ValueOf(arg0)
for v = reflect.ValueOf(arg0); v.Kind() == reflect.Ptr; {
v = v.Elem()
}

err := m.TraversalsByNameFunc(v.Type(), names, func(i int, t []int) error {
err := q.Mapper.TraversalsByNameFunc(v.Type(), q.Names, func(i int, t []int) error {
if len(t) != 0 {
val := reflectx.FieldByIndexesReadOnly(v, t) // nolint:scopelint
arglist = append(arglist, val.Interface())
} else {
val, ok := arg1[names[i]]
val, ok := arg1[q.Names[i]]
if !ok {
return fmt.Errorf("could not find name %q in %#v and %#v", names[i], arg0, arg1)
return fmt.Errorf("could not find name %q in %#v and %#v", q.Names[i], arg0, arg1)
}
arglist = append(arglist, val)
}

if q.tr != nil {
arglist[i] = q.tr(q.Names[i], arglist[i])
}

return nil
})

Expand All @@ -165,7 +179,7 @@ func bindStructArgs(names []string, arg0 interface{}, arg1 map[string]interface{

// BindMap binds query named parameters using map.
func (q *Queryx) BindMap(arg map[string]interface{}) *Queryx {
arglist, err := bindMapArgs(q.Names, arg)
arglist, err := q.bindMapArgs(arg)
if err != nil {
q.err = fmt.Errorf("bind error: %s", err)
} else {
Expand All @@ -176,14 +190,18 @@ func (q *Queryx) BindMap(arg map[string]interface{}) *Queryx {
return q
}

func bindMapArgs(names []string, arg map[string]interface{}) ([]interface{}, error) {
arglist := make([]interface{}, 0, len(names))
func (q *Queryx) bindMapArgs(arg map[string]interface{}) ([]interface{}, error) {
arglist := make([]interface{}, 0, len(q.Names))

for _, name := range names {
for _, name := range q.Names {
val, ok := arg[name]
if !ok {
return arglist, fmt.Errorf("could not find name %q in %#v", name, arg)
}

if q.tr != nil {
val = q.tr(name, val)
}
arglist = append(arglist, val)
}
return arglist, nil
Expand Down
72 changes: 66 additions & 6 deletions queryx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func TestQueryxBindStruct(t *testing.T) {

t.Run("simple", func(t *testing.T) {
names := []string{"name", "age", "first", "last"}
args, err := bindStructArgs(names, v, nil, DefaultMapper)
args, err := Query(nil, names).bindStructArgs(v, nil)
if err != nil {
t.Fatal(err)
}
Expand All @@ -88,9 +88,28 @@ func TestQueryxBindStruct(t *testing.T) {
}
})

t.Run("with transformer", func(t *testing.T) {
tr := func(name string, val interface{}) interface{} {
if name == "age" {
return 42
}
return val
}

names := []string{"name", "age", "first", "last"}
args, err := Query(nil, names).WithBindTransformer(tr).bindStructArgs(v, nil)
if err != nil {
t.Fatal(err)
}

if diff := cmp.Diff(args, []interface{}{"name", 42, "first", "last"}); diff != "" {
t.Error("args mismatch", diff)
}
})

t.Run("error", func(t *testing.T) {
names := []string{"name", "age", "first", "not_found"}
_, err := bindStructArgs(names, v, nil, DefaultMapper)
_, err := Query(nil, names).bindStructArgs(v, nil)
if err == nil {
t.Fatal("unexpected error")
}
Expand All @@ -101,7 +120,7 @@ func TestQueryxBindStruct(t *testing.T) {
m := map[string]interface{}{
"not_found": "last",
}
args, err := bindStructArgs(names, v, m, DefaultMapper)
args, err := Query(nil, names).bindStructArgs(v, m)
if err != nil {
t.Fatal(err)
}
Expand All @@ -111,12 +130,34 @@ func TestQueryxBindStruct(t *testing.T) {
}
})

t.Run("fallback with transformer", func(t *testing.T) {
tr := func(name string, val interface{}) interface{} {
if name == "not_found" {
return "map_found"
}
return val
}

names := []string{"name", "age", "first", "not_found"}
m := map[string]interface{}{
"not_found": "last",
}
args, err := Query(nil, names).WithBindTransformer(tr).bindStructArgs(v, m)
if err != nil {
t.Fatal(err)
}

if diff := cmp.Diff(args, []interface{}{"name", 30, "first", "map_found"}); diff != "" {
t.Error("args mismatch", diff)
}
})

t.Run("fallback error", func(t *testing.T) {
names := []string{"name", "age", "first", "not_found", "really_not_found"}
m := map[string]interface{}{
"not_found": "last",
}
_, err := bindStructArgs(names, v, m, DefaultMapper)
_, err := Query(nil, names).bindStructArgs(v, m)
if err == nil {
t.Fatal("unexpected error")
}
Expand All @@ -133,7 +174,7 @@ func TestQueryxBindMap(t *testing.T) {

t.Run("simple", func(t *testing.T) {
names := []string{"name", "age", "first", "last"}
args, err := bindMapArgs(names, v)
args, err := Query(nil, names).bindMapArgs(v)
if err != nil {
t.Fatal(err)
}
Expand All @@ -143,9 +184,28 @@ func TestQueryxBindMap(t *testing.T) {
}
})

t.Run("with transformer", func(t *testing.T) {
tr := func(name string, val interface{}) interface{} {
if name == "age" {
return 42
}
return val
}

names := []string{"name", "age", "first", "last"}
args, err := Query(nil, names).WithBindTransformer(tr).bindMapArgs(v)
if err != nil {
t.Fatal(err)
}

if diff := cmp.Diff(args, []interface{}{"name", 42, "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)
_, err := Query(nil, names).bindMapArgs(v)
if err == nil {
t.Fatal("unexpected error")
}
Expand Down
2 changes: 2 additions & 0 deletions session.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func (s Session) ContextQuery(ctx context.Context, stmt string, names []string)
Query: s.Session.Query(stmt).WithContext(ctx),
Names: names,
Mapper: s.Mapper,
tr: DefaultBindTransformer,
}
}

Expand All @@ -62,6 +63,7 @@ func (s Session) Query(stmt string, names []string) *Queryx {
Query: s.Session.Query(stmt),
Names: names,
Mapper: s.Mapper,
tr: DefaultBindTransformer,
}
}

Expand Down
Loading