diff --git a/qb/cmp.go b/qb/cmp.go new file mode 100644 index 0000000..e1b8c7c --- /dev/null +++ b/qb/cmp.go @@ -0,0 +1,171 @@ +package qb + +import "bytes" + +type op byte + +const ( + eq op = iota + lt + leq + gt + geq + in + cnt +) + +type Cmp struct { + op op + column string + name string +} + +func (cmp Cmp) writeCql(cql *bytes.Buffer) string { + cql.WriteString(cmp.column) + switch cmp.op { + case eq: + cql.WriteByte('=') + case lt: + cql.WriteByte('<') + case leq: + cql.WriteByte('<') + cql.WriteByte('=') + case gt: + cql.WriteByte('>') + case geq: + cql.WriteByte('>') + cql.WriteByte('=') + case in: + cql.WriteString(" IN ") + case cnt: + cql.WriteString(" CONTAINS ") + } + cql.WriteByte('?') + + return cmp.name +} + +func Eq(column string) Cmp { + return Cmp{ + op: eq, + column: column, + name: column, + } +} + +func EqNamed(column, name string) Cmp { + return Cmp{ + op: eq, + column: column, + name: name, + } +} + +func Lt(column string) Cmp { + return Cmp{ + op: lt, + column: column, + name: column, + } +} + +func LtNamed(column, name string) Cmp { + return Cmp{ + op: lt, + column: column, + name: name, + } +} + +func LtOrEq(column string) Cmp { + return Cmp{ + op: leq, + column: column, + name: column, + } +} + +func LtOrEqNamed(column, name string) Cmp { + return Cmp{ + op: leq, + column: column, + name: name, + } +} + +func Gt(column string) Cmp { + return Cmp{ + op: gt, + column: column, + name: column, + } +} + +func GtNamed(column, name string) Cmp { + return Cmp{ + op: gt, + column: column, + name: name, + } +} + +func GtOrEq(column string) Cmp { + return Cmp{ + op: geq, + column: column, + name: column, + } +} + +func GtOrEqNamed(column, name string) Cmp { + return Cmp{ + op: geq, + column: column, + name: name, + } +} + +func In(column string) Cmp { + return Cmp{ + op: in, + column: column, + name: column, + } +} + +func InNamed(column, name string) Cmp { + return Cmp{ + op: in, + column: column, + name: name, + } +} + +func Contains(column string) Cmp { + return Cmp{ + op: cnt, + column: column, + name: column, + } +} + +func ContainsNamed(column, name string) Cmp { + return Cmp{ + op: cnt, + column: column, + name: name, + } +} + +type cmps []Cmp + +func (cs cmps) writeCql(cql *bytes.Buffer) (names []string) { + for i, c := range cs { + names = append(names, c.writeCql(cql)) + if i < len(cs)-1 { + cql.WriteString(" AND ") + } + } + cql.WriteByte(' ') + return +} diff --git a/qb/cmp_test.go b/qb/cmp_test.go new file mode 100644 index 0000000..d0870eb --- /dev/null +++ b/qb/cmp_test.go @@ -0,0 +1,114 @@ +package qb + +import ( + "bytes" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestCmp(t *testing.T) { + table := []struct { + C Cmp + S string + N string + }{ + { + C: Eq("eq"), + S: "eq=?", + N: "eq", + }, + { + C: EqNamed("eq", "name"), + S: "eq=?", + N: "name", + }, + { + C: Lt("lt"), + S: "lt?", + N: "gt", + }, + { + C: GtNamed("gt", "name"), + S: "gt>?", + N: "name", + }, + { + C: GtOrEq("gt"), + S: "gt>=?", + N: "gt", + }, + { + C: GtOrEqNamed("gt", "name"), + S: "gt>=?", + N: "name", + }, + { + C: In("in"), + S: "in IN ?", + N: "in", + }, + { + C: InNamed("in", "name"), + S: "in IN ?", + N: "name", + }, + { + C: Contains("cnt"), + S: "cnt CONTAINS ?", + N: "cnt", + }, + { + C: ContainsNamed("cnt", "name"), + S: "cnt CONTAINS ?", + N: "name", + }, + } + + buf := bytes.Buffer{} + for _, test := range table { + buf.Reset() + name := test.C.writeCql(&buf) + if diff := cmp.Diff(test.S, buf.String()); diff != "" { + t.Error(diff) + } + if diff := cmp.Diff(test.N, name); diff != "" { + t.Error(diff) + } + } +} + +func BenchmarkCmp(b *testing.B) { + buf := bytes.Buffer{} + b.ResetTimer() + for i := 0; i < b.N; i++ { + buf.Reset() + c := cmps{ + Eq("id"), + Lt("user_uuid"), + LtOrEq("firstname"), + Gt("stars"), + } + c.writeCql(&buf) + } +} diff --git a/qb/delete.go b/qb/delete.go index 745c2f0..aca64b3 100644 --- a/qb/delete.go +++ b/qb/delete.go @@ -77,13 +77,13 @@ func (b *DeleteBuilder) Timestamp(t time.Time) *DeleteBuilder { return b } -func (b *DeleteBuilder) Where(e ...expr) *DeleteBuilder { - b.where = append(b.where, e...) +func (b *DeleteBuilder) Where(w ...Cmp) *DeleteBuilder { + b.where = append(b.where, w...) return b } -func (b *DeleteBuilder) If(e ...expr) *DeleteBuilder { - b._if = append(b._if, e...) +func (b *DeleteBuilder) If(w ...Cmp) *DeleteBuilder { + b._if = append(b._if, w...) return b } diff --git a/qb/delete_test.go b/qb/delete_test.go index 7f1823e..48037db 100644 --- a/qb/delete_test.go +++ b/qb/delete_test.go @@ -8,10 +8,7 @@ import ( ) func TestDeleteBuilder(t *testing.T) { - m := mockExpr{ - cql: "expr", - names: []string{"expr"}, - } + w := EqNamed("id", "expr") table := []struct { B *DeleteBuilder @@ -20,56 +17,44 @@ func TestDeleteBuilder(t *testing.T) { }{ // Basic test for delete { - B: Delete("cycling.cyclist_name").Where(m), - S: "DELETE FROM cycling.cyclist_name WHERE expr ", + B: Delete("cycling.cyclist_name").Where(w), + S: "DELETE FROM cycling.cyclist_name WHERE id=? ", N: []string{"expr"}, }, // Change table name { - B: Delete("cycling.cyclist_name").Where(m).From("Foobar"), - S: "DELETE FROM Foobar WHERE expr ", + B: Delete("cycling.cyclist_name").Where(w).From("Foobar"), + S: "DELETE FROM Foobar WHERE id=? ", N: []string{"expr"}, }, // Add column { - B: Delete("cycling.cyclist_name").Where(m).Columns("stars"), - S: "DELETE stars FROM cycling.cyclist_name WHERE expr ", + B: Delete("cycling.cyclist_name").Where(w).Columns("stars"), + S: "DELETE stars FROM cycling.cyclist_name WHERE id=? ", 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"}, + B: Delete("cycling.cyclist_name").Where(w, Gt("firstname")), + S: "DELETE FROM cycling.cyclist_name WHERE id=? AND firstname>? ", + N: []string{"expr", "firstname"}, }, // 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"}, + B: Delete("cycling.cyclist_name").Where(w).If(Gt("firstname")), + S: "DELETE FROM cycling.cyclist_name WHERE id=? IF firstname>? ", + N: []string{"expr", "firstname"}, }, // 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 ", + 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"}, }, // Add IF EXISTS { - B: Delete("cycling.cyclist_name").Where(m).Existing(), - S: "DELETE FROM cycling.cyclist_name WHERE expr IF EXISTS ", + B: Delete("cycling.cyclist_name").Where(w).Existing(), + S: "DELETE FROM cycling.cyclist_name WHERE id=? IF EXISTS ", N: []string{"expr"}, }, } @@ -89,12 +74,8 @@ func TestDeleteBuilder(t *testing.T) { } 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) + Delete("cycling.cyclist_name").Columns("id", "user_uuid", "firstname", "stars").Where(Eq("id")) } } diff --git a/qb/expr.go b/qb/expr.go index ae8fe5b..e9d98b4 100644 --- a/qb/expr.go +++ b/qb/expr.go @@ -3,16 +3,9 @@ package qb import ( "bytes" "fmt" - "strings" "time" ) -type expr interface { - // WriteCql writes a CQL representation of the expr to a buffer and returns - // slice of parameter names. - WriteCql(cql *bytes.Buffer) (names []string) -} - type columns []string func (cols columns) writeCql(cql *bytes.Buffer) { @@ -30,21 +23,26 @@ type using struct { } func (u using) writeCql(cql *bytes.Buffer) { - var v []string - if !u.timestamp.IsZero() { - v = append(v, fmt.Sprint("TIMESTAMP ", u.timestamp.UnixNano()/1000)) + ts := !u.timestamp.IsZero() + + if ts { + cql.WriteString("USING TIMESTAMP ") + cql.WriteString(fmt.Sprint(u.timestamp.UnixNano() / 1000)) + cql.WriteByte(' ') } + if u.ttl != 0 { - v = append(v, fmt.Sprint("TTL ", int(u.ttl.Seconds()))) - } - if len(v) > 0 { - cql.WriteString("USING ") - cql.WriteString(strings.Join(v, ",")) + if ts { + cql.WriteString("AND TTL ") + } else { + cql.WriteString("USING TTL ") + } + cql.WriteString(fmt.Sprint(int(u.ttl.Seconds()))) cql.WriteByte(' ') } } -type where []expr +type where cmps func (w where) writeCql(cql *bytes.Buffer) (names []string) { if len(w) == 0 { @@ -52,10 +50,10 @@ func (w where) writeCql(cql *bytes.Buffer) (names []string) { } cql.WriteString("WHERE ") - return writeCql(w, cql) + return cmps(w).writeCql(cql) } -type _if []expr +type _if cmps func (w _if) writeCql(cql *bytes.Buffer) (names []string) { if len(w) == 0 { @@ -63,16 +61,5 @@ func (w _if) writeCql(cql *bytes.Buffer) (names []string) { } 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 + return cmps(w).writeCql(cql) } diff --git a/qb/mock.go b/qb/mock.go deleted file mode 100644 index 6524fb0..0000000 --- a/qb/mock.go +++ /dev/null @@ -1,14 +0,0 @@ -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/select.go b/qb/select.go index fdd15fe..4e69786 100644 --- a/qb/select.go +++ b/qb/select.go @@ -120,8 +120,8 @@ func (b *SelectBuilder) Distinct(columns... string) *SelectBuilder { return b } -func (b *SelectBuilder) Where(e ...expr) *SelectBuilder { - b.where = append(b.where, e...) +func (b *SelectBuilder) Where(w ...Cmp) *SelectBuilder { + b.where = append(b.where, w...) return b } diff --git a/qb/select_test.go b/qb/select_test.go index bd0487e..a5fc761 100644 --- a/qb/select_test.go +++ b/qb/select_test.go @@ -7,10 +7,7 @@ import ( ) func TestSelectBuilder(t *testing.T) { - m := mockExpr{ - cql: "expr", - names: []string{"expr"}, - } + w := EqNamed("id", "expr") table := []struct { B *SelectBuilder @@ -39,15 +36,9 @@ func TestSelectBuilder(t *testing.T) { }, // Add WHERE { - B: Select("cycling.cyclist_name").Where(m).Where(mockExpr{ - cql: "expr_1", - names: []string{"expr_1"}, - }, mockExpr{ - cql: "expr_2", - names: []string{"expr_2"}, - }), - S: "SELECT * FROM cycling.cyclist_name WHERE expr AND expr_1 AND expr_2 ", - N: []string{"expr", "expr_1", "expr_2"}, + B: Select("cycling.cyclist_name").Where(w, Gt("firstname")), + S: "SELECT * FROM cycling.cyclist_name WHERE id=? AND firstname>? ", + N: []string{"expr", "firstname"}, }, // Add GROUP BY { @@ -56,26 +47,26 @@ func TestSelectBuilder(t *testing.T) { }, // Add ORDER BY { - B: Select("cycling.cyclist_name").Where(m).OrderBy("firstname", ASC), - S: "SELECT * FROM cycling.cyclist_name WHERE expr ORDER BY firstname ASC ", + B: Select("cycling.cyclist_name").Where(w).OrderBy("firstname", ASC), + S: "SELECT * FROM cycling.cyclist_name WHERE id=? ORDER BY firstname ASC ", N: []string{"expr"}, }, // Add LIMIT { - B: Select("cycling.cyclist_name").Where(m).Limit(10), - S: "SELECT * FROM cycling.cyclist_name WHERE expr LIMIT 10 ", + B: Select("cycling.cyclist_name").Where(w).Limit(10), + S: "SELECT * FROM cycling.cyclist_name WHERE id=? LIMIT 10 ", N: []string{"expr"}, }, // Add PER PARTITION LIMIT { - B: Select("cycling.cyclist_name").Where(m).LimitPerPartition(10), - S: "SELECT * FROM cycling.cyclist_name WHERE expr PER PARTITION LIMIT 10 ", + B: Select("cycling.cyclist_name").Where(w).LimitPerPartition(10), + S: "SELECT * FROM cycling.cyclist_name WHERE id=? PER PARTITION LIMIT 10 ", N: []string{"expr"}, }, // Add ALLOW FILTERING { - B: Select("cycling.cyclist_name").Where(m).AllowFiltering(), - S: "SELECT * FROM cycling.cyclist_name WHERE expr ALLOW FILTERING ", + B: Select("cycling.cyclist_name").Where(w).AllowFiltering(), + S: "SELECT * FROM cycling.cyclist_name WHERE id=? ALLOW FILTERING ", N: []string{"expr"}, }, } @@ -95,12 +86,8 @@ func TestSelectBuilder(t *testing.T) { } func BenchmarkSelectBuilder(b *testing.B) { - m := mockExpr{ - cql: "expr", - names: []string{"expr"}, - } b.ResetTimer() for i := 0; i < b.N; i++ { - Select("cycling.cyclist_name").Columns("id", "user_uuid", "firstname", "stars").Where(m) + Select("cycling.cyclist_name").Columns("id", "user_uuid", "firstname", "stars").Where(Eq("id")) } } diff --git a/qb/update.go b/qb/update.go index da82ee2..ebd99b9 100644 --- a/qb/update.go +++ b/qb/update.go @@ -94,13 +94,13 @@ func (b *UpdateBuilder) Set(columns ...string) *UpdateBuilder { return b } -func (b *UpdateBuilder) Where(e ...expr) *UpdateBuilder { - b.where = append(b.where, e...) +func (b *UpdateBuilder) Where(w ...Cmp) *UpdateBuilder { + b.where = append(b.where, w...) return b } -func (b *UpdateBuilder) If(e ...expr) *UpdateBuilder { - b._if = append(b._if, e...) +func (b *UpdateBuilder) If(w ...Cmp) *UpdateBuilder { + b._if = append(b._if, w...) return b } diff --git a/qb/update_test.go b/qb/update_test.go index 119893e..81ae220 100644 --- a/qb/update_test.go +++ b/qb/update_test.go @@ -8,10 +8,7 @@ import ( ) func TestUpdateBuilder(t *testing.T) { - m := mockExpr{ - cql: "expr", - names: []string{"expr"}, - } + w := EqNamed("id", "expr") table := []struct { B *UpdateBuilder @@ -20,62 +17,50 @@ func TestUpdateBuilder(t *testing.T) { }{ // 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 ", + B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w), + S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=? WHERE id=? ", 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 ", + B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).Table("Foobar"), + S: "UPDATE Foobar SET id=?,user_uuid=?,firstname=? WHERE id=? ", 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 ", + B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).Set("stars"), + S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=?,stars=? WHERE id=? ", 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"}, + B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w, Gt("firstname")), + S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=? WHERE id=? AND firstname>? ", + N: []string{"id", "user_uuid", "firstname", "expr", "firstname"}, }, // 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"}, + B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).If(Gt("firstname")), + S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=? WHERE id=? IF firstname>? ", + N: []string{"id", "user_uuid", "firstname", "expr", "firstname"}, }, // 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 ", + 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"}, }, // 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 ", + 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"}, }, // 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 ", + B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).Existing(), + S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=? WHERE id=? IF EXISTS ", N: []string{"id", "user_uuid", "firstname", "expr"}, }, } @@ -95,12 +80,8 @@ func TestUpdateBuilder(t *testing.T) { } 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) + Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname", "stars").Where(Eq("id")) } }