diff --git a/README.md b/README.md index 85b5100..64d87e1 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ hood it uses `sqlx/reflectx` package so `sqlx` models will also work with `gocql * Builders for `SELECT`, `INSERT`, `UPDATE` `DELETE` and `BATCH` * Queries with named parameters (:identifier) support +* Functions support * Binding parameters form struct or map * Scanning results into structs and slices * Automatic query releasing @@ -85,38 +86,6 @@ type Person struct { t.Log(people) // [{Patricia Citizen [patricia.citzen@gocqlx_test.com patricia1.citzen@gocqlx_test.com]} {Igy Citizen [igy.citzen@gocqlx_test.com]} {Ian Citizen [ian.citzen@gocqlx_test.com]}] } - -// Batch insert two rows in a single query, advanced struct binding. -{ - i := qb.Insert("gocqlx_test.person").Columns("first_name", "last_name", "email") - - stmt, names := qb.Batch(). - AddWithPrefix("a", i). - AddWithPrefix("b", i). - ToCql() - - batch := struct { - A Person - B Person - }{ - A: Person{ - "Igy", - "Citizen", - []string{"igy.citzen@gocqlx_test.com"}, - }, - B: Person{ - "Ian", - "Citizen", - []string{"ian.citzen@gocqlx_test.com"}, - }, - } - - q := gocqlx.Query(session.Query(stmt), names).BindStruct(&batch) - - if err := q.ExecRelease(); err != nil { - t.Fatal(err) - } -} ``` See more examples in [example_test.go](https://github.com/scylladb/gocqlx/blob/master/example_test.go). diff --git a/example_test.go b/example_test.go index 2da97c5..6ebfb4d 100644 --- a/example_test.go +++ b/example_test.go @@ -89,6 +89,22 @@ func TestExample(t *testing.T) { } } + // Advanced update, adding and removing elements to collections and counters. + { + stmt, names := qb.Update("gocqlx_test.person"). + AddNamed("email", "new_email"). + Where(qb.Eq("first_name"), qb.Eq("last_name")). + ToCql() + + q := gocqlx.Query(session.Query(stmt), names).BindStructMap(p, qb.M{ + "new_email": []string{"patricia2.citzen@gocqlx_test.com", "patricia3.citzen@gocqlx_test.com"}, + }) + + if err := q.ExecRelease(); err != nil { + t.Fatal(err) + } + } + // Batch insert two rows in a single query, advanced struct binding. { i := qb.Insert("gocqlx_test.person").Columns("first_name", "last_name", "email") @@ -137,7 +153,7 @@ func TestExample(t *testing.T) { } t.Log(p) - // {Patricia Citizen [patricia.citzen@gocqlx_test.com patricia1.citzen@gocqlx_test.com]} + // {Patricia Citizen [patricia.citzen@gocqlx_test.com patricia1.citzen@gocqlx_test.com patricia2.citzen@gocqlx_test.com patricia3.citzen@gocqlx_test.com]} } // Select, load all the results into a slice. @@ -156,7 +172,7 @@ func TestExample(t *testing.T) { } t.Log(people) - // [{Patricia Citizen [patricia.citzen@gocqlx_test.com patricia1.citzen@gocqlx_test.com]} {Igy Citizen [igy.citzen@gocqlx_test.com]} {Ian Citizen [ian.citzen@gocqlx_test.com]}] + // [{Ian Citizen [ian.citzen@gocqlx_test.com]} {Igy Citizen [igy.citzen@gocqlx_test.com]} {Patricia Citizen [patricia.citzen@gocqlx_test.com patricia1.citzen@gocqlx_test.com patricia2.citzen@gocqlx_test.com patricia3.citzen@gocqlx_test.com]}] } // Easy token based pagination. @@ -168,6 +184,7 @@ func TestExample(t *testing.T) { } stmt, names := qb.Select("gocqlx_test.person"). + Columns("first_name"). Where(qb.Token("first_name").Gt()). Limit(10). ToCql() @@ -180,7 +197,7 @@ func TestExample(t *testing.T) { } t.Log(people) - // [{Patricia Citizen [patricia.citzen@gocqlx_test.com patricia1.citzen@gocqlx_test.com]} {Igy Citizen [igy.citzen@gocqlx_test.com]}] + // [{Patricia []} {Igy []}] } // Named query compilation. diff --git a/qb/cmp.go b/qb/cmp.go index ba59617..e1efb3b 100644 --- a/qb/cmp.go +++ b/qb/cmp.go @@ -28,49 +28,8 @@ const ( type Cmp struct { op op column string - fn string - names []string -} - -// Func wraps comparator value with a custom function, fn is a function name, -// names are function arguments' bind names. For instance function: -// -// CREATE FUNCTION somefunction(somearg int, anotherarg text) -// -// can be used like this: -// -// stmt, names := qb.Select("table"). -// Where(qb.Eq("t").Func("somefunction", "somearg", "anotherarg")). -// ToCql() -// -// q := gocqlx.Query(session.Query(stmt), names).BindMap(qb.M{ -// "somearg": 1, -// "anotherarg": "text", -// }) -func (c Cmp) Func(fn string, names ...string) Cmp { - c.fn = fn - c.names = names - return c -} - -// MinTimeuuid sets minTimeuuid(?) compare value. -func (c Cmp) MinTimeuuid(name string) Cmp { - return c.Func("minTimeuuid", name) -} - -// MaxTimeuuid sets maxTimeuuid(?) compare value. -func (c Cmp) MaxTimeuuid(name string) Cmp { - return c.Func("maxTimeuuid", name) -} - -// Now sets now() compare value. -func (c Cmp) Now() Cmp { - return c.Func("now") -} - -// Token sets Token(?,?...) compare value. -func (c Cmp) Token(names ...string) Cmp { - return c.Func("token", names...) + name string + fn *Func } func (c Cmp) writeCql(cql *bytes.Buffer) (names []string) { @@ -94,19 +53,15 @@ func (c Cmp) writeCql(cql *bytes.Buffer) (names []string) { cql.WriteString(" CONTAINS ") } - if c.fn == "" { + if c.fn != nil { + names = append(names, c.fn.writeCql(cql)...) + } else { cql.WriteByte('?') - if c.names == nil { + if c.name == "" { names = append(names, c.column) } else { - names = append(names, c.names...) + names = append(names, c.name) } - } else { - cql.WriteString(c.fn) - cql.WriteByte('(') - placeholders(cql, len(c.names)) - cql.WriteByte(')') - names = append(names, c.names...) } return @@ -125,7 +80,16 @@ func EqNamed(column, name string) Cmp { return Cmp{ op: eq, column: column, - names: []string{name}, + name: name, + } +} + +// EqFunc produces column=someFunc(?...). +func EqFunc(column string, fn *Func) Cmp { + return Cmp{ + op: eq, + column: column, + fn: fn, } } @@ -142,7 +106,16 @@ func LtNamed(column, name string) Cmp { return Cmp{ op: lt, column: column, - names: []string{name}, + name: name, + } +} + +// LtFunc produces columnsomeFunc(?...). +func GtFunc(column string, fn *Func) Cmp { + return Cmp{ + op: gt, + column: column, + fn: fn, } } @@ -193,7 +184,16 @@ func GtOrEqNamed(column, name string) Cmp { return Cmp{ op: geq, column: column, - names: []string{name}, + name: name, + } +} + +// GtOrEqFunc produces column>=someFunc(?...). +func GtOrEqFunc(column string, fn *Func) Cmp { + return Cmp{ + op: geq, + column: column, + fn: fn, } } @@ -210,7 +210,7 @@ func InNamed(column, name string) Cmp { return Cmp{ op: in, column: column, - names: []string{name}, + name: name, } } @@ -227,7 +227,7 @@ func ContainsNamed(column, name string) Cmp { return Cmp{ op: cnt, column: column, - names: []string{name}, + name: name, } } diff --git a/qb/cmp_test.go b/qb/cmp_test.go index 28a5912..464d765 100644 --- a/qb/cmp_test.go +++ b/qb/cmp_test.go @@ -93,29 +93,24 @@ func TestCmp(t *testing.T) { // Functions { - C: Eq("eq").Func("fn", "arg0", "arg1"), + C: EqFunc("eq", Fn("fn", "arg0", "arg1")), S: "eq=fn(?,?)", N: []string{"arg0", "arg1"}, }, { - C: Eq("eq").MaxTimeuuid("arg0"), + C: EqFunc("eq", MaxTimeuuid("arg0")), S: "eq=maxTimeuuid(?)", N: []string{"arg0"}, }, { - C: Eq("eq").MinTimeuuid("arg0"), + C: EqFunc("eq", MinTimeuuid("arg0")), S: "eq=minTimeuuid(?)", N: []string{"arg0"}, }, { - C: Eq("eq").Now(), + C: EqFunc("eq", Now()), S: "eq=now()", }, - { - C: Eq("eq").Token("arg0", "arg1"), - S: "eq=token(?,?)", - N: []string{"arg0", "arg1"}, - }, } buf := bytes.Buffer{} diff --git a/qb/func.go b/qb/func.go new file mode 100644 index 0000000..d3ff29a --- /dev/null +++ b/qb/func.go @@ -0,0 +1,51 @@ +// 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" + +// Functions reference: +// https://cassandra.apache.org/doc/latest/cql/functions.html + +// Func is a custom database function invocation that can be use in a comparator +// or update statement. +type Func struct { + // function name + Name string + // name of the function parameters + ParamNames []string +} + +func (f *Func) writeCql(cql *bytes.Buffer) (names []string) { + cql.WriteString(f.Name) + cql.WriteByte('(') + placeholders(cql, len(f.ParamNames)) + cql.WriteByte(')') + names = append(names, f.ParamNames...) + return +} + +// Fn creates Func. +func Fn(name string, paramNames ...string) *Func { + return &Func{ + Name: name, + ParamNames: paramNames, + } +} + +// MinTimeuuid produces minTimeuuid(?). +func MinTimeuuid(name string) *Func { + return Fn("minTimeuuid", name) +} + +// MaxTimeuuid produces maxTimeuuid(?). +func MaxTimeuuid(name string) *Func { + return Fn("maxTimeuuid", name) +} + +// Now produces now(). +func Now() *Func { + return Fn("now") +} diff --git a/qb/token.go b/qb/token.go index 566b06c..acfd4f4 100644 --- a/qb/token.go +++ b/qb/token.go @@ -9,110 +9,72 @@ import ( "strings" ) +// TokenBuilder helps implement pagination using token function. +type TokenBuilder []string + // Token creates a new TokenBuilder. func Token(columns ...string) TokenBuilder { return TokenBuilder(columns) } -// TokenBuilder helps implement pagination using token function. -type TokenBuilder []string - // Eq produces token(column)=token(?). func (t TokenBuilder) Eq() Cmp { - return Cmp{ - op: eq, - fn: "token", - column: fmt.Sprint("token(", strings.Join(t, ","), ")"), - names: t, - } + return t.cmp(eq, nil) } // EqNamed produces token(column)=token(?) with a custom parameter name. func (t TokenBuilder) EqNamed(names ...string) Cmp { - return Cmp{ - op: eq, - fn: "token", - column: fmt.Sprint("token(", strings.Join(t, ","), ")"), - names: names, - } + return t.cmp(eq, names) } // Lt produces token(column)token(?). func (t TokenBuilder) Gt() Cmp { - return Cmp{ - op: gt, - fn: "token", - column: fmt.Sprint("token(", strings.Join(t, ","), ")"), - names: t, - } + return t.cmp(gt, nil) } // GtNamed produces token(column)>token(?) with a custom parameter name. func (t TokenBuilder) GtNamed(names ...string) Cmp { - return Cmp{ - op: gt, - fn: "token", - column: fmt.Sprint("token(", strings.Join(t, ","), ")"), - names: names, - } + return t.cmp(gt, names) } // GtOrEq produces token(column)>=token(?). func (t TokenBuilder) GtOrEq() Cmp { - return Cmp{ - op: geq, - fn: "token", - column: fmt.Sprint("token(", strings.Join(t, ","), ")"), - names: t, - } + return t.cmp(geq, nil) } // GtOrEqNamed produces token(column)>=token(?) with a custom parameter name. func (t TokenBuilder) GtOrEqNamed(names ...string) Cmp { + return t.cmp(geq, names) +} + +func (t TokenBuilder) cmp(op op, names []string) Cmp { + s := names + if s == nil { + s = t + } return Cmp{ - op: geq, - fn: "token", + op: op, column: fmt.Sprint("token(", strings.Join(t, ","), ")"), - names: names, + fn: Fn("token", s...), } } diff --git a/qb/update.go b/qb/update.go index 1b9ff1f..ad1a695 100644 --- a/qb/update.go +++ b/qb/update.go @@ -9,16 +9,41 @@ package qb import ( "bytes" + "fmt" ) +// assignment specifies an assignment in a set operation. +type assignment struct { + column string + name string + expr bool + fn *Func +} + +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 +} + // UpdateBuilder builds CQL UPDATE statements. type UpdateBuilder struct { - table string - using using - columns columns - where where - _if _if - exists bool + table string + using using + assignments []assignment + where where + _if _if + exists bool } // Update returns a new UpdateBuilder with the given table name. @@ -39,14 +64,12 @@ func (b *UpdateBuilder) ToCql() (stmt string, names []string) { names = append(names, b.using.writeCql(&cql)...) cql.WriteString("SET ") - for i, c := range b.columns { - cql.WriteString(c) - cql.WriteString("=?") - if i < len(b.columns)-1 { + for i, a := range b.assignments { + names = append(names, a.writeCql(&cql)...) + if i < len(b.assignments)-1 { cql.WriteByte(',') } } - names = append(names, b.columns...) cql.WriteByte(' ') names = append(names, b.where.writeCql(&cql)...) @@ -80,7 +103,51 @@ func (b *UpdateBuilder) TTL() *UpdateBuilder { // Set adds SET clauses to the query. func (b *UpdateBuilder) Set(columns ...string) *UpdateBuilder { - b.columns = append(b.columns, columns...) + for _, c := range columns { + b.assignments = append(b.assignments, assignment{ + column: c, + name: c, + }) + } + + 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}) + return b +} + +// Add adds SET column=column+? clauses to the query. +func (b *UpdateBuilder) Add(column string) *UpdateBuilder { + return b.AddNamed(column, column) +} + +// AddNamed adds SET column=column+? clauses to the query with a custom +// parameter name. +func (b *UpdateBuilder) AddNamed(column, name string) *UpdateBuilder { + b.assignments = append(b.assignments, assignment{ + column: fmt.Sprint(column, "=", column, "+?"), + name: name, + expr: true, + }) + return b +} + +// Remove adds SET column=column-? clauses to the query. +func (b *UpdateBuilder) Remove(column string) *UpdateBuilder { + return b.RemoveNamed(column, column) +} + +// RemoveNamed adds SET column=column-? clauses to the query with a custom +// parameter name. +func (b *UpdateBuilder) RemoveNamed(column, name string) *UpdateBuilder { + b.assignments = append(b.assignments, assignment{ + column: fmt.Sprint(column, "=", column, "-?"), + name: name, + expr: true, + }) return b } diff --git a/qb/update_test.go b/qb/update_test.go index dd954a6..ba93dbe 100644 --- a/qb/update_test.go +++ b/qb/update_test.go @@ -36,6 +36,36 @@ 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 SetFunc + { + B: Update("cycling.cyclist_name").SetFunc("user_uuid", Fn("someFunc", "param_0", "param_1")).Where(w).Set("stars"), + S: "UPDATE cycling.cyclist_name SET user_uuid=someFunc(?,?),stars=? WHERE id=? ", + N: []string{"param_0", "param_1", "stars", "expr"}, + }, + // Add SET Add + { + B: Update("cycling.cyclist_name").Add("total").Where(w), + S: "UPDATE cycling.cyclist_name SET total=total+? WHERE id=? ", + N: []string{"total", "expr"}, + }, + // Add SET AddNamed + { + B: Update("cycling.cyclist_name").AddNamed("total", "inc").Where(w), + S: "UPDATE cycling.cyclist_name SET total=total+? WHERE id=? ", + N: []string{"inc", "expr"}, + }, + // Add SET Remove + { + B: Update("cycling.cyclist_name").Remove("total").Where(w), + S: "UPDATE cycling.cyclist_name SET total=total-? WHERE id=? ", + N: []string{"total", "expr"}, + }, + // Add SET RemoveNamed + { + B: Update("cycling.cyclist_name").RemoveNamed("total", "dec").Where(w), + S: "UPDATE cycling.cyclist_name SET total=total-? WHERE id=? ", + N: []string{"dec", "expr"}, + }, // Add WHERE { B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w, Gt("firstname")),