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
This commit is contained in:
committed by
Michał Matczuk
parent
9fa5432a65
commit
12d360a0c3
114
qb/cmp.go
114
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 column<literal and does not add a parameter to the query.
|
||||
func LtLit(column, literal string) Cmp {
|
||||
return Cmp{
|
||||
op: lt,
|
||||
column: column,
|
||||
value: lit(literal),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +119,7 @@ func LtFunc(column string, fn *Func) Cmp {
|
||||
return Cmp{
|
||||
op: lt,
|
||||
column: column,
|
||||
fn: fn,
|
||||
value: fn,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +128,7 @@ func LtOrEq(column string) Cmp {
|
||||
return Cmp{
|
||||
op: leq,
|
||||
column: column,
|
||||
value: param(column),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +137,16 @@ func LtOrEqNamed(column, name string) Cmp {
|
||||
return Cmp{
|
||||
op: leq,
|
||||
column: column,
|
||||
name: name,
|
||||
value: param(name),
|
||||
}
|
||||
}
|
||||
|
||||
// LtOrEqLit produces column<=literal and does not add a parameter to the query.
|
||||
func LtOrEqLit(column, literal string) Cmp {
|
||||
return Cmp{
|
||||
op: leq,
|
||||
column: column,
|
||||
value: lit(literal),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +155,7 @@ func LtOrEqFunc(column string, fn *Func) Cmp {
|
||||
return Cmp{
|
||||
op: leq,
|
||||
column: column,
|
||||
fn: fn,
|
||||
value: fn,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +164,7 @@ func Gt(column string) Cmp {
|
||||
return Cmp{
|
||||
op: gt,
|
||||
column: column,
|
||||
value: param(column),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +173,16 @@ func GtNamed(column, name string) Cmp {
|
||||
return Cmp{
|
||||
op: gt,
|
||||
column: column,
|
||||
name: name,
|
||||
value: param(name),
|
||||
}
|
||||
}
|
||||
|
||||
// GtLit produces column>literal 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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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: "lt<litval",
|
||||
},
|
||||
{
|
||||
C: LtOrEqLit("lt", "litval"),
|
||||
S: "lt<=litval",
|
||||
},
|
||||
{
|
||||
C: GtLit("gt", "litval"),
|
||||
S: "gt>litval",
|
||||
},
|
||||
{
|
||||
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")),
|
||||
|
||||
57
qb/insert.go
57
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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...),
|
||||
}
|
||||
}
|
||||
|
||||
81
qb/update.go
81
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
|
||||
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.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
|
||||
}
|
||||
|
||||
@@ -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")),
|
||||
|
||||
31
qb/value.go
Normal file
31
qb/value.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user