From 12d360a0c3f9bd9feeb9b00d7f198579ac88c4d5 Mon Sep 17 00:00:00 2001 From: Josh Giles Date: Sun, 29 Oct 2017 13:18:49 -0400 Subject: [PATCH] Support for literals in INSERT and UPDATE and comparisons The 'value' interface represents a CQL value for use in a comparison, update, or intitialization operation. A consistent interface for this allows us to easily support specifying default-named, custom-named, literal, and evaluated-function values in all these contexts. Parameters to Func should probably also be values to support full composition, but that would be a breaking change because Func's properties are exposed. The value interface could itself be exposed if we wanted to allow clients to pass their own values to SetValue, etc, but for now it is a package-internal abstraction. BLA --- qb/cmp.go | 114 ++++++++++++++++++++++++++++++++++------------ qb/cmp_test.go | 30 ++++++++++++ qb/insert.go | 57 +++++++++++++++++++++-- qb/insert_test.go | 12 +++++ qb/token.go | 2 +- qb/update.go | 85 +++++++++++++++++++++++----------- qb/update_test.go | 18 ++++++++ qb/value.go | 31 +++++++++++++ 8 files changed, 286 insertions(+), 63 deletions(-) create mode 100644 qb/value.go 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 +}