From 69dad13e4b411025f3f3cc392fac77135be03e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Matczuk?= Date: Thu, 27 Jul 2017 09:48:33 +0200 Subject: [PATCH] qb: update, delete and performance improvements --- qb/delete.go | 93 +++++++++++++++++++++++++++++++++++++++ qb/delete_test.go | 100 +++++++++++++++++++++++++++++++++++++++++ qb/expr.go | 49 ++++++++++++++++++++- qb/insert.go | 13 +++--- qb/insert_test.go | 2 +- qb/mock.go | 14 ++++++ qb/qb.go | 12 +++-- qb/update.go | 110 ++++++++++++++++++++++++++++++++++++++++++++++ qb/update_test.go | 106 ++++++++++++++++++++++++++++++++++++++++++++ queryx_test.go | 2 +- 10 files changed, 486 insertions(+), 15 deletions(-) create mode 100644 qb/delete.go create mode 100644 qb/delete_test.go create mode 100644 qb/mock.go create mode 100644 qb/update.go create mode 100644 qb/update_test.go diff --git a/qb/delete.go b/qb/delete.go new file mode 100644 index 0000000..745c2f0 --- /dev/null +++ b/qb/delete.go @@ -0,0 +1,93 @@ +package qb + +// DELETE reference: +// http://docs.datastax.com/en/dse/5.1/cql/cql/cql_reference/cql_commands/cqlDelete.html + +import ( + "bytes" + "errors" + "fmt" + "time" +) + +type DeleteBuilder struct { + table string + columns columns + using using + where where + _if _if + exists bool +} + +// Delete returns a new DeleteBuilder with the given table name. +func Delete(table string) *DeleteBuilder { + return &DeleteBuilder{ + table: table, + } +} + +func (b *DeleteBuilder) ToCql() (stmt string, names []string, err error) { + if b.table == "" { + err = errors.New("delete statements must specify a table") + return + } + if len(b.where) == 0 { + err = fmt.Errorf("delete statements must have at least one WHERE clause") + return + } + + cql := bytes.Buffer{} + + cql.WriteString("DELETE ") + if len(b.columns) > 0 { + b.columns.writeCql(&cql) + cql.WriteByte(' ') + } + cql.WriteString("FROM ") + cql.WriteString(b.table) + cql.WriteByte(' ') + + b.using.writeCql(&cql) + + names = append(names, b.where.writeCql(&cql)...) + names = append(names, b._if.writeCql(&cql)...) + + if b.exists { + cql.WriteString("IF EXISTS ") + } + + stmt = cql.String() + return +} + +// From sets the table to be deleted from. +func (b *DeleteBuilder) From(table string) *DeleteBuilder { + b.table = table + return b +} + +// Columns adds delete columns to the query. +func (b *DeleteBuilder) Columns(columns ...string) *DeleteBuilder { + b.columns = append(b.columns, columns...) + return b +} + +func (b *DeleteBuilder) Timestamp(t time.Time) *DeleteBuilder { + b.using.timestamp = t + return b +} + +func (b *DeleteBuilder) Where(e ...expr) *DeleteBuilder { + b.where = append(b.where, e...) + return b +} + +func (b *DeleteBuilder) If(e ...expr) *DeleteBuilder { + b._if = append(b._if, e...) + return b +} + +func (b *DeleteBuilder) Existing() *DeleteBuilder { + b.exists = true + return b +} diff --git a/qb/delete_test.go b/qb/delete_test.go new file mode 100644 index 0000000..7f1823e --- /dev/null +++ b/qb/delete_test.go @@ -0,0 +1,100 @@ +package qb + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +func TestDeleteBuilder(t *testing.T) { + m := mockExpr{ + cql: "expr", + names: []string{"expr"}, + } + + table := []struct { + B *DeleteBuilder + N []string + S string + }{ + // Basic test for delete + { + B: Delete("cycling.cyclist_name").Where(m), + S: "DELETE FROM cycling.cyclist_name WHERE expr ", + N: []string{"expr"}, + }, + // Change table name + { + B: Delete("cycling.cyclist_name").Where(m).From("Foobar"), + S: "DELETE FROM Foobar WHERE expr ", + N: []string{"expr"}, + }, + // Add column + { + B: Delete("cycling.cyclist_name").Where(m).Columns("stars"), + S: "DELETE stars FROM cycling.cyclist_name WHERE expr ", + N: []string{"expr"}, + }, + // Add WHERE + { + B: Delete("cycling.cyclist_name").Where(m).Where(mockExpr{ + cql: "expr_1", + names: []string{"expr_1"}, + }, mockExpr{ + cql: "expr_2", + names: []string{"expr_2"}, + }), + S: "DELETE FROM cycling.cyclist_name WHERE expr AND expr_1 AND expr_2 ", + N: []string{"expr", "expr_1", "expr_2"}, + }, + // Add IF + { + B: Delete("cycling.cyclist_name").Where(m).If(mockExpr{ + cql: "expr_1", + names: []string{"expr_1"}, + }, mockExpr{ + cql: "expr_2", + names: []string{"expr_2"}, + }), + S: "DELETE FROM cycling.cyclist_name WHERE expr IF expr_1 AND expr_2 ", + N: []string{"expr", "expr_1", "expr_2"}, + }, + // Add TIMESTAMP + { + B: Delete("cycling.cyclist_name").Where(m).Timestamp(time.Unix(0, 0).Add(time.Microsecond * 123456789)), + S: "DELETE FROM cycling.cyclist_name USING TIMESTAMP 123456789 WHERE expr ", + N: []string{"expr"}, + }, + // Add IF EXISTS + { + B: Delete("cycling.cyclist_name").Where(m).Existing(), + S: "DELETE FROM cycling.cyclist_name WHERE expr IF EXISTS ", + N: []string{"expr"}, + }, + } + + for _, test := range table { + stmt, names, err := test.B.ToCql() + if err != nil { + t.Error("unexpected error", err) + } + if diff := cmp.Diff(test.S, stmt); diff != "" { + t.Error(diff) + } + if diff := cmp.Diff(test.N, names); diff != "" { + t.Error(diff) + } + } +} + +func BenchmarkDeleteBuilder(b *testing.B) { + m := mockExpr{ + cql: "expr", + names: []string{"expr"}, + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + Delete("cycling.cyclist_name").Columns("id", "user_uuid", "firstname", "stars").Where(m) + } +} diff --git a/qb/expr.go b/qb/expr.go index f525a39..2227e2c 100644 --- a/qb/expr.go +++ b/qb/expr.go @@ -13,12 +13,24 @@ type expr interface { WriteCql(cql *bytes.Buffer) (names []string) } +type columns []string + +func (cols columns) writeCql(cql *bytes.Buffer) (names []string) { + for i, c := range cols { + cql.WriteString(c) + if i < len(cols)-1 { + cql.WriteByte(',') + } + } + return +} + type using struct { timestamp time.Time ttl time.Duration } -func (u using) WriteCql(cql *bytes.Buffer) (names []string) { +func (u using) writeCql(cql *bytes.Buffer) (names []string) { var v []string if !u.timestamp.IsZero() { v = append(v, fmt.Sprint("TIMESTAMP ", u.timestamp.UnixNano()/1000)) @@ -29,8 +41,41 @@ func (u using) WriteCql(cql *bytes.Buffer) (names []string) { if len(v) > 0 { cql.WriteString("USING ") cql.WriteString(strings.Join(v, ",")) - cql.WriteString(" ") + cql.WriteByte(' ') } return } + +type where []expr + +func (w where) writeCql(cql *bytes.Buffer) (names []string) { + if len(w) == 0 { + return + } + + cql.WriteString("WHERE ") + return writeCql(w, cql) +} + +type _if []expr + +func (w _if) writeCql(cql *bytes.Buffer) (names []string) { + if len(w) == 0 { + return + } + + cql.WriteString("IF ") + return writeCql(w, cql) +} + +func writeCql(es []expr, cql *bytes.Buffer) (names []string) { + for i, c := range es { + names = append(names, c.WriteCql(cql)...) + if i < len(es)-1 { + cql.WriteString(" AND ") + } + } + cql.WriteByte(' ') + return +} diff --git a/qb/insert.go b/qb/insert.go index cbba2ea..8a2cb89 100644 --- a/qb/insert.go +++ b/qb/insert.go @@ -6,13 +6,12 @@ package qb import ( "bytes" "errors" - "strings" "time" ) type InsertBuilder struct { table string - columns []string + columns columns unique bool using using } @@ -41,17 +40,17 @@ func (b *InsertBuilder) ToCql() (stmt string, names []string, err error) { cql.WriteString("INTO ") cql.WriteString(b.table) - cql.WriteString(" ") + cql.WriteByte(' ') - cql.WriteString("(") - cql.WriteString(strings.Join(b.columns, ",")) + cql.WriteByte('(') + b.columns.writeCql(&cql) cql.WriteString(") ") cql.WriteString("VALUES (") - cql.WriteString(placeholders(len(b.columns))) + placeholders(&cql, len(b.columns)) cql.WriteString(") ") - b.using.WriteCql(&cql) + b.using.writeCql(&cql) if b.unique { cql.WriteString("IF NOT EXISTS ") diff --git a/qb/insert_test.go b/qb/insert_test.go index 7e5ff83..b00eeff 100644 --- a/qb/insert_test.go +++ b/qb/insert_test.go @@ -69,6 +69,6 @@ func TestInsertBuilder(t *testing.T) { func BenchmarkInsertBuilder(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - Insert("foo").Columns("name", "age", "first", "last").ToCql() + Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname", "stars") } } diff --git a/qb/mock.go b/qb/mock.go new file mode 100644 index 0000000..6524fb0 --- /dev/null +++ b/qb/mock.go @@ -0,0 +1,14 @@ +package qb + +import "bytes" + +type mockExpr struct { + cql string + names []string +} + +func (m mockExpr) WriteCql(cql *bytes.Buffer) (names []string) { + cql.WriteString(m.cql) + names = m.names + return +} diff --git a/qb/qb.go b/qb/qb.go index 5af0189..c89dc5e 100644 --- a/qb/qb.go +++ b/qb/qb.go @@ -1,14 +1,18 @@ package qb import ( - "strings" + "bytes" ) // placeholders returns a string with count ? placeholders joined with commas. -func placeholders(count int) string { +func placeholders(cql *bytes.Buffer, count int) { if count < 1 { - return "" + return } - return strings.Repeat(",?", count)[1:] + for i := 0; i < count-1; i++ { + cql.WriteByte('?') + cql.WriteByte(',') + } + cql.WriteByte('?') } diff --git a/qb/update.go b/qb/update.go new file mode 100644 index 0000000..da82ee2 --- /dev/null +++ b/qb/update.go @@ -0,0 +1,110 @@ +package qb + +import ( + "bytes" + "errors" + "fmt" +) + +// UPDATE reference: +// http://docs.datastax.com/en/dse/5.1/cql/cql/cql_reference/cql_commands/cqlUpdate.html + +import ( + "time" +) + +type UpdateBuilder struct { + table string + using using + columns columns + where where + _if _if + exists bool +} + +// Update returns a new UpdateBuilder with the given table name. +func Update(table string) *UpdateBuilder { + return &UpdateBuilder{ + table: table, + } +} + +func (b *UpdateBuilder) ToCql() (stmt string, names []string, err error) { + if b.table == "" { + err = errors.New("update statements must specify a table") + return + } + if len(b.columns) == 0 { + err = fmt.Errorf("update statements must have at least one SET clause") + return + } + if len(b.where) == 0 { + err = fmt.Errorf("update statements must have at least one WHERE clause") + return + } + + cql := bytes.Buffer{} + + cql.WriteString("UPDATE ") + cql.WriteString(b.table) + cql.WriteByte(' ') + + b.using.writeCql(&cql) + + cql.WriteString("SET ") + for i, c := range b.columns { + cql.WriteString(c) + cql.WriteString("=?") + if i < len(b.columns)-1 { + cql.WriteByte(',') + } + } + names = append(names, b.columns...) + cql.WriteByte(' ') + + names = append(names, b.where.writeCql(&cql)...) + names = append(names, b._if.writeCql(&cql)...) + + if b.exists { + cql.WriteString("IF EXISTS ") + } + + stmt = cql.String() + return +} + +// Table sets the table to be updated. +func (b *UpdateBuilder) Table(table string) *UpdateBuilder { + b.table = table + return b +} + +func (b *UpdateBuilder) Timestamp(t time.Time) *UpdateBuilder { + b.using.timestamp = t + return b +} + +func (b *UpdateBuilder) TTL(d time.Duration) *UpdateBuilder { + b.using.ttl = d + return b +} + +func (b *UpdateBuilder) Set(columns ...string) *UpdateBuilder { + b.columns = append(b.columns, columns...) + return b +} + +func (b *UpdateBuilder) Where(e ...expr) *UpdateBuilder { + b.where = append(b.where, e...) + return b +} + +func (b *UpdateBuilder) If(e ...expr) *UpdateBuilder { + b._if = append(b._if, e...) + return b +} + +func (b *UpdateBuilder) Existing() *UpdateBuilder { + b.exists = true + return b +} diff --git a/qb/update_test.go b/qb/update_test.go new file mode 100644 index 0000000..119893e --- /dev/null +++ b/qb/update_test.go @@ -0,0 +1,106 @@ +package qb + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +func TestUpdateBuilder(t *testing.T) { + m := mockExpr{ + cql: "expr", + names: []string{"expr"}, + } + + table := []struct { + B *UpdateBuilder + N []string + S string + }{ + // Basic test for update + { + B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(m), + S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=? WHERE expr ", + N: []string{"id", "user_uuid", "firstname", "expr"}, + }, + // Change table name + { + B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(m).Table("Foobar"), + S: "UPDATE Foobar SET id=?,user_uuid=?,firstname=? WHERE expr ", + N: []string{"id", "user_uuid", "firstname", "expr"}, + }, + // Add SET + { + B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(m).Set("stars"), + S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=?,stars=? WHERE expr ", + N: []string{"id", "user_uuid", "firstname", "stars", "expr"}, + }, + // Add WHERE + { + B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(m).Where(mockExpr{ + cql: "expr_1", + names: []string{"expr_1"}, + }, mockExpr{ + cql: "expr_2", + names: []string{"expr_2"}, + }), + S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=? WHERE expr AND expr_1 AND expr_2 ", + N: []string{"id", "user_uuid", "firstname", "expr", "expr_1", "expr_2"}, + }, + // Add IF + { + B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(m).If(mockExpr{ + cql: "expr_1", + names: []string{"expr_1"}, + }, mockExpr{ + cql: "expr_2", + names: []string{"expr_2"}, + }), + S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=? WHERE expr IF expr_1 AND expr_2 ", + N: []string{"id", "user_uuid", "firstname", "expr", "expr_1", "expr_2"}, + }, + // Add TTL + { + B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(m).TTL(time.Second * 86400), + S: "UPDATE cycling.cyclist_name USING TTL 86400 SET id=?,user_uuid=?,firstname=? WHERE expr ", + N: []string{"id", "user_uuid", "firstname", "expr"}, + }, + // Add TIMESTAMP + { + B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(m).Timestamp(time.Unix(0, 0).Add(time.Microsecond * 123456789)), + S: "UPDATE cycling.cyclist_name USING TIMESTAMP 123456789 SET id=?,user_uuid=?,firstname=? WHERE expr ", + N: []string{"id", "user_uuid", "firstname", "expr"}, + }, + // Add IF EXISTS + { + B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(m).Existing(), + S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=? WHERE expr IF EXISTS ", + N: []string{"id", "user_uuid", "firstname", "expr"}, + }, + } + + for _, test := range table { + stmt, names, err := test.B.ToCql() + if err != nil { + t.Error("unexpected error", err) + } + if diff := cmp.Diff(test.S, stmt); diff != "" { + t.Error(diff) + } + if diff := cmp.Diff(test.N, names); diff != "" { + t.Error(diff) + } + } +} + +func BenchmarkUpdateBuilder(b *testing.B) { + m := mockExpr{ + cql: "expr", + names: []string{"expr"}, + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname", "stars").Where(m) + } +} diff --git a/queryx_test.go b/queryx_test.go index 7a06042..7b9afa7 100644 --- a/queryx_test.go +++ b/queryx_test.go @@ -59,7 +59,7 @@ func TestCompileQuery(t *testing.T) { } func BenchmarkCompileNamedQuery(b *testing.B) { - q := []byte("INSERT INTO foo (name, age, first, last) VALUES (:name, :age, :first, :last)") + q := []byte("INSERT INTO cycling.cyclist_name (id, user_uuid, firstname, stars) VALUES (:id, :user_uuid, :firstname, :stars)") b.ResetTimer() for i := 0; i < b.N; i++ { CompileNamedQuery(q)