diff --git a/benchmark_test.go b/benchmark_test.go index f7755b1..7852971 100644 --- a/benchmark_test.go +++ b/benchmark_test.go @@ -1,4 +1,4 @@ -// +build integration +// +build all integration package gocqlx_test diff --git a/example_test.go b/example_test.go index 15c0f08..9aede55 100644 --- a/example_test.go +++ b/example_test.go @@ -4,6 +4,7 @@ package gocqlx_test import ( "testing" + "time" "github.com/gocql/gocql" "github.com/scylladb/gocqlx" @@ -134,6 +135,17 @@ func TestExample(t *testing.T) { 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 { p.Email = append(p.Email, "patricia1.citzen@gocqlx_test.com") diff --git a/qb/delete.go b/qb/delete.go index 21c92e1..f5b4bd4 100644 --- a/qb/delete.go +++ b/qb/delete.go @@ -5,7 +5,6 @@ package qb import ( "bytes" - "time" ) // DeleteBuilder builds CQL DELETE statements. @@ -38,8 +37,7 @@ func (b *DeleteBuilder) ToCql() (stmt string, names []string) { cql.WriteString(b.table) cql.WriteByte(' ') - b.using.writeCql(&cql) - + names = append(names, b.using.writeCql(&cql)...) names = append(names, b.where.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. -func (b *DeleteBuilder) Timestamp(t time.Time) *DeleteBuilder { - b.using.timestamp = t +func (b *DeleteBuilder) Timestamp() *DeleteBuilder { + b.using.timestamp = true return b } diff --git a/qb/delete_test.go b/qb/delete_test.go index db01ca1..01d6bf2 100644 --- a/qb/delete_test.go +++ b/qb/delete_test.go @@ -2,7 +2,6 @@ package qb import ( "testing" - "time" "github.com/google/go-cmp/cmp" ) @@ -47,9 +46,9 @@ func TestDeleteBuilder(t *testing.T) { }, // Add TIMESTAMP { - B: Delete("cycling.cyclist_name").Where(w).Timestamp(time.Unix(0, 0).Add(time.Microsecond * 123456789)), - S: "DELETE FROM cycling.cyclist_name USING TIMESTAMP 123456789 WHERE id=? ", - N: []string{"expr"}, + B: Delete("cycling.cyclist_name").Where(w).Timestamp(), + S: "DELETE FROM cycling.cyclist_name USING TIMESTAMP ? WHERE id=? ", + N: []string{"_ts", "expr"}, }, // Add IF EXISTS { diff --git a/qb/expr.go b/qb/expr.go index e9d98b4..18069be 100644 --- a/qb/expr.go +++ b/qb/expr.go @@ -2,8 +2,6 @@ package qb import ( "bytes" - "fmt" - "time" ) type columns []string @@ -18,28 +16,26 @@ func (cols columns) writeCql(cql *bytes.Buffer) { } type using struct { - timestamp time.Time - ttl time.Duration + timestamp bool + ttl bool } -func (u using) writeCql(cql *bytes.Buffer) { - ts := !u.timestamp.IsZero() - - if ts { - cql.WriteString("USING TIMESTAMP ") - cql.WriteString(fmt.Sprint(u.timestamp.UnixNano() / 1000)) - cql.WriteByte(' ') +func (u using) writeCql(cql *bytes.Buffer) (names []string) { + if u.timestamp { + cql.WriteString("USING TIMESTAMP ? ") + names = append(names, "_ts") } - if u.ttl != 0 { - if ts { - cql.WriteString("AND TTL ") + if u.ttl { + if u.timestamp { + cql.WriteString("AND TTL ? ") } else { - cql.WriteString("USING TTL ") + cql.WriteString("USING TTL ? ") } - cql.WriteString(fmt.Sprint(int(u.ttl.Seconds()))) - cql.WriteByte(' ') + names = append(names, "_ttl") } + + return } type where cmps diff --git a/qb/insert.go b/qb/insert.go index 86a98a3..384ce92 100644 --- a/qb/insert.go +++ b/qb/insert.go @@ -5,7 +5,6 @@ package qb import ( "bytes" - "time" ) // InsertBuilder builds CQL INSERT statements. @@ -35,19 +34,20 @@ func (b *InsertBuilder) ToCql() (stmt string, names []string) { cql.WriteByte('(') b.columns.writeCql(&cql) + names = append(names, b.columns...) cql.WriteString(") ") cql.WriteString("VALUES (") placeholders(&cql, len(b.columns)) cql.WriteString(") ") - b.using.writeCql(&cql) + names = append(names, b.using.writeCql(&cql)...) if b.unique { cql.WriteString("IF NOT EXISTS ") } - stmt, names = cql.String(), b.columns + stmt = cql.String() return } @@ -70,13 +70,13 @@ func (b *InsertBuilder) Unique() *InsertBuilder { } // Timestamp sets a USING TIMESTAMP clause on the query. -func (b *InsertBuilder) Timestamp(t time.Time) *InsertBuilder { - b.using.timestamp = t +func (b *InsertBuilder) Timestamp() *InsertBuilder { + b.using.timestamp = true return b } // TTL sets a USING TTL clause on the query. -func (b *InsertBuilder) TTL(d time.Duration) *InsertBuilder { - b.using.ttl = d +func (b *InsertBuilder) TTL() *InsertBuilder { + b.using.ttl = true return b } diff --git a/qb/insert_test.go b/qb/insert_test.go index 02299eb..7a4d600 100644 --- a/qb/insert_test.go +++ b/qb/insert_test.go @@ -2,7 +2,6 @@ package qb import ( "testing" - "time" "github.com/google/go-cmp/cmp" ) @@ -34,15 +33,15 @@ func TestInsertBuilder(t *testing.T) { }, // Add TTL { - B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").TTL(time.Second * 86400), - S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TTL 86400 ", - N: []string{"id", "user_uuid", "firstname"}, + B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").TTL(), + S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TTL ? ", + N: []string{"id", "user_uuid", "firstname", "_ttl"}, }, // Add TIMESTAMP { - B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").Timestamp(time.Unix(0, 0).Add(time.Microsecond * 123456789)), - S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TIMESTAMP 123456789 ", - N: []string{"id", "user_uuid", "firstname"}, + B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").Timestamp(), + S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TIMESTAMP ? ", + N: []string{"id", "user_uuid", "firstname", "_ts"}, }, // Add IF NOT EXISTS { diff --git a/qb/update.go b/qb/update.go index 4fd433c..1a5b802 100644 --- a/qb/update.go +++ b/qb/update.go @@ -5,7 +5,6 @@ package qb import ( "bytes" - "time" ) // UpdateBuilder builds CQL UPDATE statements. @@ -33,7 +32,7 @@ func (b *UpdateBuilder) ToCql() (stmt string, names []string) { cql.WriteString(b.table) cql.WriteByte(' ') - b.using.writeCql(&cql) + names = append(names, b.using.writeCql(&cql)...) cql.WriteString("SET ") 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. -func (b *UpdateBuilder) Timestamp(t time.Time) *UpdateBuilder { - b.using.timestamp = t +func (b *UpdateBuilder) Timestamp() *UpdateBuilder { + b.using.timestamp = true return b } // TTL sets a USING TTL clause on the query. -func (b *UpdateBuilder) TTL(d time.Duration) *UpdateBuilder { - b.using.ttl = d +func (b *UpdateBuilder) TTL() *UpdateBuilder { + b.using.ttl = true return b } diff --git a/qb/update_test.go b/qb/update_test.go index ba77154..2a2447f 100644 --- a/qb/update_test.go +++ b/qb/update_test.go @@ -2,7 +2,6 @@ package qb import ( "testing" - "time" "github.com/google/go-cmp/cmp" ) @@ -47,15 +46,15 @@ func TestUpdateBuilder(t *testing.T) { }, // Add TTL { - B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).TTL(time.Second * 86400), - S: "UPDATE cycling.cyclist_name USING TTL 86400 SET id=?,user_uuid=?,firstname=? WHERE id=? ", - N: []string{"id", "user_uuid", "firstname", "expr"}, + B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).TTL(), + S: "UPDATE cycling.cyclist_name USING TTL ? SET id=?,user_uuid=?,firstname=? WHERE id=? ", + N: []string{"_ttl", "id", "user_uuid", "firstname", "expr"}, }, // Add TIMESTAMP { - B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).Timestamp(time.Unix(0, 0).Add(time.Microsecond * 123456789)), - S: "UPDATE cycling.cyclist_name USING TIMESTAMP 123456789 SET id=?,user_uuid=?,firstname=? WHERE id=? ", - N: []string{"id", "user_uuid", "firstname", "expr"}, + B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).Timestamp(), + S: "UPDATE cycling.cyclist_name USING TIMESTAMP ? SET id=?,user_uuid=?,firstname=? WHERE id=? ", + N: []string{"_ts", "id", "user_uuid", "firstname", "expr"}, }, // Add IF EXISTS { diff --git a/qb/utils.go b/qb/utils.go index c89dc5e..5a83081 100644 --- a/qb/utils.go +++ b/qb/utils.go @@ -2,6 +2,7 @@ package qb import ( "bytes" + "time" ) // placeholders returns a string with count ? placeholders joined with commas. @@ -16,3 +17,13 @@ func placeholders(cql *bytes.Buffer, count int) { } 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 +} diff --git a/qb/utils_test.go b/qb/utils_test.go new file mode 100644 index 0000000..6d47701 --- /dev/null +++ b/qb/utils_test.go @@ -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") + } +} diff --git a/queryx.go b/queryx.go index d7e7da0..f26392e 100644 --- a/queryx.go +++ b/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 { - arglist, err := bindStructArgs(q.Names, arg, q.Mapper) + arglist, err := bindStructArgs(q.Names, arg, nil, q.Mapper) if err != nil { return err } @@ -103,22 +104,41 @@ func (q Queryx) BindStruct(arg interface{}) error { 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)) // grab the indirected value of arg - v := reflect.ValueOf(arg) - for v = reflect.ValueOf(arg); v.Kind() == reflect.Ptr; { + v := reflect.ValueOf(arg0) + for v = reflect.ValueOf(arg0); 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) + if len(t) != 0 { + val := reflectx.FieldByIndexesReadOnly(v, t) + 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) } - val := reflectx.FieldByIndexesReadOnly(v, t) - arglist = append(arglist, val.Interface()) } return arglist, nil diff --git a/queryx_test.go b/queryx_test.go index 2c74f85..df0f212 100644 --- a/queryx_test.go +++ b/queryx_test.go @@ -83,7 +83,7 @@ func TestBindStruct(t *testing.T) { t.Run("simple", func(t *testing.T) { names := []string{"name", "age", "first", "last"} - args, err := bindStructArgs(names, v, DefaultMapper) + args, err := bindStructArgs(names, v, nil, DefaultMapper) if err != nil { t.Fatal(err) } @@ -94,8 +94,34 @@ func TestBindStruct(t *testing.T) { }) t.Run("error", func(t *testing.T) { - names := []string{"name", "first", "not_found"} - _, err := bindStructArgs(names, v, DefaultMapper) + names := []string{"name", "age", "first", "not_found"} + _, 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 { t.Fatal("unexpected error") }