diff --git a/example_test.go b/example_test.go index 5a1d5dc..395de03 100644 --- a/example_test.go +++ b/example_test.go @@ -61,13 +61,12 @@ func TestExample(t *testing.T) { { stmt, names := qb.Insert("gocqlx_test.person"). Columns("first_name", "last_name", "email"). - TTL(). + TTL(86400 * time.Second). + Timestamp(time.Now()). ToCql() - q := gocqlx.Query(session.Query(stmt), names).BindStructMap(p, qb.M{ - "_ttl": qb.TTL(86400 * time.Second), - }) - if err := q.ExecRelease(); err != nil { + err := gocqlx.Query(session.Query(stmt), names).BindStruct(p).ExecRelease() + if err != nil { t.Fatal(err) } } diff --git a/qb/batch.go b/qb/batch.go index 1642dda..09666ba 100644 --- a/qb/batch.go +++ b/qb/batch.go @@ -7,6 +7,7 @@ package qb import ( "bytes" "fmt" + "time" ) // BATCH reference: @@ -98,14 +99,27 @@ func (b *BatchBuilder) Counter() *BatchBuilder { return b } -// Timestamp sets a USING TIMESTAMP clause on the query. -func (b *BatchBuilder) Timestamp() *BatchBuilder { - b.using.timestamp = true +// TTL adds USING TTL clause to the query. +func (b *BatchBuilder) TTL(d time.Duration) *BatchBuilder { + b.using.TTL(d) return b } -// TTL sets a USING TTL clause on the query. -func (b *BatchBuilder) TTL() *BatchBuilder { - b.using.ttl = true +// TTLNamed adds USING TTL clause to the query with a custom parameter name. +func (b *BatchBuilder) TTLNamed(name string) *BatchBuilder { + b.using.TTLNamed(name) + return b +} + +// Timestamp adds USING TIMESTAMP clause to the query. +func (b *BatchBuilder) Timestamp(t time.Time) *BatchBuilder { + b.using.Timestamp(t) + return b +} + +// TimestampNamed adds a USING TIMESTAMP clause to the query with a custom +// parameter name. +func (b *BatchBuilder) TimestampNamed(name string) *BatchBuilder { + b.using.TimestampNamed(name) return b } diff --git a/qb/batch_test.go b/qb/batch_test.go index f902220..6fe6294 100644 --- a/qb/batch_test.go +++ b/qb/batch_test.go @@ -6,6 +6,7 @@ package qb import ( "testing" + "time" "github.com/google/go-cmp/cmp" ) @@ -53,15 +54,23 @@ func TestBatchBuilder(t *testing.T) { }, // Add TTL { - B: Batch().TTL(), + B: Batch().TTL(time.Second), + S: "BEGIN BATCH USING TTL 1 APPLY BATCH ", + }, + { + B: Batch().TTLNamed("ttl"), S: "BEGIN BATCH USING TTL ? APPLY BATCH ", - N: []string{"_ttl"}, + N: []string{"ttl"}, }, // Add TIMESTAMP { - B: Batch().Timestamp(), + B: Batch().Timestamp(time.Date(2005, 05, 05, 0, 0, 0, 0, time.UTC)), + S: "BEGIN BATCH USING TIMESTAMP 1115251200000000 APPLY BATCH ", + }, + { + B: Batch().TimestampNamed("ts"), S: "BEGIN BATCH USING TIMESTAMP ? APPLY BATCH ", - N: []string{"_ts"}, + N: []string{"ts"}, }, } diff --git a/qb/cmp.go b/qb/cmp.go index 6cc68aa..fe13260 100644 --- a/qb/cmp.go +++ b/qb/cmp.go @@ -297,3 +297,25 @@ func (cs cmps) writeCql(cql *bytes.Buffer) (names []string) { cql.WriteByte(' ') return } + +type where cmps + +func (w where) writeCql(cql *bytes.Buffer) (names []string) { + if len(w) == 0 { + return + } + + cql.WriteString("WHERE ") + return cmps(w).writeCql(cql) +} + +type _if cmps + +func (w _if) writeCql(cql *bytes.Buffer) (names []string) { + if len(w) == 0 { + return + } + + cql.WriteString("IF ") + return cmps(w).writeCql(cql) +} diff --git a/qb/delete.go b/qb/delete.go index f7784cb..db8f6e8 100644 --- a/qb/delete.go +++ b/qb/delete.go @@ -9,6 +9,7 @@ package qb import ( "bytes" + "time" ) // DeleteBuilder builds CQL DELETE statements. @@ -65,9 +66,16 @@ func (b *DeleteBuilder) Columns(columns ...string) *DeleteBuilder { return b } -// Timestamp sets a USING TIMESTAMP clause on the query. -func (b *DeleteBuilder) Timestamp() *DeleteBuilder { - b.using.timestamp = true +// Timestamp adds USING TIMESTAMP clause to the query. +func (b *DeleteBuilder) Timestamp(t time.Time) *DeleteBuilder { + b.using.Timestamp(t) + return b +} + +// TimestampNamed adds a USING TIMESTAMP clause to the query with a custom +// parameter name. +func (b *DeleteBuilder) TimestampNamed(name string) *DeleteBuilder { + b.using.TimestampNamed(name) return b } diff --git a/qb/delete_test.go b/qb/delete_test.go index 2388d77..b2bc93e 100644 --- a/qb/delete_test.go +++ b/qb/delete_test.go @@ -6,6 +6,7 @@ package qb import ( "testing" + "time" "github.com/google/go-cmp/cmp" ) @@ -50,9 +51,14 @@ func TestDeleteBuilder(t *testing.T) { }, // Add TIMESTAMP { - B: Delete("cycling.cyclist_name").Where(w).Timestamp(), + B: Delete("cycling.cyclist_name").Where(w).Timestamp(time.Date(2005, 05, 05, 0, 0, 0, 0, time.UTC)), + S: "DELETE FROM cycling.cyclist_name USING TIMESTAMP 1115251200000000 WHERE id=? ", + N: []string{"expr"}, + }, + { + B: Delete("cycling.cyclist_name").Where(w).TimestampNamed("ts"), S: "DELETE FROM cycling.cyclist_name USING TIMESTAMP ? WHERE id=? ", - N: []string{"_ts", "expr"}, + N: []string{"ts", "expr"}, }, // Add IF EXISTS { @@ -68,7 +74,7 @@ func TestDeleteBuilder(t *testing.T) { t.Error(diff) } if diff := cmp.Diff(test.N, names); diff != "" { - t.Error(diff) + t.Error(diff, names) } } } diff --git a/qb/expr.go b/qb/expr.go deleted file mode 100644 index e60c738..0000000 --- a/qb/expr.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (C) 2017 ScyllaDB -// Use of this source code is governed by a ALv2-style -// license that can be found in the LICENSE file. - -package qb - -import ( - "bytes" -) - -type columns []string - -func (cols columns) writeCql(cql *bytes.Buffer) { - for i, c := range cols { - cql.WriteString(c) - if i < len(cols)-1 { - cql.WriteByte(',') - } - } -} - -type using struct { - timestamp bool - ttl bool -} - -func (u using) writeCql(cql *bytes.Buffer) (names []string) { - if u.timestamp { - cql.WriteString("USING TIMESTAMP ? ") - names = append(names, "_ts") - } - - if u.ttl { - if u.timestamp { - cql.WriteString("AND TTL ? ") - } else { - cql.WriteString("USING TTL ? ") - } - names = append(names, "_ttl") - } - - return -} - -type where cmps - -func (w where) writeCql(cql *bytes.Buffer) (names []string) { - if len(w) == 0 { - return - } - - cql.WriteString("WHERE ") - return cmps(w).writeCql(cql) -} - -type _if cmps - -func (w _if) writeCql(cql *bytes.Buffer) (names []string) { - if len(w) == 0 { - return - } - - cql.WriteString("IF ") - return cmps(w).writeCql(cql) -} diff --git a/qb/insert.go b/qb/insert.go index 67e2340..dc628c4 100644 --- a/qb/insert.go +++ b/qb/insert.go @@ -9,6 +9,7 @@ package qb import ( "bytes" + "time" ) // initializer specifies an value for a column in an insert operation. @@ -119,14 +120,27 @@ func (b *InsertBuilder) Unique() *InsertBuilder { return b } -// Timestamp sets a USING TIMESTAMP clause on the query. -func (b *InsertBuilder) Timestamp() *InsertBuilder { - b.using.timestamp = true +// TTL adds USING TTL clause to the query. +func (b *InsertBuilder) TTL(d time.Duration) *InsertBuilder { + b.using.TTL(d) return b } -// TTL sets a USING TTL clause on the query. -func (b *InsertBuilder) TTL() *InsertBuilder { - b.using.ttl = true +// TTLNamed adds USING TTL clause to the query with a custom parameter name. +func (b *InsertBuilder) TTLNamed(name string) *InsertBuilder { + b.using.TTLNamed(name) + return b +} + +// Timestamp adds USING TIMESTAMP clause to the query. +func (b *InsertBuilder) Timestamp(t time.Time) *InsertBuilder { + b.using.Timestamp(t) + return b +} + +// TimestampNamed adds a USING TIMESTAMP clause to the query with a custom +// parameter name. +func (b *InsertBuilder) TimestampNamed(name string) *InsertBuilder { + b.using.TimestampNamed(name) return b } diff --git a/qb/insert_test.go b/qb/insert_test.go index 47f2134..91196c5 100644 --- a/qb/insert_test.go +++ b/qb/insert_test.go @@ -6,6 +6,7 @@ package qb import ( "testing" + "time" "github.com/google/go-cmp/cmp" ) @@ -49,15 +50,25 @@ func TestInsertBuilder(t *testing.T) { }, // Add TTL { - B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").TTL(), + B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").TTL(time.Second), + S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TTL 1 ", + N: []string{"id", "user_uuid", "firstname"}, + }, + { + B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").TTLNamed("ttl"), S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TTL ? ", - N: []string{"id", "user_uuid", "firstname", "_ttl"}, + N: []string{"id", "user_uuid", "firstname", "ttl"}, }, // Add TIMESTAMP { - B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").Timestamp(), + B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").Timestamp(time.Date(2005, 05, 05, 0, 0, 0, 0, time.UTC)), + S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TIMESTAMP 1115251200000000 ", + N: []string{"id", "user_uuid", "firstname"}, + }, + { + B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").TimestampNamed("ts"), S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TIMESTAMP ? ", - N: []string{"id", "user_uuid", "firstname", "_ts"}, + N: []string{"id", "user_uuid", "firstname", "ts"}, }, // Add IF NOT EXISTS { diff --git a/qb/update.go b/qb/update.go index c8236ca..c21be4e 100644 --- a/qb/update.go +++ b/qb/update.go @@ -9,6 +9,7 @@ package qb import ( "bytes" + "time" ) // assignment specifies an assignment in a set operation. @@ -78,15 +79,28 @@ func (b *UpdateBuilder) Table(table string) *UpdateBuilder { return b } -// Timestamp sets a USING TIMESTAMP clause on the query. -func (b *UpdateBuilder) Timestamp() *UpdateBuilder { - b.using.timestamp = true +// TTL adds USING TTL clause to the query. +func (b *UpdateBuilder) TTL(d time.Duration) *UpdateBuilder { + b.using.TTL(d) return b } -// TTL sets a USING TTL clause on the query. -func (b *UpdateBuilder) TTL() *UpdateBuilder { - b.using.ttl = true +// TTLNamed adds USING TTL clause to the query with a custom parameter name. +func (b *UpdateBuilder) TTLNamed(name string) *UpdateBuilder { + b.using.TTLNamed(name) + return b +} + +// Timestamp adds USING TIMESTAMP clause to the query. +func (b *UpdateBuilder) Timestamp(t time.Time) *UpdateBuilder { + b.using.Timestamp(t) + return b +} + +// TimestampNamed adds a USING TIMESTAMP clause to the query with a custom +// parameter name. +func (b *UpdateBuilder) TimestampNamed(name string) *UpdateBuilder { + b.using.TimestampNamed(name) return b } diff --git a/qb/update_test.go b/qb/update_test.go index 21a55be..e510893 100644 --- a/qb/update_test.go +++ b/qb/update_test.go @@ -6,6 +6,7 @@ package qb import ( "testing" + "time" "github.com/google/go-cmp/cmp" ) @@ -98,15 +99,25 @@ func TestUpdateBuilder(t *testing.T) { }, // Add TTL { - B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).TTL(), + B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).TTL(time.Second), + S: "UPDATE cycling.cyclist_name USING TTL 1 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).TTLNamed("ttl"), S: "UPDATE cycling.cyclist_name USING TTL ? SET id=?,user_uuid=?,firstname=? WHERE id=? ", - N: []string{"_ttl", "id", "user_uuid", "firstname", "expr"}, + N: []string{"ttl", "id", "user_uuid", "firstname", "expr"}, }, // Add TIMESTAMP { - B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).Timestamp(), + B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).Timestamp(time.Date(2005, 05, 05, 0, 0, 0, 0, time.UTC)), + S: "UPDATE cycling.cyclist_name USING TIMESTAMP 1115251200000000 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).TimestampNamed("ts"), S: "UPDATE cycling.cyclist_name USING TIMESTAMP ? SET id=?,user_uuid=?,firstname=? WHERE id=? ", - N: []string{"_ts", "id", "user_uuid", "firstname", "expr"}, + N: []string{"ts", "id", "user_uuid", "firstname", "expr"}, }, // Add IF EXISTS { diff --git a/qb/using.go b/qb/using.go new file mode 100644 index 0000000..fc1ab13 --- /dev/null +++ b/qb/using.go @@ -0,0 +1,92 @@ +// Copyright (C) 2017 ScyllaDB +// Use of this source code is governed by a ALv2-style +// license that can be found in the LICENSE file. + +package qb + +import ( + "bytes" + "fmt" + "time" +) + +// 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 +} + +type using struct { + ttl int64 + ttlName string + timestamp int64 + timestampName string +} + +func (u *using) TTL(d time.Duration) *using { + u.ttl = TTL(d) + if u.ttl == 0 { + u.ttl = -1 + } + u.timestampName = "" + return u +} + +func (u *using) TTLNamed(name string) *using { + u.ttl = 0 + u.ttlName = name + return u +} + +func (u *using) Timestamp(t time.Time) *using { + u.timestamp = Timestamp(t) + u.timestampName = "" + return u +} + +func (u *using) TimestampNamed(name string) *using { + u.timestamp = 0 + u.timestampName = name + return u +} + +func (u *using) writeCql(cql *bytes.Buffer) (names []string) { + hasTTL := false + + if u.ttl != 0 { + hasTTL = true + if u.ttl == -1 { + u.ttl = 0 + } + cql.WriteString("USING TTL ") + cql.WriteString(fmt.Sprint(u.ttl)) + cql.WriteByte(' ') + } else if u.ttlName != "" { + hasTTL = true + cql.WriteString("USING TTL ? ") + names = append(names, u.ttlName) + } + + if u.timestamp != 0 { + if hasTTL { + cql.WriteString("AND TIMESTAMP ") + } else { + cql.WriteString("USING TIMESTAMP ") + } + cql.WriteString(fmt.Sprint(u.timestamp)) + cql.WriteByte(' ') + } else if u.timestampName != "" { + if hasTTL { + cql.WriteString("AND TIMESTAMP ? ") + } else { + cql.WriteString("USING TIMESTAMP ? ") + } + names = append(names, u.timestampName) + } + + return +} diff --git a/qb/using_test.go b/qb/using_test.go new file mode 100644 index 0000000..96dbe1d --- /dev/null +++ b/qb/using_test.go @@ -0,0 +1,102 @@ +// Copyright (C) 2017 ScyllaDB +// Use of this source code is governed by a ALv2-style +// license that can be found in the LICENSE file. + +package qb + +import ( + "bytes" + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +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") + } +} + +func TestUsing(t *testing.T) { + table := []struct { + B *using + N []string + S string + }{ + // TTL + { + B: new(using).TTL(time.Second), + S: "USING TTL 1 ", + }, + // TTLNamed + { + B: new(using).TTLNamed("ttl"), + S: "USING TTL ? ", + N: []string{"ttl"}, + }, + // Timestamp + { + B: new(using).Timestamp(time.Date(2005, 05, 05, 0, 0, 0, 0, time.UTC)), + S: "USING TIMESTAMP 1115251200000000 ", + }, + // TimestampNamed + { + B: new(using).TimestampNamed("ts"), + S: "USING TIMESTAMP ? ", + N: []string{"ts"}, + }, + // TTL Timestamp + { + B: new(using).TTL(time.Second).Timestamp(time.Date(2005, 05, 05, 0, 0, 0, 0, time.UTC)), + S: "USING TTL 1 AND TIMESTAMP 1115251200000000 ", + }, + // TTLNamed TimestampNamed + { + B: new(using).TTLNamed("ttl").TimestampNamed("ts"), + S: "USING TTL ? AND TIMESTAMP ? ", + N: []string{"ttl", "ts"}, + }, + // TTL TTLNamed + { + B: new(using).TTL(time.Second).TTLNamed("ttl"), + S: "USING TTL ? ", + N: []string{"ttl"}, + }, + // TTLNamed TTL + { + B: new(using).TTLNamed("ttl").TTL(time.Second), + S: "USING TTL 1 ", + }, + // Timestamp TimestampNamed + { + B: new(using).Timestamp(time.Date(2005, 05, 05, 0, 0, 0, 0, time.UTC)).TimestampNamed("ts"), + S: "USING TIMESTAMP ? ", + N: []string{"ts"}, + }, + // TimestampNamed Timestamp + { + B: new(using).TimestampNamed("ts").Timestamp(time.Date(2005, 05, 05, 0, 0, 0, 0, time.UTC)), + S: "USING TIMESTAMP 1115251200000000 ", + }, + } + + for _, test := range table { + buf := bytes.NewBuffer(nil) + names := test.B.writeCql(buf) + stmt := buf.String() + + if diff := cmp.Diff(test.S, stmt); diff != "" { + t.Error(diff) + } + if diff := cmp.Diff(test.N, names); diff != "" { + t.Error(diff) + } + } +} diff --git a/qb/utils.go b/qb/utils.go index 1cd6541..ffa4ab4 100644 --- a/qb/utils.go +++ b/qb/utils.go @@ -6,7 +6,6 @@ package qb import ( "bytes" - "time" ) // placeholders returns a string with count ? placeholders joined with commas. @@ -22,12 +21,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()) -} +type columns []string -// Timestamp converts time to format expected in USING TIMESTAMP clause. -func Timestamp(t time.Time) int64 { - return t.UnixNano() / 1000 +func (cols columns) writeCql(cql *bytes.Buffer) { + for i, c := range cols { + cql.WriteString(c) + if i < len(cols)-1 { + cql.WriteByte(',') + } + } } diff --git a/qb/utils_test.go b/qb/utils_test.go deleted file mode 100644 index ef2048f..0000000 --- a/qb/utils_test.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (C) 2017 ScyllaDB -// Use of this source code is governed by a ALv2-style -// license that can be found in the LICENSE file. - -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") - } -}