diff --git a/qb/cmp.go b/qb/cmp.go index e1efb3b..6cc68aa 100644 --- a/qb/cmp.go +++ b/qb/cmp.go @@ -4,9 +4,6 @@ package qb -// Functions reference: -// http://cassandra.apache.org/doc/latest/cql/functions.html - import ( "bytes" ) @@ -28,8 +25,7 @@ const ( type Cmp struct { op op column string - name string - fn *Func + value value } func (c Cmp) writeCql(cql *bytes.Buffer) (names []string) { @@ -52,19 +48,7 @@ func (c Cmp) writeCql(cql *bytes.Buffer) (names []string) { case cnt: cql.WriteString(" CONTAINS ") } - - if c.fn != nil { - names = append(names, c.fn.writeCql(cql)...) - } else { - cql.WriteByte('?') - if c.name == "" { - names = append(names, c.column) - } else { - names = append(names, c.name) - } - } - - return + return c.value.writeCql(cql) } // Eq produces column=?. @@ -72,6 +56,7 @@ func Eq(column string) Cmp { return Cmp{ op: eq, column: column, + value: param(column), } } @@ -80,7 +65,16 @@ func EqNamed(column, name string) Cmp { return Cmp{ op: eq, column: column, - name: name, + value: param(name), + } +} + +// EqLit produces column=literal and does not add a parameter to the query. +func EqLit(column, literal string) Cmp { + return Cmp{ + op: eq, + column: column, + value: lit(literal), } } @@ -89,7 +83,7 @@ func EqFunc(column string, fn *Func) Cmp { return Cmp{ op: eq, column: column, - fn: fn, + value: fn, } } @@ -98,6 +92,7 @@ func Lt(column string) Cmp { return Cmp{ op: lt, column: column, + value: param(column), } } @@ -106,7 +101,16 @@ func LtNamed(column, name string) Cmp { return Cmp{ op: lt, column: column, - name: name, + value: param(name), + } +} + +// LtLit produces columnliteral and does not add a parameter to the query. +func GtLit(column, literal string) Cmp { + return Cmp{ + op: gt, + column: column, + value: lit(literal), } } @@ -167,7 +191,7 @@ func GtFunc(column string, fn *Func) Cmp { return Cmp{ op: gt, column: column, - fn: fn, + value: fn, } } @@ -176,6 +200,7 @@ func GtOrEq(column string) Cmp { return Cmp{ op: geq, column: column, + value: param(column), } } @@ -184,7 +209,16 @@ func GtOrEqNamed(column, name string) Cmp { return Cmp{ op: geq, column: column, - name: name, + value: param(name), + } +} + +// GtOrEqLit produces column>=literal and does not add a parameter to the query. +func GtOrEqLit(column, literal string) Cmp { + return Cmp{ + op: geq, + column: column, + value: lit(literal), } } @@ -193,7 +227,7 @@ func GtOrEqFunc(column string, fn *Func) Cmp { return Cmp{ op: geq, column: column, - fn: fn, + value: fn, } } @@ -202,6 +236,7 @@ func In(column string) Cmp { return Cmp{ op: in, column: column, + value: param(column), } } @@ -210,7 +245,16 @@ func InNamed(column, name string) Cmp { return Cmp{ op: in, column: column, - name: name, + value: param(name), + } +} + +// InLit produces column IN literal and does not add a parameter to the query. +func InLit(column, literal string) Cmp { + return Cmp{ + op: in, + column: column, + value: lit(literal), } } @@ -219,6 +263,7 @@ func Contains(column string) Cmp { return Cmp{ op: cnt, column: column, + value: param(column), } } @@ -227,7 +272,16 @@ func ContainsNamed(column, name string) Cmp { return Cmp{ op: cnt, column: column, - name: name, + value: param(name), + } +} + +// ContainsLit produces column CONTAINS literal and does not add a parameter to the query. +func ContainsLit(column, literal string) Cmp { + return Cmp{ + op: cnt, + column: column, + value: lit(literal), } } diff --git a/qb/cmp_test.go b/qb/cmp_test.go index 464d765..a46ee23 100644 --- a/qb/cmp_test.go +++ b/qb/cmp_test.go @@ -91,6 +91,36 @@ func TestCmp(t *testing.T) { N: []string{"name"}, }, + // Literals + { + C: EqLit("eq", "litval"), + S: "eq=litval", + }, + { + C: LtLit("lt", "litval"), + S: "ltlitval", + }, + { + C: GtOrEqLit("gt", "litval"), + S: "gt>=litval", + }, + { + C: InLit("in", "litval"), + S: "in IN litval", + }, + { + C: ContainsLit("cnt", "litval"), + S: "cnt CONTAINS litval", + }, + // Functions { C: EqFunc("eq", Fn("fn", "arg0", "arg1")), diff --git a/qb/insert.go b/qb/insert.go index a5331df..67e2340 100644 --- a/qb/insert.go +++ b/qb/insert.go @@ -11,10 +11,16 @@ import ( "bytes" ) +// initializer specifies an value for a column in an insert operation. +type initializer struct { + column string + value value +} + // InsertBuilder builds CQL INSERT statements. type InsertBuilder struct { table string - columns columns + columns []initializer unique bool using using } @@ -37,12 +43,21 @@ func (b *InsertBuilder) ToCql() (stmt string, names []string) { cql.WriteByte(' ') cql.WriteByte('(') - b.columns.writeCql(&cql) - names = append(names, b.columns...) + for i, c := range b.columns { + cql.WriteString(c.column) + if i < len(b.columns)-1 { + cql.WriteByte(',') + } + } cql.WriteString(") ") cql.WriteString("VALUES (") - placeholders(&cql, len(b.columns)) + for i, c := range b.columns { + names = append(names, c.value.writeCql(&cql)...) + if i < len(b.columns)-1 { + cql.WriteByte(',') + } + } cql.WriteString(") ") if b.unique { @@ -62,7 +77,39 @@ func (b *InsertBuilder) Into(table string) *InsertBuilder { // Columns adds insert columns to the query. func (b *InsertBuilder) Columns(columns ...string) *InsertBuilder { - b.columns = append(b.columns, columns...) + for _, c := range columns { + b.columns = append(b.columns, initializer{ + column: c, + value: param(c), + }) + } + return b +} + +// NamedColumn adds an insert column with a custom parameter name. +func (b *InsertBuilder) NamedColumn(column, name string) *InsertBuilder { + b.columns = append(b.columns, initializer{ + column: column, + value: param(name), + }) + return b +} + +// LitColumn adds an insert column with a literal value to the query. +func (b *InsertBuilder) LitColumn(column, literal string) *InsertBuilder { + b.columns = append(b.columns, initializer{ + column: column, + value: lit(literal), + }) + return b +} + +// FuncColumn adds an insert column initialized by evaluating a CQL function. +func (b *InsertBuilder) FuncColumn(column string, fn *Func) *InsertBuilder { + b.columns = append(b.columns, initializer{ + column: column, + value: fn, + }) return b } diff --git a/qb/insert_test.go b/qb/insert_test.go index f133b5d..b0cd24b 100644 --- a/qb/insert_test.go +++ b/qb/insert_test.go @@ -35,6 +35,18 @@ func TestInsertBuilder(t *testing.T) { S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname,stars) VALUES (?,?,?,?) ", N: []string{"id", "user_uuid", "firstname", "stars"}, }, + // Add a named column + { + B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").NamedColumn("stars", "stars_name"), + S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname,stars) VALUES (?,?,?,?) ", + N: []string{"id", "user_uuid", "firstname", "stars_name"}, + }, + // Add a literal column + { + B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").LitColumn("stars", "stars_lit"), + S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname,stars) VALUES (?,?,?,stars_lit) ", + N: []string{"id", "user_uuid", "firstname"}, + }, // Add TTL { B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").TTL(), diff --git a/qb/token.go b/qb/token.go index acfd4f4..fc68f53 100644 --- a/qb/token.go +++ b/qb/token.go @@ -75,6 +75,6 @@ func (t TokenBuilder) cmp(op op, names []string) Cmp { return Cmp{ op: op, column: fmt.Sprint("token(", strings.Join(t, ","), ")"), - fn: Fn("token", s...), + value: Fn("token", s...), } } diff --git a/qb/update.go b/qb/update.go index ad1a695..c8236ca 100644 --- a/qb/update.go +++ b/qb/update.go @@ -9,31 +9,20 @@ package qb import ( "bytes" - "fmt" ) // assignment specifies an assignment in a set operation. type assignment struct { - column string - name string - expr bool - fn *Func + column string + value value + valuePrefix string // Tbe value prefix to use for add/remove operations. } func (a assignment) writeCql(cql *bytes.Buffer) (names []string) { cql.WriteString(a.column) - switch { - case a.expr: - names = append(names, a.name) - case a.fn != nil: - cql.WriteByte('=') - names = append(names, a.fn.writeCql(cql)...) - default: - cql.WriteByte('=') - cql.WriteByte('?') - names = append(names, a.name) - } - return + cql.WriteByte('=') + cql.WriteString(a.valuePrefix) + return a.value.writeCql(cql) } // UpdateBuilder builds CQL UPDATE statements. @@ -106,47 +95,89 @@ func (b *UpdateBuilder) Set(columns ...string) *UpdateBuilder { for _, c := range columns { b.assignments = append(b.assignments, assignment{ column: c, - name: c, + value: param(c), }) } return b } +// SetNamed adds SET column=? clause to the query with a custom parameter name. +func (b *UpdateBuilder) SetNamed(column, name string) *UpdateBuilder { + b.assignments = append( + b.assignments, assignment{column: column, value: param(name)}) + return b +} + +// SetLit adds SET column=literal clause to the query. +func (b *UpdateBuilder) SetLit(column, literal string) *UpdateBuilder { + b.assignments = append( + b.assignments, assignment{column: column, value: lit(literal)}) + return b +} + // SetFunc adds SET column=someFunc(?...) clause to the query. func (b *UpdateBuilder) SetFunc(column string, fn *Func) *UpdateBuilder { - b.assignments = append(b.assignments, assignment{column: column, fn: fn}) + b.assignments = append(b.assignments, assignment{column: column, value: fn}) return b } // Add adds SET column=column+? clauses to the query. func (b *UpdateBuilder) Add(column string) *UpdateBuilder { - return b.AddNamed(column, column) + return b.addValue(column, param(column)) } // AddNamed adds SET column=column+? clauses to the query with a custom // parameter name. func (b *UpdateBuilder) AddNamed(column, name string) *UpdateBuilder { + return b.addValue(column, param(name)) +} + +// AddLit adds SET column=column+literal clauses to the query. +func (b *UpdateBuilder) AddLit(column, literal string) *UpdateBuilder { + return b.addValue(column, lit(literal)) +} + +// AddFunc adds SET column=column+someFunc(?...) clauses to the query. +func (b *UpdateBuilder) AddFunc(column string, fn *Func) *UpdateBuilder { + return b.addValue(column, fn) +} + +func (b *UpdateBuilder) addValue(column string, value value) *UpdateBuilder { b.assignments = append(b.assignments, assignment{ - column: fmt.Sprint(column, "=", column, "+?"), - name: name, - expr: true, + column: column, + value: value, + valuePrefix: column + "+", }) return b } // Remove adds SET column=column-? clauses to the query. func (b *UpdateBuilder) Remove(column string) *UpdateBuilder { - return b.RemoveNamed(column, column) + return b.removeValue(column, param(column)) } // RemoveNamed adds SET column=column-? clauses to the query with a custom // parameter name. func (b *UpdateBuilder) RemoveNamed(column, name string) *UpdateBuilder { + return b.removeValue(column, param(name)) +} + +// RemoveLit adds SET column=column-literal clauses to the query. +func (b *UpdateBuilder) RemoveLit(column, literal string) *UpdateBuilder { + return b.removeValue(column, lit(literal)) +} + +// RemoveFunc adds SET column=column-someFunc(?...) clauses to the query. +func (b *UpdateBuilder) RemoveFunc(column string, fn *Func) *UpdateBuilder { + return b.removeValue(column, fn) +} + +func (b *UpdateBuilder) removeValue(column string, value value) *UpdateBuilder { b.assignments = append(b.assignments, assignment{ - column: fmt.Sprint(column, "=", column, "-?"), - name: name, - expr: true, + column: column, + value: value, + valuePrefix: column + "-", }) return b } diff --git a/qb/update_test.go b/qb/update_test.go index ba93dbe..70eeaf6 100644 --- a/qb/update_test.go +++ b/qb/update_test.go @@ -36,6 +36,12 @@ func TestUpdateBuilder(t *testing.T) { S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=?,stars=? WHERE id=? ", N: []string{"id", "user_uuid", "firstname", "stars", "expr"}, }, + // Add SET literal + { + B: Update("cycling.cyclist_name").SetLit("user_uuid", "literal_uuid").Where(w).Set("stars"), + S: "UPDATE cycling.cyclist_name SET user_uuid=literal_uuid,stars=? WHERE id=? ", + N: []string{"stars", "expr"}, + }, // Add SET SetFunc { B: Update("cycling.cyclist_name").SetFunc("user_uuid", Fn("someFunc", "param_0", "param_1")).Where(w).Set("stars"), @@ -54,6 +60,12 @@ func TestUpdateBuilder(t *testing.T) { S: "UPDATE cycling.cyclist_name SET total=total+? WHERE id=? ", N: []string{"inc", "expr"}, }, + // Add SET AddLit + { + B: Update("cycling.cyclist_name").AddLit("total", "1").Where(w), + S: "UPDATE cycling.cyclist_name SET total=total+1 WHERE id=? ", + N: []string{"expr"}, + }, // Add SET Remove { B: Update("cycling.cyclist_name").Remove("total").Where(w), @@ -66,6 +78,12 @@ func TestUpdateBuilder(t *testing.T) { S: "UPDATE cycling.cyclist_name SET total=total-? WHERE id=? ", N: []string{"dec", "expr"}, }, + // Add SET RemoveLit + { + B: Update("cycling.cyclist_name").RemoveLit("total", "1").Where(w), + S: "UPDATE cycling.cyclist_name SET total=total-1 WHERE id=? ", + N: []string{"expr"}, + }, // Add WHERE { B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w, Gt("firstname")), diff --git a/qb/value.go b/qb/value.go new file mode 100644 index 0000000..c66daff --- /dev/null +++ b/qb/value.go @@ -0,0 +1,31 @@ +// 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" + +// value is a CQL value expression for use in an initializer, assignment, +// or comparison. +type value interface { + // writeCql writes the bytes for this value to the buffer and returns the + // list of names of parameters which need substitution. + writeCql(cql *bytes.Buffer) (names []string) +} + +// param is a named CQL '?' parameter. +type param string + +func (p param) writeCql(cql *bytes.Buffer) (names []string) { + cql.WriteByte('?') + return []string{string(p)} +} + +// lit is a literal CQL value. +type lit string + +func (l lit) writeCql(cql *bytes.Buffer) (names []string) { + cql.WriteString(string(l)) + return nil +}