diff --git a/qb/expr.go b/qb/expr.go new file mode 100644 index 0000000..f525a39 --- /dev/null +++ b/qb/expr.go @@ -0,0 +1,36 @@ +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 using struct { + timestamp time.Time + ttl time.Duration +} + +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)) + } + 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, ",")) + cql.WriteString(" ") + } + + return +} diff --git a/qb/insert.go b/qb/insert.go new file mode 100644 index 0000000..cbba2ea --- /dev/null +++ b/qb/insert.go @@ -0,0 +1,87 @@ +package qb + +// INSERT reference: +// http://docs.datastax.com/en/dse/5.1/cql/cql/cql_reference/cql_commands/cqlInsert.html + +import ( + "bytes" + "errors" + "strings" + "time" +) + +type InsertBuilder struct { + table string + columns []string + unique bool + using using +} + +// Insert returns a new InsertBuilder with the given table name. +func Insert(table string) *InsertBuilder { + return &InsertBuilder{ + table: table, + } +} + +func (b *InsertBuilder) ToCql() (stmt string, names []string, err error) { + if b.table == "" { + err = errors.New("insert statements must specify a table") + return + } + + if len(b.columns) == 0 { + err = errors.New("insert statements must specify columns") + return + } + + cql := bytes.Buffer{} + + cql.WriteString("INSERT ") + + cql.WriteString("INTO ") + cql.WriteString(b.table) + cql.WriteString(" ") + + cql.WriteString("(") + cql.WriteString(strings.Join(b.columns, ",")) + cql.WriteString(") ") + + cql.WriteString("VALUES (") + cql.WriteString(placeholders(len(b.columns))) + cql.WriteString(") ") + + b.using.WriteCql(&cql) + + if b.unique { + cql.WriteString("IF NOT EXISTS ") + } + + stmt, names = cql.String(), b.columns + return +} + +func (b *InsertBuilder) Into(table string) *InsertBuilder { + b.table = table + return b +} + +func (b *InsertBuilder) Columns(columns ...string) *InsertBuilder { + b.columns = append(b.columns, columns...) + return b +} + +func (b *InsertBuilder) Unique() *InsertBuilder { + b.unique = true + return b +} + +func (b *InsertBuilder) Timestamp(t time.Time) *InsertBuilder { + b.using.timestamp = t + return b +} + +func (b *InsertBuilder) TTL(d time.Duration) *InsertBuilder { + b.using.ttl = d + return b +} diff --git a/qb/insert_test.go b/qb/insert_test.go new file mode 100644 index 0000000..7e5ff83 --- /dev/null +++ b/qb/insert_test.go @@ -0,0 +1,74 @@ +package qb + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +func TestInsertBuilder(t *testing.T) { + table := []struct { + B *InsertBuilder + N []string + S string + }{ + + // Basic test for insert + { + B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname"), + S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) ", + N: []string{"id", "user_uuid", "firstname"}, + }, + // Change table name + { + B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").Into("Foobar"), + S: "INSERT INTO Foobar (id,user_uuid,firstname) VALUES (?,?,?) ", + N: []string{"id", "user_uuid", "firstname"}, + }, + // Add columns + { + B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").Columns("stars"), + S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname,stars) VALUES (?,?,?,?) ", + N: []string{"id", "user_uuid", "firstname", "stars"}, + }, + // 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"}, + }, + // 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"}, + }, + // Add IF NOT EXISTS + { + B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").Unique(), + S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) IF NOT EXISTS ", + N: []string{"id", "user_uuid", "firstname"}, + }, + } + + 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 BenchmarkInsertBuilder(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + Insert("foo").Columns("name", "age", "first", "last").ToCql() + } +} diff --git a/qb/qb.go b/qb/qb.go new file mode 100644 index 0000000..5af0189 --- /dev/null +++ b/qb/qb.go @@ -0,0 +1,14 @@ +package qb + +import ( + "strings" +) + +// placeholders returns a string with count ? placeholders joined with commas. +func placeholders(count int) string { + if count < 1 { + return "" + } + + return strings.Repeat(",?", count)[1:] +} diff --git a/queryx_test.go b/queryx_test.go index f0be41c..7a06042 100644 --- a/queryx_test.go +++ b/queryx_test.go @@ -12,7 +12,7 @@ func TestCompileQuery(t *testing.T) { Q, R string V []string }{ - // basic test for named parameters, invalid char ',' terminating + // Basic test for named parameters, invalid char ',' terminating { Q: `INSERT INTO foo (a,b,c,d) VALUES (:name, :age, :first, :last)`, R: `INSERT INTO foo (a,b,c,d) VALUES (?, ?, ?, ?)`, @@ -59,10 +59,10 @@ func TestCompileQuery(t *testing.T) { } func BenchmarkCompileNamedQuery(b *testing.B) { - q1 := `INSERT INTO foo (a, b, c, d) VALUES (:name, :age, :first, :last)` + q := []byte("INSERT INTO foo (name, age, first, last) VALUES (:name, :age, :first, :last)") b.ResetTimer() for i := 0; i < b.N; i++ { - CompileNamedQuery([]byte(q1)) + CompileNamedQuery(q) } }