Merge pull request #5 from scylladb/mmt/named_params_for_using
Named parameters for USING TTL and TIMESTAMP
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
// +build integration
|
// +build all integration
|
||||||
|
|
||||||
package gocqlx_test
|
package gocqlx_test
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package gocqlx_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gocql/gocql"
|
"github.com/gocql/gocql"
|
||||||
"github.com/scylladb/gocqlx"
|
"github.com/scylladb/gocqlx"
|
||||||
@@ -134,6 +135,17 @@ func TestExample(t *testing.T) {
|
|||||||
mustExec(q.Query)
|
mustExec(q.Query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Insert with TTL
|
||||||
|
{
|
||||||
|
q := Query(qb.Insert("gocqlx_test.person").Columns("first_name", "last_name", "email").TTL().ToCql())
|
||||||
|
if err := q.BindStructMap(p, map[string]interface{}{
|
||||||
|
"_ttl": qb.TTL(86400 * time.Second),
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal("bind:", err)
|
||||||
|
}
|
||||||
|
mustExec(q.Query)
|
||||||
|
}
|
||||||
|
|
||||||
// Update
|
// Update
|
||||||
{
|
{
|
||||||
p.Email = append(p.Email, "patricia1.citzen@gocqlx_test.com")
|
p.Email = append(p.Email, "patricia1.citzen@gocqlx_test.com")
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ package qb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DeleteBuilder builds CQL DELETE statements.
|
// DeleteBuilder builds CQL DELETE statements.
|
||||||
@@ -38,8 +37,7 @@ func (b *DeleteBuilder) ToCql() (stmt string, names []string) {
|
|||||||
cql.WriteString(b.table)
|
cql.WriteString(b.table)
|
||||||
cql.WriteByte(' ')
|
cql.WriteByte(' ')
|
||||||
|
|
||||||
b.using.writeCql(&cql)
|
names = append(names, b.using.writeCql(&cql)...)
|
||||||
|
|
||||||
names = append(names, b.where.writeCql(&cql)...)
|
names = append(names, b.where.writeCql(&cql)...)
|
||||||
names = append(names, b._if.writeCql(&cql)...)
|
names = append(names, b._if.writeCql(&cql)...)
|
||||||
|
|
||||||
@@ -64,8 +62,8 @@ func (b *DeleteBuilder) Columns(columns ...string) *DeleteBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Timestamp sets a USING TIMESTAMP clause on the query.
|
// Timestamp sets a USING TIMESTAMP clause on the query.
|
||||||
func (b *DeleteBuilder) Timestamp(t time.Time) *DeleteBuilder {
|
func (b *DeleteBuilder) Timestamp() *DeleteBuilder {
|
||||||
b.using.timestamp = t
|
b.using.timestamp = true
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package qb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
)
|
)
|
||||||
@@ -47,9 +46,9 @@ func TestDeleteBuilder(t *testing.T) {
|
|||||||
},
|
},
|
||||||
// Add TIMESTAMP
|
// Add TIMESTAMP
|
||||||
{
|
{
|
||||||
B: Delete("cycling.cyclist_name").Where(w).Timestamp(time.Unix(0, 0).Add(time.Microsecond * 123456789)),
|
B: Delete("cycling.cyclist_name").Where(w).Timestamp(),
|
||||||
S: "DELETE FROM cycling.cyclist_name USING TIMESTAMP 123456789 WHERE id=? ",
|
S: "DELETE FROM cycling.cyclist_name USING TIMESTAMP ? WHERE id=? ",
|
||||||
N: []string{"expr"},
|
N: []string{"_ts", "expr"},
|
||||||
},
|
},
|
||||||
// Add IF EXISTS
|
// Add IF EXISTS
|
||||||
{
|
{
|
||||||
|
|||||||
30
qb/expr.go
30
qb/expr.go
@@ -2,8 +2,6 @@ package qb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type columns []string
|
type columns []string
|
||||||
@@ -18,28 +16,26 @@ func (cols columns) writeCql(cql *bytes.Buffer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type using struct {
|
type using struct {
|
||||||
timestamp time.Time
|
timestamp bool
|
||||||
ttl time.Duration
|
ttl bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u using) writeCql(cql *bytes.Buffer) {
|
func (u using) writeCql(cql *bytes.Buffer) (names []string) {
|
||||||
ts := !u.timestamp.IsZero()
|
if u.timestamp {
|
||||||
|
cql.WriteString("USING TIMESTAMP ? ")
|
||||||
if ts {
|
names = append(names, "_ts")
|
||||||
cql.WriteString("USING TIMESTAMP ")
|
|
||||||
cql.WriteString(fmt.Sprint(u.timestamp.UnixNano() / 1000))
|
|
||||||
cql.WriteByte(' ')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if u.ttl != 0 {
|
if u.ttl {
|
||||||
if ts {
|
if u.timestamp {
|
||||||
cql.WriteString("AND TTL ")
|
cql.WriteString("AND TTL ? ")
|
||||||
} else {
|
} else {
|
||||||
cql.WriteString("USING TTL ")
|
cql.WriteString("USING TTL ? ")
|
||||||
}
|
}
|
||||||
cql.WriteString(fmt.Sprint(int(u.ttl.Seconds())))
|
names = append(names, "_ttl")
|
||||||
cql.WriteByte(' ')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type where cmps
|
type where cmps
|
||||||
|
|||||||
14
qb/insert.go
14
qb/insert.go
@@ -5,7 +5,6 @@ package qb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// InsertBuilder builds CQL INSERT statements.
|
// InsertBuilder builds CQL INSERT statements.
|
||||||
@@ -35,19 +34,20 @@ func (b *InsertBuilder) ToCql() (stmt string, names []string) {
|
|||||||
|
|
||||||
cql.WriteByte('(')
|
cql.WriteByte('(')
|
||||||
b.columns.writeCql(&cql)
|
b.columns.writeCql(&cql)
|
||||||
|
names = append(names, b.columns...)
|
||||||
cql.WriteString(") ")
|
cql.WriteString(") ")
|
||||||
|
|
||||||
cql.WriteString("VALUES (")
|
cql.WriteString("VALUES (")
|
||||||
placeholders(&cql, len(b.columns))
|
placeholders(&cql, len(b.columns))
|
||||||
cql.WriteString(") ")
|
cql.WriteString(") ")
|
||||||
|
|
||||||
b.using.writeCql(&cql)
|
names = append(names, b.using.writeCql(&cql)...)
|
||||||
|
|
||||||
if b.unique {
|
if b.unique {
|
||||||
cql.WriteString("IF NOT EXISTS ")
|
cql.WriteString("IF NOT EXISTS ")
|
||||||
}
|
}
|
||||||
|
|
||||||
stmt, names = cql.String(), b.columns
|
stmt = cql.String()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,13 +70,13 @@ func (b *InsertBuilder) Unique() *InsertBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Timestamp sets a USING TIMESTAMP clause on the query.
|
// Timestamp sets a USING TIMESTAMP clause on the query.
|
||||||
func (b *InsertBuilder) Timestamp(t time.Time) *InsertBuilder {
|
func (b *InsertBuilder) Timestamp() *InsertBuilder {
|
||||||
b.using.timestamp = t
|
b.using.timestamp = true
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// TTL sets a USING TTL clause on the query.
|
// TTL sets a USING TTL clause on the query.
|
||||||
func (b *InsertBuilder) TTL(d time.Duration) *InsertBuilder {
|
func (b *InsertBuilder) TTL() *InsertBuilder {
|
||||||
b.using.ttl = d
|
b.using.ttl = true
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package qb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
)
|
)
|
||||||
@@ -34,15 +33,15 @@ func TestInsertBuilder(t *testing.T) {
|
|||||||
},
|
},
|
||||||
// Add TTL
|
// Add TTL
|
||||||
{
|
{
|
||||||
B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").TTL(time.Second * 86400),
|
B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").TTL(),
|
||||||
S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TTL 86400 ",
|
S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TTL ? ",
|
||||||
N: []string{"id", "user_uuid", "firstname"},
|
N: []string{"id", "user_uuid", "firstname", "_ttl"},
|
||||||
},
|
},
|
||||||
// Add TIMESTAMP
|
// Add TIMESTAMP
|
||||||
{
|
{
|
||||||
B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").Timestamp(time.Unix(0, 0).Add(time.Microsecond * 123456789)),
|
B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").Timestamp(),
|
||||||
S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TIMESTAMP 123456789 ",
|
S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TIMESTAMP ? ",
|
||||||
N: []string{"id", "user_uuid", "firstname"},
|
N: []string{"id", "user_uuid", "firstname", "_ts"},
|
||||||
},
|
},
|
||||||
// Add IF NOT EXISTS
|
// Add IF NOT EXISTS
|
||||||
{
|
{
|
||||||
|
|||||||
11
qb/update.go
11
qb/update.go
@@ -5,7 +5,6 @@ package qb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// UpdateBuilder builds CQL UPDATE statements.
|
// UpdateBuilder builds CQL UPDATE statements.
|
||||||
@@ -33,7 +32,7 @@ func (b *UpdateBuilder) ToCql() (stmt string, names []string) {
|
|||||||
cql.WriteString(b.table)
|
cql.WriteString(b.table)
|
||||||
cql.WriteByte(' ')
|
cql.WriteByte(' ')
|
||||||
|
|
||||||
b.using.writeCql(&cql)
|
names = append(names, b.using.writeCql(&cql)...)
|
||||||
|
|
||||||
cql.WriteString("SET ")
|
cql.WriteString("SET ")
|
||||||
for i, c := range b.columns {
|
for i, c := range b.columns {
|
||||||
@@ -64,14 +63,14 @@ func (b *UpdateBuilder) Table(table string) *UpdateBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Timestamp sets a USING TIMESTAMP clause on the query.
|
// Timestamp sets a USING TIMESTAMP clause on the query.
|
||||||
func (b *UpdateBuilder) Timestamp(t time.Time) *UpdateBuilder {
|
func (b *UpdateBuilder) Timestamp() *UpdateBuilder {
|
||||||
b.using.timestamp = t
|
b.using.timestamp = true
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// TTL sets a USING TTL clause on the query.
|
// TTL sets a USING TTL clause on the query.
|
||||||
func (b *UpdateBuilder) TTL(d time.Duration) *UpdateBuilder {
|
func (b *UpdateBuilder) TTL() *UpdateBuilder {
|
||||||
b.using.ttl = d
|
b.using.ttl = true
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package qb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
)
|
)
|
||||||
@@ -47,15 +46,15 @@ func TestUpdateBuilder(t *testing.T) {
|
|||||||
},
|
},
|
||||||
// Add TTL
|
// Add TTL
|
||||||
{
|
{
|
||||||
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).TTL(time.Second * 86400),
|
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).TTL(),
|
||||||
S: "UPDATE cycling.cyclist_name USING TTL 86400 SET id=?,user_uuid=?,firstname=? WHERE id=? ",
|
S: "UPDATE cycling.cyclist_name USING TTL ? SET id=?,user_uuid=?,firstname=? WHERE id=? ",
|
||||||
N: []string{"id", "user_uuid", "firstname", "expr"},
|
N: []string{"_ttl", "id", "user_uuid", "firstname", "expr"},
|
||||||
},
|
},
|
||||||
// Add TIMESTAMP
|
// Add TIMESTAMP
|
||||||
{
|
{
|
||||||
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).Timestamp(time.Unix(0, 0).Add(time.Microsecond * 123456789)),
|
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).Timestamp(),
|
||||||
S: "UPDATE cycling.cyclist_name USING TIMESTAMP 123456789 SET id=?,user_uuid=?,firstname=? WHERE id=? ",
|
S: "UPDATE cycling.cyclist_name USING TIMESTAMP ? SET id=?,user_uuid=?,firstname=? WHERE id=? ",
|
||||||
N: []string{"id", "user_uuid", "firstname", "expr"},
|
N: []string{"_ts", "id", "user_uuid", "firstname", "expr"},
|
||||||
},
|
},
|
||||||
// Add IF EXISTS
|
// Add IF EXISTS
|
||||||
{
|
{
|
||||||
|
|||||||
11
qb/utils.go
11
qb/utils.go
@@ -2,6 +2,7 @@ package qb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// placeholders returns a string with count ? placeholders joined with commas.
|
// placeholders returns a string with count ? placeholders joined with commas.
|
||||||
@@ -16,3 +17,13 @@ func placeholders(cql *bytes.Buffer, count int) {
|
|||||||
}
|
}
|
||||||
cql.WriteByte('?')
|
cql.WriteByte('?')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TTL converts duration to format expected in USING TTL clause.
|
||||||
|
func TTL(d time.Duration) int64 {
|
||||||
|
return int64(d.Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timestamp converts time to format expected in USING TIMESTAMP clause.
|
||||||
|
func Timestamp(t time.Time) int64 {
|
||||||
|
return t.UnixNano() / 1000
|
||||||
|
}
|
||||||
|
|||||||
18
qb/utils_test.go
Normal file
18
qb/utils_test.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package qb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTTL(t *testing.T) {
|
||||||
|
if TTL(time.Second*86400) != 86400 {
|
||||||
|
t.Fatal("wrong ttl")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimestamp(t *testing.T) {
|
||||||
|
if Timestamp(time.Unix(0, 0).Add(time.Microsecond*123456789)) != 123456789 {
|
||||||
|
t.Fatal("wrong timestamp")
|
||||||
|
}
|
||||||
|
}
|
||||||
36
queryx.go
36
queryx.go
@@ -91,9 +91,10 @@ func Query(q *gocql.Query, names []string) Queryx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BindStruct binds query named parameters using mapper.
|
// 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{}) error {
|
func (q Queryx) BindStruct(arg interface{}) error {
|
||||||
arglist, err := bindStructArgs(q.Names, arg, q.Mapper)
|
arglist, err := bindStructArgs(q.Names, arg, nil, q.Mapper)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -103,22 +104,41 @@ func (q Queryx) BindStruct(arg interface{}) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func bindStructArgs(names []string, arg interface{}, m *reflectx.Mapper) ([]interface{}, error) {
|
// BindStructMap binds query named parameters to values from arg0 and arg1
|
||||||
|
// 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{}) error {
|
||||||
|
arglist, err := bindStructArgs(q.Names, arg0, arg1, q.Mapper)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
q.Bind(arglist...)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindStructArgs(names []string, arg0 interface{}, arg1 map[string]interface{}, m *reflectx.Mapper) ([]interface{}, error) {
|
||||||
arglist := make([]interface{}, 0, len(names))
|
arglist := make([]interface{}, 0, len(names))
|
||||||
|
|
||||||
// grab the indirected value of arg
|
// grab the indirected value of arg
|
||||||
v := reflect.ValueOf(arg)
|
v := reflect.ValueOf(arg0)
|
||||||
for v = reflect.ValueOf(arg); v.Kind() == reflect.Ptr; {
|
for v = reflect.ValueOf(arg0); v.Kind() == reflect.Ptr; {
|
||||||
v = v.Elem()
|
v = v.Elem()
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := m.TraversalsByName(v.Type(), names)
|
fields := m.TraversalsByName(v.Type(), names)
|
||||||
for i, t := range fields {
|
for i, t := range fields {
|
||||||
if len(t) == 0 {
|
if len(t) != 0 {
|
||||||
return arglist, fmt.Errorf("could not find name %s in %#v", names[i], arg)
|
|
||||||
}
|
|
||||||
val := reflectx.FieldByIndexesReadOnly(v, t)
|
val := reflectx.FieldByIndexesReadOnly(v, t)
|
||||||
arglist = append(arglist, val.Interface())
|
arglist = append(arglist, val.Interface())
|
||||||
|
} else {
|
||||||
|
val, ok := arg1[names[i]]
|
||||||
|
if !ok {
|
||||||
|
return arglist, fmt.Errorf("could not find name %s in %#v and %#v", names[i], arg0, arg1)
|
||||||
|
}
|
||||||
|
arglist = append(arglist, val)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return arglist, nil
|
return arglist, nil
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ func TestBindStruct(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("simple", func(t *testing.T) {
|
t.Run("simple", func(t *testing.T) {
|
||||||
names := []string{"name", "age", "first", "last"}
|
names := []string{"name", "age", "first", "last"}
|
||||||
args, err := bindStructArgs(names, v, DefaultMapper)
|
args, err := bindStructArgs(names, v, nil, DefaultMapper)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -94,8 +94,34 @@ func TestBindStruct(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("error", func(t *testing.T) {
|
t.Run("error", func(t *testing.T) {
|
||||||
names := []string{"name", "first", "not_found"}
|
names := []string{"name", "age", "first", "not_found"}
|
||||||
_, err := bindStructArgs(names, v, DefaultMapper)
|
_, err := bindStructArgs(names, v, nil, DefaultMapper)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("unexpected error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fallback", func(t *testing.T) {
|
||||||
|
names := []string{"name", "age", "first", "not_found"}
|
||||||
|
m := map[string]interface{}{
|
||||||
|
"not_found": "last",
|
||||||
|
}
|
||||||
|
args, err := bindStructArgs(names, v, m, 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("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)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("unexpected error")
|
t.Fatal("unexpected error")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user