Merge pull request #1 from scylladb/mmt/qb

Query builder
This commit is contained in:
Michał Matczuk
2017-07-31 15:34:16 +02:00
committed by GitHub
22 changed files with 1607 additions and 127 deletions

113
README.md
View File

@@ -1,6 +1,7 @@
# gocqlx [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/scylladb/gocqlx) [![Go Report Card](https://goreportcard.com/badge/github.com/scylladb/gocqlx)](https://goreportcard.com/report/github.com/scylladb/gocqlx) [![Build Status](https://travis-ci.org/scylladb/gocqlx.svg?branch=master)](https://travis-ci.org/scylladb/gocqlx)
Package `gocqlx` is a `gocql` extension, similar to what `sqlx` is to `database/sql`.
Package `gocqlx` is a Scylla / Cassandra productivity toolkit for `gocql`, it's
similar to what `sqlx` is to `database/sql`.
It contains wrappers over `gocql` types that provide convenience methods which
are useful in the development of database driven applications. Under the
@@ -12,43 +13,97 @@ hood it uses `sqlx/reflectx` package so `sqlx` models will also work with `gocql
## Features
Read all rows into a slice.
Fast, boilerplate free and flexible `SELECTS`, `INSERTS`, `UPDATES` and `DELETES`.
```go
var v []*Item
if err := gocqlx.Select(&v, session.Query(`SELECT * FROM items WHERE id = ?`, id)); err != nil {
log.Fatal("select failed", err)
type Person struct {
FirstName string // no need to add `db:"first_name"` etc.
LastName string
Email []string
}
```
Read a single row into a struct.
```go
var v Item
if err := gocqlx.Get(&v, session.Query(`SELECT * FROM items WHERE id = ?`, id)); err != nil {
log.Fatal("get failed", err)
p := &Person{
"Patricia",
"Citizen",
[]string{"patricia.citzen@gocqlx_test.com"},
}
```
Bind named query parameters from a struct or map.
```go
stmt, names, err := gocqlx.CompileNamedQuery([]byte("INSERT INTO items (id, name) VALUES (:id, :name)"))
if err != nil {
t.Fatal("compile:", err)
}
q := gocqlx.Queryx{
Query: session.Query(stmt),
Names: names,
}
if err := q.BindStruct(&Item{"id", "name"}); err != nil {
// Insert
{
q := Query(qb.Insert("person").Columns("first_name", "last_name", "email").ToCql())
if err := q.BindStruct(p); err != nil {
t.Fatal("bind:", err)
}
if err := q.Query.Exec(); err != nil {
log.Fatal("get failed", err)
mustExec(q.Query)
}
// Update
{
p.Email = append(p.Email, "patricia1.citzen@gocqlx_test.com")
q := Query(qb.Update("person").Set("email").Where(qb.Eq("first_name"), qb.Eq("last_name")).ToCql())
if err := q.BindStruct(p); err != nil {
t.Fatal("bind:", err)
}
mustExec(q.Query)
}
// Select
{
q := Query(qb.Select("person").Where(qb.In("first_name")).ToCql())
m := map[string]interface{}{
"first_name": []string{"Patricia", "John"},
}
if err := q.BindMap(m); err != nil {
t.Fatal("bind:", err)
}
var people []Person
if err := gocqlx.Select(&people, q.Query); err != nil {
t.Fatal(err)
}
t.Log(people)
// [{Patricia Citizen [patricia.citzen@gocqlx_test.com patricia1.citzen@gocqlx_test.com]} {John Doe [johndoeDNE@gmail.net]}]
}
```
## Example
For more details see [example test](https://github.com/scylladb/gocqlx/blob/master/example_test.go).
See [example test](https://github.com/scylladb/gocqlx/blob/master/example_test.go).
## Performance
Gocqlx is fast, below is a benchmark result comparing `gocqlx` to raw `gocql` on
my machine, see the benchmark [here](https://github.com/scylladb/gocqlx/blob/master/benchmark_test.go).
For query binding gocqlx is faster as it does not require parameter rewriting
while binding. For get and insert the performance is comparable.
```
BenchmarkE2EGocqlInsert-4 1000 1580420 ns/op 2624 B/op 59 allocs/op
BenchmarkE2EGocqlxInsert-4 2000 648769 ns/op 1557 B/op 34 allocs/op
BenchmarkE2EGocqlGet-4 3000 664618 ns/op 1086 B/op 29 allocs/op
BenchmarkE2EGocqlxGet-4 3000 631415 ns/op 1440 B/op 32 allocs/op
BenchmarkE2EGocqlSelect-4 50 35646283 ns/op 34072 B/op 922 allocs/op
BenchmarkE2EGocqlxSelect-4 50 37128897 ns/op 28304 B/op 933 allocs/op
```
Gocqlx comes with automatic snake case support for field names and does not
require manual tagging. This is also fast, below is a comparison to
`strings.ToLower` function (`sqlx` default).
```
BenchmarkSnakeCase-4 10000000 124 ns/op 32 B/op 2 allocs/op
BenchmarkToLower-4 100000000 57.9 ns/op 0 B/op 0 allocs/op
```
Building queries is fast and low on allocations too.
```
BenchmarkCmp-4 3000000 464 ns/op 112 B/op 3 allocs/op
BenchmarkDeleteBuilder-4 10000000 214 ns/op 112 B/op 2 allocs/op
BenchmarkInsertBuilder-4 20000000 103 ns/op 64 B/op 1 allocs/op
BenchmarkSelectBuilder-4 10000000 214 ns/op 112 B/op 2 allocs/op
BenchmarkUpdateBuilder-4 10000000 212 ns/op 112 B/op 2 allocs/op
```
Enyoy!

239
benchmark_test.go Normal file
View File

@@ -0,0 +1,239 @@
// +build integration
package gocqlx_test
import (
"encoding/json"
"os"
"testing"
"github.com/gocql/gocql"
"github.com/scylladb/gocqlx"
"github.com/scylladb/gocqlx/qb"
)
type benchPerson struct {
ID int `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email []string `json:"email"`
Gender string `json:"gender"`
IPAddress string `json:"ip_address"`
}
var benchPersonSchema = `
CREATE TABLE IF NOT EXISTS gocqlx_test.bench_person (
id int,
first_name text,
last_name text,
email list<text>,
gender text,
ip_address text,
PRIMARY KEY(id)
)`
var benchPersonCols = []string{"id", "first_name", "last_name", "email", "gender", "ip_address"}
func loadFixtures() []*benchPerson {
f, err := os.Open("test-fixtures/people.json")
if err != nil {
panic(err)
}
defer func() {
if err := f.Close(); err != nil {
panic(err)
}
}()
var v []*benchPerson
if err := json.NewDecoder(f).Decode(&v); err != nil {
panic(err)
}
return v
}
//
// Insert
//
// BenchmarkE2EGocqlInsert performs standard insert.
func BenchmarkE2EGocqlInsert(b *testing.B) {
people := loadFixtures()
session := createSession(b)
defer session.Close()
if err := createTable(session, benchPersonSchema); err != nil {
b.Fatal(err)
}
stmt, _ := qb.Insert("gocqlx_test.bench_person").Columns(benchPersonCols...).ToCql()
q := session.Query(stmt)
defer q.Release()
b.ResetTimer()
for i := 0; i < b.N; i++ {
// prepare
p := people[i%len(people)]
if err := q.Bind(p.ID, p.FirstName, p.LastName, p.Email, p.Gender, p.IPAddress).Exec(); err != nil {
b.Fatal(err)
}
// insert
if err := q.Exec(); err != nil {
b.Fatal(err)
}
}
}
// BenchmarkE2EGocqlInsert performs insert with struct binding.
func BenchmarkE2EGocqlxInsert(b *testing.B) {
people := loadFixtures()
session := createSession(b)
defer session.Close()
if err := createTable(session, benchPersonSchema); err != nil {
b.Fatal(err)
}
stmt, names := qb.Insert("gocqlx_test.bench_person").Columns(benchPersonCols...).ToCql()
q := gocqlx.Query(session.Query(stmt), names)
defer q.Release()
b.ResetTimer()
for i := 0; i < b.N; i++ {
// prepare
p := people[i%len(people)]
if err := q.BindStruct(p); err != nil {
b.Fatal("bind:", err)
}
// insert
if err := q.Exec(); err != nil {
b.Fatal(err)
}
}
}
//
// Get
//
// BenchmarkE2EGocqlGet performs standard scan.
func BenchmarkE2EGocqlGet(b *testing.B) {
people := loadFixtures()
session := createSession(b)
defer session.Close()
initTable(b, session, people)
stmt, _ := qb.Select("gocqlx_test.bench_person").Columns(benchPersonCols...).Where(qb.Eq("id")).Limit(1).ToCql()
var p benchPerson
b.ResetTimer()
for i := 0; i < b.N; i++ {
// prepare
q := session.Query(stmt)
q.Bind(people[i%len(people)].ID)
// scan
if err := q.Scan(&p.ID, &p.FirstName, &p.LastName, &p.Email, &p.Gender, &p.IPAddress); err != nil {
b.Fatal(err)
}
// release
q.Release()
}
}
// BenchmarkE2EGocqlxGet performs get.
func BenchmarkE2EGocqlxGet(b *testing.B) {
people := loadFixtures()
session := createSession(b)
defer session.Close()
initTable(b, session, people)
stmt, _ := qb.Select("gocqlx_test.bench_person").Columns(benchPersonCols...).Where(qb.Eq("id")).Limit(1).ToCql()
var p benchPerson
b.ResetTimer()
for i := 0; i < b.N; i++ {
// prepare
q := session.Query(stmt)
q.Bind(people[i%len(people)].ID)
// get
gocqlx.Get(&p, q)
}
}
//
// Select
//
// BenchmarkE2EGocqlSelect performs standard loop scan.
func BenchmarkE2EGocqlSelect(b *testing.B) {
people := loadFixtures()
session := createSession(b)
defer session.Close()
initTable(b, session, people)
stmt, _ := qb.Select("gocqlx_test.bench_person").Columns(benchPersonCols...).Limit(100).ToCql()
b.ResetTimer()
for i := 0; i < b.N; i++ {
// prepare
v := make([]*benchPerson, 100)
q := session.Query(stmt)
i := q.Iter()
// loop scan
p := new(benchPerson)
for i.Scan(&p.ID, &p.FirstName, &p.LastName, &p.Email, &p.Gender, &p.IPAddress) {
v = append(v, p)
p = new(benchPerson)
}
if err := i.Close(); err != nil {
b.Fatal(err)
}
// release
q.Release()
}
}
// BenchmarkE2EGocqlSelect performs select.
func BenchmarkE2EGocqlxSelect(b *testing.B) {
people := loadFixtures()
session := createSession(b)
defer session.Close()
initTable(b, session, people)
stmt, _ := qb.Select("gocqlx_test.bench_person").Columns(benchPersonCols...).Limit(100).ToCql()
b.ResetTimer()
for i := 0; i < b.N; i++ {
// prepare
q := session.Query(stmt)
var v []*benchPerson
// select
if err := gocqlx.Select(&v, q); err != nil {
b.Fatal(err)
}
}
}
func initTable(b *testing.B, session *gocql.Session, people []*benchPerson) {
if err := createTable(session, benchPersonSchema); err != nil {
b.Fatal(err)
}
stmt, names := qb.Insert("gocqlx_test.bench_person").Columns(benchPersonCols...).ToCql()
q := gocqlx.Query(session.Query(stmt), names)
for _, p := range people {
if err := q.BindStruct(p); err != nil {
b.Fatal(err)
}
if err := q.Exec(); err != nil {
b.Fatal(err)
}
}
}

View File

@@ -72,7 +72,6 @@ func createKeyspace(tb testing.TB, cluster *gocql.ClusterConfig, keyspace string
panic(err)
}
defer session.Close()
defer tb.Log("closing keyspace session")
err = createTable(session, `DROP KEYSPACE IF EXISTS `+keyspace)
if err != nil {

35
doc.go
View File

@@ -1,38 +1,7 @@
// Package gocqlx is a gocql extension, similar to what sqlx is to database/sql.
// Package gocqlx is a Scylla / Cassandra productivity toolkit for `gocql`, it's
// similar to what `sqlx` is to `database/sql`.
//
// It contains wrappers over gocql types that provide convenience methods which
// are useful in the development of database driven applications. Under the
// hood it uses sqlx/reflectx package so sqlx models will also work with gocqlx.
//
// Example, read all rows into a slice
//
// var v []*Item
// if err := gocqlx.Select(&v, session.Query(`SELECT * FROM items WHERE id = ?`, id)); err != nil {
// log.Fatal("select failed", err)
// }
//
// Example, read a single row into a struct
//
// var v Item
// if err := gocqlx.Get(&v, session.Query(`SELECT * FROM items WHERE id = ?`, id)); err != nil {
// log.Fatal("get failed", err)
// }
//
// Example, bind named query parameters from a struct or map
//
// stmt, names, err := gocqlx.CompileNamedQuery([]byte("INSERT INTO items (id, name) VALUES (:id, :name)"))
// if err != nil {
// t.Fatal("compile:", err)
// }
// q := gocqlx.Queryx{
// Query: session.Query(stmt),
// Names: names,
// }
// if err := q.BindStruct(&Item{"id", "name"}); err != nil {
// t.Fatal("bind:", err)
// }
// if err := q.Query.Exec(); err != nil {
// log.Fatal("get failed", err)
// }
//
package gocqlx

View File

@@ -3,15 +3,15 @@
package gocqlx_test
import (
"fmt"
"testing"
"github.com/gocql/gocql"
"github.com/scylladb/gocqlx"
"github.com/scylladb/gocqlx/qb"
)
var personSchema = `
CREATE TABLE gocqlx_test.person (
CREATE TABLE IF NOT EXISTS gocqlx_test.person (
first_name text,
last_name text,
email list<text>,
@@ -19,7 +19,7 @@ CREATE TABLE gocqlx_test.person (
)`
var placeSchema = `
CREATE TABLE gocqlx_test.place (
CREATE TABLE IF NOT EXISTS gocqlx_test.place (
country text,
city text,
code int,
@@ -27,7 +27,7 @@ CREATE TABLE gocqlx_test.place (
)`
// Field names are converted to camel case by default, no need to add
// `db:"first_name"`, if you want to disable a filed add `db:"-"` tag
// `db:"first_name"`, if you want to disable a filed add `db:"-"` tag.
type Person struct {
FirstName string
LastName string
@@ -46,13 +46,15 @@ func TestExample(t *testing.T) {
mustExec := func(q *gocql.Query) {
if err := q.Exec(); err != nil {
t.Fatal("insert:", q, err)
t.Fatal("query:", q, err)
}
}
// Fill person table
// Fill person table.
{
mustExec(session.Query(personSchema))
if err := createTable(session, personSchema); err != nil {
t.Fatal("create table:", err)
}
q := session.Query("INSERT INTO gocqlx_test.person (first_name, last_name, email) VALUES (?, ?, ?)")
mustExec(q.Bind("Jason", "Moiron", []string{"jmoiron@jmoiron.net"}))
@@ -60,9 +62,11 @@ func TestExample(t *testing.T) {
q.Release()
}
// Fill place table
// Fill place table.
{
mustExec(session.Query(placeSchema))
if err := createTable(session, placeSchema); err != nil {
t.Fatal("create table:", err)
}
q := session.Query("INSERT INTO gocqlx_test.place (country, city, code) VALUES (?, ?, ?)")
mustExec(q.Bind("United States", "New York", 1))
@@ -71,72 +75,131 @@ func TestExample(t *testing.T) {
q.Release()
}
// Query the database, storing results in a []Person (wrapped in []interface{})
// Query the database, storing results in a []Person (wrapped in []interface{}).
{
people := []Person{}
if err := gocqlx.Select(&people, session.Query("SELECT * FROM person")); err != nil {
var people []Person
if err := gocqlx.Select(&people, session.Query("SELECT * FROM gocqlx_test.person")); err != nil {
t.Fatal("select:", err)
}
t.Log(people)
fmt.Printf("%#v\n%#v\n", people[0], people[1])
// gocqlx_test.Person{FirstName:"John", LastName:"Doe", Email:[]string{"johndoeDNE@gmail.net"}}
// gocqlx_test.Person{FirstName:"Jason", LastName:"Moiron", Email:[]string{"jmoiron@jmoiron.net"}}
// [{John Doe [johndoeDNE@gmail.net]} {Jason Moiron [jmoiron@jmoiron.net]}]
}
// Get a single result, a la QueryRow
// Get a single result.
{
var jason Person
if err := gocqlx.Get(&jason, session.Query("SELECT * FROM person WHERE first_name=?", "Jason")); err != nil {
if err := gocqlx.Get(&jason, session.Query("SELECT * FROM gocqlx_test.person WHERE first_name=?", "Jason")); err != nil {
t.Fatal("get:", err)
}
fmt.Printf("%#v\n", jason)
// gocqlx_test.Person{FirstName:"Jason", LastName:"Moiron", Email:[]string{"jmoiron@jmoiron.net"}}
t.Log(jason)
// Jason Moiron [jmoiron@jmoiron.net]}
}
// Loop through rows using only one struct
// Loop through rows using only one struct.
{
var place Place
iter := gocqlx.Iter(session.Query("SELECT * FROM place"))
iter := gocqlx.Iter(session.Query("SELECT * FROM gocqlx_test.place"))
for iter.StructScan(&place) {
fmt.Printf("%#v\n", place)
t.Log(place)
}
if err := iter.Close(); err != nil {
t.Fatal("iter:", err)
}
iter.ReleaseQuery()
// gocqlx_test.Place{Country:"Hong Kong", City:"", TelCode:852}
// gocqlx_test.Place{Country:"United States", City:"New York", TelCode:1}
// gocqlx_test.Place{Country:"Singapore", City:"", TelCode:65}
// {Hong Kong 852}
// {United States New York 1}
// {Singapore 65}
}
// Named queries, using `:name` as the bindvar
// Query builder, using DSL to build queries, using `:name` as the bindvar.
{
stmt, names, err := gocqlx.CompileNamedQuery([]byte("INSERT INTO person (first_name, last_name, email) VALUES (:first_name, :last_name, :email)"))
// helper function for creating session queries
Query := gocqlx.SessionQuery(session)
p := &Person{
"Patricia",
"Citizen",
[]string{"patricia.citzen@gocqlx_test.com"},
}
// Insert
{
q := Query(qb.Insert("gocqlx_test.person").Columns("first_name", "last_name", "email").ToCql())
if err := q.BindStruct(p); err != nil {
t.Fatal("bind:", err)
}
mustExec(q.Query)
}
// Update
{
p.Email = append(p.Email, "patricia1.citzen@gocqlx_test.com")
q := Query(qb.Update("gocqlx_test.person").Set("email").Where(qb.Eq("first_name"), qb.Eq("last_name")).ToCql())
if err := q.BindStruct(p); err != nil {
t.Fatal("bind:", err)
}
mustExec(q.Query)
}
// Select
{
q := Query(qb.Select("gocqlx_test.person").Where(qb.In("first_name")).ToCql())
m := map[string]interface{}{
"first_name": []string{"Patricia", "John"},
}
if err := q.BindMap(m); err != nil {
t.Fatal("bind:", err)
}
var people []Person
if err := gocqlx.Select(&people, q.Query); err != nil {
t.Fatal("select:", err)
}
t.Log(people)
// [{Patricia Citizen [patricia.citzen@gocqlx_test.com patricia1.citzen@gocqlx_test.com]} {John Doe [johndoeDNE@gmail.net]}]
}
}
// Named queries, using `:name` as the bindvar.
{
// compile query to valid gocqlx query and list of named parameters
stmt, names, err := gocqlx.CompileNamedQuery([]byte("INSERT INTO gocqlx_test.person (first_name, last_name, email) VALUES (:first_name, :last_name, :email)"))
if err != nil {
t.Fatal("compile:", err)
}
q := gocqlx.Query(session.Query(stmt), names)
q := gocqlx.Queryx{
Query: session.Query(stmt),
Names: names,
}
if err := q.BindStruct(&Person{
// bind named parameters from a struct
{
p := &Person{
"Jane",
"Citizen",
[]string{"jane.citzen@gocqlx_test.com"},
}); err != nil {
}
if err := q.BindStruct(p); err != nil {
t.Fatal("bind:", err)
}
mustExec(q.Query)
}
if err := q.BindMap(map[string]interface{}{
// bind named parameters from a map
{
m := map[string]interface{}{
"first_name": "Bin",
"last_name": "Smuth",
"email": []string{"bensmith@allblacks.nz"},
}); err != nil {
}
if err := q.BindMap(m); err != nil {
t.Fatal("bind:", err)
}
mustExec(q.Query)
}
}
}

View File

@@ -69,16 +69,12 @@ func TestSnakeCase(t *testing.T) {
func BenchmarkSnakeCase(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, test := range snakeTable {
snakeCase(test.N)
}
snakeCase(snakeTable[b.N%len(snakeTable)].N)
}
}
func BenchmarkToLower(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, test := range snakeTable {
strings.ToLower(test.N)
}
strings.ToLower(snakeTable[b.N%len(snakeTable)].N)
}
}

187
qb/cmp.go Normal file
View File

@@ -0,0 +1,187 @@
package qb
import "bytes"
// op specifies Cmd operation type.
type op byte
const (
eq op = iota
lt
leq
gt
geq
in
cnt
)
// Cmp if a filtering comparator that is used in WHERE and IF clauses.
type Cmp struct {
op op
column string
name string
}
func (cmp Cmp) writeCql(cql *bytes.Buffer) string {
cql.WriteString(cmp.column)
switch cmp.op {
case eq:
cql.WriteByte('=')
case lt:
cql.WriteByte('<')
case leq:
cql.WriteByte('<')
cql.WriteByte('=')
case gt:
cql.WriteByte('>')
case geq:
cql.WriteByte('>')
cql.WriteByte('=')
case in:
cql.WriteString(" IN ")
case cnt:
cql.WriteString(" CONTAINS ")
}
cql.WriteByte('?')
return cmp.name
}
// Eq produces column=?.
func Eq(column string) Cmp {
return Cmp{
op: eq,
column: column,
name: column,
}
}
// EqNamed produces column=? with a custom parameter name.
func EqNamed(column, name string) Cmp {
return Cmp{
op: eq,
column: column,
name: name,
}
}
// Lt produces column<?.
func Lt(column string) Cmp {
return Cmp{
op: lt,
column: column,
name: column,
}
}
// LtNamed produces column<? with a custom parameter name.
func LtNamed(column, name string) Cmp {
return Cmp{
op: lt,
column: column,
name: name,
}
}
// LtOrEq produces column<=?.
func LtOrEq(column string) Cmp {
return Cmp{
op: leq,
column: column,
name: column,
}
}
// LtOrEqNamed produces column<=? with a custom parameter name.
func LtOrEqNamed(column, name string) Cmp {
return Cmp{
op: leq,
column: column,
name: name,
}
}
// Gt produces column>?.
func Gt(column string) Cmp {
return Cmp{
op: gt,
column: column,
name: column,
}
}
// GtNamed produces column>? with a custom parameter name.
func GtNamed(column, name string) Cmp {
return Cmp{
op: gt,
column: column,
name: name,
}
}
// GtOrEq produces column>=?.
func GtOrEq(column string) Cmp {
return Cmp{
op: geq,
column: column,
name: column,
}
}
// GtOrEqNamed produces column>=? with a custom parameter name.
func GtOrEqNamed(column, name string) Cmp {
return Cmp{
op: geq,
column: column,
name: name,
}
}
// In produces column IN ?.
func In(column string) Cmp {
return Cmp{
op: in,
column: column,
name: column,
}
}
// InNamed produces column IN ? with a custom parameter name.
func InNamed(column, name string) Cmp {
return Cmp{
op: in,
column: column,
name: name,
}
}
// Contains produces column CONTAINS ?.
func Contains(column string) Cmp {
return Cmp{
op: cnt,
column: column,
name: column,
}
}
// ContainsNamed produces column CONTAINS ? with a custom parameter name.
func ContainsNamed(column, name string) Cmp {
return Cmp{
op: cnt,
column: column,
name: name,
}
}
type cmps []Cmp
func (cs cmps) writeCql(cql *bytes.Buffer) (names []string) {
for i, c := range cs {
names = append(names, c.writeCql(cql))
if i < len(cs)-1 {
cql.WriteString(" AND ")
}
}
cql.WriteByte(' ')
return
}

114
qb/cmp_test.go Normal file
View File

@@ -0,0 +1,114 @@
package qb
import (
"bytes"
"testing"
"github.com/google/go-cmp/cmp"
)
func TestCmp(t *testing.T) {
table := []struct {
C Cmp
S string
N string
}{
{
C: Eq("eq"),
S: "eq=?",
N: "eq",
},
{
C: EqNamed("eq", "name"),
S: "eq=?",
N: "name",
},
{
C: Lt("lt"),
S: "lt<?",
N: "lt",
},
{
C: LtNamed("lt", "name"),
S: "lt<?",
N: "name",
},
{
C: LtOrEq("lt"),
S: "lt<=?",
N: "lt",
},
{
C: LtOrEqNamed("lt", "name"),
S: "lt<=?",
N: "name",
},
{
C: Gt("gt"),
S: "gt>?",
N: "gt",
},
{
C: GtNamed("gt", "name"),
S: "gt>?",
N: "name",
},
{
C: GtOrEq("gt"),
S: "gt>=?",
N: "gt",
},
{
C: GtOrEqNamed("gt", "name"),
S: "gt>=?",
N: "name",
},
{
C: In("in"),
S: "in IN ?",
N: "in",
},
{
C: InNamed("in", "name"),
S: "in IN ?",
N: "name",
},
{
C: Contains("cnt"),
S: "cnt CONTAINS ?",
N: "cnt",
},
{
C: ContainsNamed("cnt", "name"),
S: "cnt CONTAINS ?",
N: "name",
},
}
buf := bytes.Buffer{}
for _, test := range table {
buf.Reset()
name := test.C.writeCql(&buf)
if diff := cmp.Diff(test.S, buf.String()); diff != "" {
t.Error(diff)
}
if diff := cmp.Diff(test.N, name); diff != "" {
t.Error(diff)
}
}
}
func BenchmarkCmp(b *testing.B) {
buf := bytes.Buffer{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
buf.Reset()
c := cmps{
Eq("id"),
Lt("user_uuid"),
LtOrEq("firstname"),
Gt("stars"),
}
c.writeCql(&buf)
}
}

90
qb/delete.go Normal file
View File

@@ -0,0 +1,90 @@
package qb
// DELETE reference:
// https://cassandra.apache.org/doc/latest/cql/dml.html#delete
import (
"bytes"
"time"
)
// DeleteBuilder builds CQL DELETE statements.
type DeleteBuilder struct {
table string
columns columns
using using
where where
_if _if
exists bool
}
// Delete returns a new DeleteBuilder with the given table name.
func Delete(table string) *DeleteBuilder {
return &DeleteBuilder{
table: table,
}
}
// ToCql builds the query into a CQL string and named args.
func (b *DeleteBuilder) ToCql() (stmt string, names []string) {
cql := bytes.Buffer{}
cql.WriteString("DELETE ")
if len(b.columns) > 0 {
b.columns.writeCql(&cql)
cql.WriteByte(' ')
}
cql.WriteString("FROM ")
cql.WriteString(b.table)
cql.WriteByte(' ')
b.using.writeCql(&cql)
names = append(names, b.where.writeCql(&cql)...)
names = append(names, b._if.writeCql(&cql)...)
if b.exists {
cql.WriteString("IF EXISTS ")
}
stmt = cql.String()
return
}
// From sets the table to be deleted from.
func (b *DeleteBuilder) From(table string) *DeleteBuilder {
b.table = table
return b
}
// Columns adds delete columns to the query.
func (b *DeleteBuilder) Columns(columns ...string) *DeleteBuilder {
b.columns = append(b.columns, columns...)
return b
}
// Timestamp sets a USING TIMESTAMP clause on the query.
func (b *DeleteBuilder) Timestamp(t time.Time) *DeleteBuilder {
b.using.timestamp = t
return b
}
// Where adds an expression to the WHERE clause of the query. Expressions are
// ANDed together in the generated CQL.
func (b *DeleteBuilder) Where(w ...Cmp) *DeleteBuilder {
b.where = append(b.where, w...)
return b
}
// If adds an expression to the IF clause of the query. Expressions are ANDed
// together in the generated CQL.
func (b *DeleteBuilder) If(w ...Cmp) *DeleteBuilder {
b._if = append(b._if, w...)
return b
}
// Existing sets a IF EXISTS clause on the query.
func (b *DeleteBuilder) Existing() *DeleteBuilder {
b.exists = true
return b
}

78
qb/delete_test.go Normal file
View File

@@ -0,0 +1,78 @@
package qb
import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
)
func TestDeleteBuilder(t *testing.T) {
w := EqNamed("id", "expr")
table := []struct {
B *DeleteBuilder
N []string
S string
}{
// Basic test for delete
{
B: Delete("cycling.cyclist_name").Where(w),
S: "DELETE FROM cycling.cyclist_name WHERE id=? ",
N: []string{"expr"},
},
// Change table name
{
B: Delete("cycling.cyclist_name").Where(w).From("Foobar"),
S: "DELETE FROM Foobar WHERE id=? ",
N: []string{"expr"},
},
// Add column
{
B: Delete("cycling.cyclist_name").Where(w).Columns("stars"),
S: "DELETE stars FROM cycling.cyclist_name WHERE id=? ",
N: []string{"expr"},
},
// Add WHERE
{
B: Delete("cycling.cyclist_name").Where(w, Gt("firstname")),
S: "DELETE FROM cycling.cyclist_name WHERE id=? AND firstname>? ",
N: []string{"expr", "firstname"},
},
// Add IF
{
B: Delete("cycling.cyclist_name").Where(w).If(Gt("firstname")),
S: "DELETE FROM cycling.cyclist_name WHERE id=? IF firstname>? ",
N: []string{"expr", "firstname"},
},
// Add TIMESTAMP
{
B: Delete("cycling.cyclist_name").Where(w).Timestamp(time.Unix(0, 0).Add(time.Microsecond * 123456789)),
S: "DELETE FROM cycling.cyclist_name USING TIMESTAMP 123456789 WHERE id=? ",
N: []string{"expr"},
},
// Add IF EXISTS
{
B: Delete("cycling.cyclist_name").Where(w).Existing(),
S: "DELETE FROM cycling.cyclist_name WHERE id=? IF EXISTS ",
N: []string{"expr"},
},
}
for _, test := range table {
stmt, names := test.B.ToCql()
if diff := cmp.Diff(test.S, stmt); diff != "" {
t.Error(diff)
}
if diff := cmp.Diff(test.N, names); diff != "" {
t.Error(diff)
}
}
}
func BenchmarkDeleteBuilder(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
Delete("cycling.cyclist_name").Columns("id", "user_uuid", "firstname", "stars").Where(Eq("id"))
}
}

4
qb/doc.go Normal file
View File

@@ -0,0 +1,4 @@
// Package qb provides CQL (Scylla / Cassandra query language) query builders.
// The builders create CQL statement and a list of named parameters that can
// later be bound using github.com/scylladb/gocqlx.
package qb

65
qb/expr.go Normal file
View File

@@ -0,0 +1,65 @@
package qb
import (
"bytes"
"fmt"
"time"
)
type columns []string
func (cols columns) writeCql(cql *bytes.Buffer) {
for i, c := range cols {
cql.WriteString(c)
if i < len(cols)-1 {
cql.WriteByte(',')
}
}
}
type using struct {
timestamp time.Time
ttl time.Duration
}
func (u using) writeCql(cql *bytes.Buffer) {
ts := !u.timestamp.IsZero()
if ts {
cql.WriteString("USING TIMESTAMP ")
cql.WriteString(fmt.Sprint(u.timestamp.UnixNano() / 1000))
cql.WriteByte(' ')
}
if u.ttl != 0 {
if ts {
cql.WriteString("AND TTL ")
} else {
cql.WriteString("USING TTL ")
}
cql.WriteString(fmt.Sprint(int(u.ttl.Seconds())))
cql.WriteByte(' ')
}
}
type where cmps
func (w where) writeCql(cql *bytes.Buffer) (names []string) {
if len(w) == 0 {
return
}
cql.WriteString("WHERE ")
return cmps(w).writeCql(cql)
}
type _if cmps
func (w _if) writeCql(cql *bytes.Buffer) (names []string) {
if len(w) == 0 {
return
}
cql.WriteString("IF ")
return cmps(w).writeCql(cql)
}

82
qb/insert.go Normal file
View File

@@ -0,0 +1,82 @@
package qb
// INSERT reference:
// https://cassandra.apache.org/doc/latest/cql/dml.html#insert
import (
"bytes"
"time"
)
// InsertBuilder builds CQL INSERT statements.
type InsertBuilder struct {
table string
columns columns
unique bool
using using
}
// Insert returns a new InsertBuilder with the given table name.
func Insert(table string) *InsertBuilder {
return &InsertBuilder{
table: table,
}
}
// ToCql builds the query into a CQL string and named args.
func (b *InsertBuilder) ToCql() (stmt string, names []string) {
cql := bytes.Buffer{}
cql.WriteString("INSERT ")
cql.WriteString("INTO ")
cql.WriteString(b.table)
cql.WriteByte(' ')
cql.WriteByte('(')
b.columns.writeCql(&cql)
cql.WriteString(") ")
cql.WriteString("VALUES (")
placeholders(&cql, len(b.columns))
cql.WriteString(") ")
b.using.writeCql(&cql)
if b.unique {
cql.WriteString("IF NOT EXISTS ")
}
stmt, names = cql.String(), b.columns
return
}
// Into sets the INTO clause of the query.
func (b *InsertBuilder) Into(table string) *InsertBuilder {
b.table = table
return b
}
// Columns adds insert columns to the query.
func (b *InsertBuilder) Columns(columns ...string) *InsertBuilder {
b.columns = append(b.columns, columns...)
return b
}
// Unique sets a IF NOT EXISTS clause on the query.
func (b *InsertBuilder) Unique() *InsertBuilder {
b.unique = true
return b
}
// Timestamp sets a USING TIMESTAMP clause on the query.
func (b *InsertBuilder) Timestamp(t time.Time) *InsertBuilder {
b.using.timestamp = t
return b
}
// TTL sets a USING TTL clause on the query.
func (b *InsertBuilder) TTL(d time.Duration) *InsertBuilder {
b.using.ttl = d
return b
}

71
qb/insert_test.go Normal file
View File

@@ -0,0 +1,71 @@
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 := test.B.ToCql()
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("cycling.cyclist_name").Columns("id", "user_uuid", "firstname", "stars")
}
}

7
qb/qb.go Normal file
View File

@@ -0,0 +1,7 @@
package qb
// Builder is interface implemented by all the builders.
type Builder interface {
// ToCql builds the query into a CQL string and named args.
ToCql() (stmt string, names []string)
}

156
qb/select.go Normal file
View File

@@ -0,0 +1,156 @@
package qb
// SELECT reference:
// https://cassandra.apache.org/doc/latest/cql/dml.html#select
import (
"bytes"
"fmt"
)
// Order specifies sorting order.
type Order bool
const (
// ASC is ascending order
ASC Order = true
// DESC is descending order
DESC = false
)
// SelectBuilder builds CQL SELECT statements.
type SelectBuilder struct {
table string
columns columns
distinct columns
where where
groupBy columns
orderBy string
order Order
limit uint
limitPerPartition uint
allowFiltering bool
}
// Select returns a new SelectBuilder with the given table name.
func Select(table string) *SelectBuilder {
return &SelectBuilder{
table: table,
}
}
// ToCql builds the query into a CQL string and named args.
func (b *SelectBuilder) ToCql() (stmt string, names []string) {
cql := bytes.Buffer{}
cql.WriteString("SELECT ")
switch {
case len(b.distinct) > 0:
cql.WriteString("DISTINCT ")
b.distinct.writeCql(&cql)
case len(b.groupBy) > 0:
b.groupBy.writeCql(&cql)
cql.WriteByte(',')
b.columns.writeCql(&cql)
case len(b.columns) == 0:
cql.WriteByte('*')
default:
b.columns.writeCql(&cql)
}
cql.WriteString(" FROM ")
cql.WriteString(b.table)
cql.WriteByte(' ')
names = b.where.writeCql(&cql)
if len(b.groupBy) > 0 {
cql.WriteString("GROUP BY ")
b.groupBy.writeCql(&cql)
cql.WriteByte(' ')
}
if b.orderBy != "" {
cql.WriteString("ORDER BY ")
cql.WriteString(b.orderBy)
if b.order {
cql.WriteString(" ASC ")
} else {
cql.WriteString(" DESC ")
}
}
if b.limit != 0 {
cql.WriteString("LIMIT ")
cql.WriteString(fmt.Sprint(b.limit))
cql.WriteByte(' ')
}
if b.limitPerPartition != 0 {
cql.WriteString("PER PARTITION LIMIT ")
cql.WriteString(fmt.Sprint(b.limitPerPartition))
cql.WriteByte(' ')
}
if b.allowFiltering {
cql.WriteString("ALLOW FILTERING ")
}
stmt = cql.String()
return
}
// From sets the table to be selected from.
func (b *SelectBuilder) From(table string) *SelectBuilder {
b.table = table
return b
}
// Columns adds result columns to the query.
func (b *SelectBuilder) Columns(columns ...string) *SelectBuilder {
b.columns = append(b.columns, columns...)
return b
}
// Distinct sets DISTINCT clause on the query.
func (b *SelectBuilder) Distinct(columns ...string) *SelectBuilder {
b.distinct = append(b.distinct, columns...)
return b
}
// Where adds an expression to the WHERE clause of the query. Expressions are
// ANDed together in the generated CQL.
func (b *SelectBuilder) Where(w ...Cmp) *SelectBuilder {
b.where = append(b.where, w...)
return b
}
// GroupBy sets GROUP BY clause on the query. Columns must be a primary key,
// this will automatically add the the columns as first selectors.
func (b *SelectBuilder) GroupBy(columns ...string) *SelectBuilder {
b.groupBy = append(b.groupBy, columns...)
return b
}
// OrderBy sets ORDER BY clause on the query.
func (b *SelectBuilder) OrderBy(column string, o Order) *SelectBuilder {
b.orderBy, b.order = column, o
return b
}
// Limit sets a LIMIT clause on the query.
func (b *SelectBuilder) Limit(limit uint) *SelectBuilder {
b.limit = limit
return b
}
// LimitPerPartition sets a PER PARTITION LIMIT clause on the query.
func (b *SelectBuilder) LimitPerPartition(limit uint) *SelectBuilder {
b.limitPerPartition = limit
return b
}
// AllowFiltering sets a ALLOW FILTERING clause on the query.
func (b *SelectBuilder) AllowFiltering() *SelectBuilder {
b.allowFiltering = true
return b
}

90
qb/select_test.go Normal file
View File

@@ -0,0 +1,90 @@
package qb
import (
"testing"
"github.com/google/go-cmp/cmp"
)
func TestSelectBuilder(t *testing.T) {
w := EqNamed("id", "expr")
table := []struct {
B *SelectBuilder
N []string
S string
}{
// Basic test for select *
{
B: Select("cycling.cyclist_name"),
S: "SELECT * FROM cycling.cyclist_name ",
},
// Basic test for select columns
{
B: Select("cycling.cyclist_name").Columns("id", "user_uuid", "firstname"),
S: "SELECT id,user_uuid,firstname FROM cycling.cyclist_name ",
},
// Basic test for select distinct
{
B: Select("cycling.cyclist_name").Distinct("id"),
S: "SELECT DISTINCT id FROM cycling.cyclist_name ",
},
// Change table name
{
B: Select("cycling.cyclist_name").From("Foobar"),
S: "SELECT * FROM Foobar ",
},
// Add WHERE
{
B: Select("cycling.cyclist_name").Where(w, Gt("firstname")),
S: "SELECT * FROM cycling.cyclist_name WHERE id=? AND firstname>? ",
N: []string{"expr", "firstname"},
},
// Add GROUP BY
{
B: Select("cycling.cyclist_name").Columns("MAX(stars) as max_stars").GroupBy("id"),
S: "SELECT id,MAX(stars) as max_stars FROM cycling.cyclist_name GROUP BY id ",
},
// Add ORDER BY
{
B: Select("cycling.cyclist_name").Where(w).OrderBy("firstname", ASC),
S: "SELECT * FROM cycling.cyclist_name WHERE id=? ORDER BY firstname ASC ",
N: []string{"expr"},
},
// Add LIMIT
{
B: Select("cycling.cyclist_name").Where(w).Limit(10),
S: "SELECT * FROM cycling.cyclist_name WHERE id=? LIMIT 10 ",
N: []string{"expr"},
},
// Add PER PARTITION LIMIT
{
B: Select("cycling.cyclist_name").Where(w).LimitPerPartition(10),
S: "SELECT * FROM cycling.cyclist_name WHERE id=? PER PARTITION LIMIT 10 ",
N: []string{"expr"},
},
// Add ALLOW FILTERING
{
B: Select("cycling.cyclist_name").Where(w).AllowFiltering(),
S: "SELECT * FROM cycling.cyclist_name WHERE id=? ALLOW FILTERING ",
N: []string{"expr"},
},
}
for _, test := range table {
stmt, names := test.B.ToCql()
if diff := cmp.Diff(test.S, stmt); diff != "" {
t.Error(diff)
}
if diff := cmp.Diff(test.N, names); diff != "" {
t.Error(diff)
}
}
}
func BenchmarkSelectBuilder(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
Select("cycling.cyclist_name").Columns("id", "user_uuid", "firstname", "stars").Where(Eq("id"))
}
}

102
qb/update.go Normal file
View File

@@ -0,0 +1,102 @@
package qb
// UPDATE reference:
// https://cassandra.apache.org/doc/latest/cql/dml.html#update
import (
"bytes"
"time"
)
// UpdateBuilder builds CQL UPDATE statements.
type UpdateBuilder struct {
table string
using using
columns columns
where where
_if _if
exists bool
}
// Update returns a new UpdateBuilder with the given table name.
func Update(table string) *UpdateBuilder {
return &UpdateBuilder{
table: table,
}
}
// ToCql builds the query into a CQL string and named args.
func (b *UpdateBuilder) ToCql() (stmt string, names []string) {
cql := bytes.Buffer{}
cql.WriteString("UPDATE ")
cql.WriteString(b.table)
cql.WriteByte(' ')
b.using.writeCql(&cql)
cql.WriteString("SET ")
for i, c := range b.columns {
cql.WriteString(c)
cql.WriteString("=?")
if i < len(b.columns)-1 {
cql.WriteByte(',')
}
}
names = append(names, b.columns...)
cql.WriteByte(' ')
names = append(names, b.where.writeCql(&cql)...)
names = append(names, b._if.writeCql(&cql)...)
if b.exists {
cql.WriteString("IF EXISTS ")
}
stmt = cql.String()
return
}
// Table sets the table to be updated.
func (b *UpdateBuilder) Table(table string) *UpdateBuilder {
b.table = table
return b
}
// Timestamp sets a USING TIMESTAMP clause on the query.
func (b *UpdateBuilder) Timestamp(t time.Time) *UpdateBuilder {
b.using.timestamp = t
return b
}
// TTL sets a USING TTL clause on the query.
func (b *UpdateBuilder) TTL(d time.Duration) *UpdateBuilder {
b.using.ttl = d
return b
}
// Set adds SET clauses to the query.
func (b *UpdateBuilder) Set(columns ...string) *UpdateBuilder {
b.columns = append(b.columns, columns...)
return b
}
// Where adds an expression to the WHERE clause of the query. Expressions are
// ANDed together in the generated CQL.
func (b *UpdateBuilder) Where(w ...Cmp) *UpdateBuilder {
b.where = append(b.where, w...)
return b
}
// If adds an expression to the IF clause of the query. Expressions are ANDed
// together in the generated CQL.
func (b *UpdateBuilder) If(w ...Cmp) *UpdateBuilder {
b._if = append(b._if, w...)
return b
}
// Existing sets a IF EXISTS clause on the query.
func (b *UpdateBuilder) Existing() *UpdateBuilder {
b.exists = true
return b
}

84
qb/update_test.go Normal file
View File

@@ -0,0 +1,84 @@
package qb
import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
)
func TestUpdateBuilder(t *testing.T) {
w := EqNamed("id", "expr")
table := []struct {
B *UpdateBuilder
N []string
S string
}{
// Basic test for update
{
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w),
S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=? WHERE id=? ",
N: []string{"id", "user_uuid", "firstname", "expr"},
},
// Change table name
{
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).Table("Foobar"),
S: "UPDATE Foobar SET id=?,user_uuid=?,firstname=? WHERE id=? ",
N: []string{"id", "user_uuid", "firstname", "expr"},
},
// Add SET
{
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).Set("stars"),
S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=?,stars=? WHERE id=? ",
N: []string{"id", "user_uuid", "firstname", "stars", "expr"},
},
// Add WHERE
{
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w, Gt("firstname")),
S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=? WHERE id=? AND firstname>? ",
N: []string{"id", "user_uuid", "firstname", "expr", "firstname"},
},
// Add IF
{
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).If(Gt("firstname")),
S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=? WHERE id=? IF firstname>? ",
N: []string{"id", "user_uuid", "firstname", "expr", "firstname"},
},
// Add TTL
{
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).TTL(time.Second * 86400),
S: "UPDATE cycling.cyclist_name USING TTL 86400 SET id=?,user_uuid=?,firstname=? WHERE id=? ",
N: []string{"id", "user_uuid", "firstname", "expr"},
},
// Add TIMESTAMP
{
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).Timestamp(time.Unix(0, 0).Add(time.Microsecond * 123456789)),
S: "UPDATE cycling.cyclist_name USING TIMESTAMP 123456789 SET id=?,user_uuid=?,firstname=? WHERE id=? ",
N: []string{"id", "user_uuid", "firstname", "expr"},
},
// Add IF EXISTS
{
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).Existing(),
S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=? WHERE id=? IF EXISTS ",
N: []string{"id", "user_uuid", "firstname", "expr"},
},
}
for _, test := range table {
stmt, names := test.B.ToCql()
if diff := cmp.Diff(test.S, stmt); diff != "" {
t.Error(diff)
}
if diff := cmp.Diff(test.N, names); diff != "" {
t.Error(diff)
}
}
}
func BenchmarkUpdateBuilder(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname", "stars").Where(Eq("id"))
}
}

18
qb/utils.go Normal file
View File

@@ -0,0 +1,18 @@
package qb
import (
"bytes"
)
// placeholders returns a string with count ? placeholders joined with commas.
func placeholders(cql *bytes.Buffer, count int) {
if count < 1 {
return
}
for i := 0; i < count-1; i++ {
cql.WriteByte('?')
cql.WriteByte(',')
}
cql.WriteByte('?')
}

View File

@@ -82,14 +82,18 @@ type Queryx struct {
Mapper *reflectx.Mapper
}
// BindStruct binds query named parameters using mapper.
func (q Queryx) BindStruct(arg interface{}) error {
m := q.Mapper
if m == nil {
m = DefaultMapper
// Query creates a new Queryx from gocql.Query using a default mapper.
func Query(q *gocql.Query, names []string) Queryx {
return Queryx{
Query: q,
Names: names,
Mapper: DefaultMapper,
}
}
arglist, err := bindStructArgs(q.Names, arg, m)
// BindStruct binds query named parameters using mapper.
func (q Queryx) BindStruct(arg interface{}) error {
arglist, err := bindStructArgs(q.Names, arg, q.Mapper)
if err != nil {
return err
}
@@ -144,3 +148,13 @@ func bindMapArgs(names []string, arg map[string]interface{}) ([]interface{}, err
}
return arglist, nil
}
// QueryFunc creates Queryx from qb.Builder.ToCql() output.
type QueryFunc func(stmt string, names []string) Queryx
// SessionQuery creates QueryFunc that's session aware.
func SessionQuery(session *gocql.Session) QueryFunc {
return func(stmt string, names []string) Queryx {
return Query(session.Query(stmt), names)
}
}

View File

@@ -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 cycling.cyclist_name (id, user_uuid, firstname, stars) VALUES (:id, :user_uuid, :firstname, :stars)")
b.ResetTimer()
for i := 0; i < b.N; i++ {
CompileNamedQuery([]byte(q1))
CompileNamedQuery(q)
}
}
@@ -101,10 +101,7 @@ func TestBindStruct(t *testing.T) {
}
func BenchmarkBindStruct(b *testing.B) {
q := Queryx{
Query: &gocql.Query{},
Names: []string{"name", "age", "first", "last"},
}
q := Query(&gocql.Query{}, []string{"name", "age", "first", "last"})
type t struct {
Name string
Age int