113
README.md
113
README.md
@@ -1,6 +1,7 @@
|
|||||||
# gocqlx [](http://godoc.org/github.com/scylladb/gocqlx) [](https://goreportcard.com/report/github.com/scylladb/gocqlx) [](https://travis-ci.org/scylladb/gocqlx)
|
# gocqlx [](http://godoc.org/github.com/scylladb/gocqlx) [](https://goreportcard.com/report/github.com/scylladb/gocqlx) [](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
|
It contains wrappers over `gocql` types that provide convenience methods which
|
||||||
are useful in the development of database driven applications. Under the
|
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
|
## Features
|
||||||
|
|
||||||
Read all rows into a slice.
|
Fast, boilerplate free and flexible `SELECTS`, `INSERTS`, `UPDATES` and `DELETES`.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
var v []*Item
|
type Person struct {
|
||||||
if err := gocqlx.Select(&v, session.Query(`SELECT * FROM items WHERE id = ?`, id)); err != nil {
|
FirstName string // no need to add `db:"first_name"` etc.
|
||||||
log.Fatal("select failed", err)
|
LastName string
|
||||||
|
Email []string
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
Read a single row into a struct.
|
p := &Person{
|
||||||
|
"Patricia",
|
||||||
```go
|
"Citizen",
|
||||||
var v Item
|
[]string{"patricia.citzen@gocqlx_test.com"},
|
||||||
if err := gocqlx.Get(&v, session.Query(`SELECT * FROM items WHERE id = ?`, id)); err != nil {
|
|
||||||
log.Fatal("get failed", err)
|
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
Bind named query parameters from a struct or map.
|
// Insert
|
||||||
|
{
|
||||||
```go
|
q := Query(qb.Insert("person").Columns("first_name", "last_name", "email").ToCql())
|
||||||
stmt, names, err := gocqlx.CompileNamedQuery([]byte("INSERT INTO items (id, name) VALUES (:id, :name)"))
|
if err := q.BindStruct(p); err != nil {
|
||||||
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)
|
t.Fatal("bind:", err)
|
||||||
|
}
|
||||||
|
mustExec(q.Query)
|
||||||
}
|
}
|
||||||
if err := q.Query.Exec(); err != nil {
|
|
||||||
log.Fatal("get failed", err)
|
// 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
239
benchmark_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,7 +72,6 @@ func createKeyspace(tb testing.TB, cluster *gocql.ClusterConfig, keyspace string
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer session.Close()
|
defer session.Close()
|
||||||
defer tb.Log("closing keyspace session")
|
|
||||||
|
|
||||||
err = createTable(session, `DROP KEYSPACE IF EXISTS `+keyspace)
|
err = createTable(session, `DROP KEYSPACE IF EXISTS `+keyspace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
35
doc.go
35
doc.go
@@ -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
|
// It contains wrappers over gocql types that provide convenience methods which
|
||||||
// are useful in the development of database driven applications. Under the
|
// 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.
|
// 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
|
package gocqlx
|
||||||
|
|||||||
135
example_test.go
135
example_test.go
@@ -3,15 +3,15 @@
|
|||||||
package gocqlx_test
|
package gocqlx_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gocql/gocql"
|
"github.com/gocql/gocql"
|
||||||
"github.com/scylladb/gocqlx"
|
"github.com/scylladb/gocqlx"
|
||||||
|
"github.com/scylladb/gocqlx/qb"
|
||||||
)
|
)
|
||||||
|
|
||||||
var personSchema = `
|
var personSchema = `
|
||||||
CREATE TABLE gocqlx_test.person (
|
CREATE TABLE IF NOT EXISTS gocqlx_test.person (
|
||||||
first_name text,
|
first_name text,
|
||||||
last_name text,
|
last_name text,
|
||||||
email list<text>,
|
email list<text>,
|
||||||
@@ -19,7 +19,7 @@ CREATE TABLE gocqlx_test.person (
|
|||||||
)`
|
)`
|
||||||
|
|
||||||
var placeSchema = `
|
var placeSchema = `
|
||||||
CREATE TABLE gocqlx_test.place (
|
CREATE TABLE IF NOT EXISTS gocqlx_test.place (
|
||||||
country text,
|
country text,
|
||||||
city text,
|
city text,
|
||||||
code int,
|
code int,
|
||||||
@@ -27,7 +27,7 @@ CREATE TABLE gocqlx_test.place (
|
|||||||
)`
|
)`
|
||||||
|
|
||||||
// Field names are converted to camel case by default, no need to add
|
// 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 {
|
type Person struct {
|
||||||
FirstName string
|
FirstName string
|
||||||
LastName string
|
LastName string
|
||||||
@@ -46,13 +46,15 @@ func TestExample(t *testing.T) {
|
|||||||
|
|
||||||
mustExec := func(q *gocql.Query) {
|
mustExec := func(q *gocql.Query) {
|
||||||
if err := q.Exec(); err != nil {
|
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 (?, ?, ?)")
|
q := session.Query("INSERT INTO gocqlx_test.person (first_name, last_name, email) VALUES (?, ?, ?)")
|
||||||
mustExec(q.Bind("Jason", "Moiron", []string{"jmoiron@jmoiron.net"}))
|
mustExec(q.Bind("Jason", "Moiron", []string{"jmoiron@jmoiron.net"}))
|
||||||
@@ -60,9 +62,11 @@ func TestExample(t *testing.T) {
|
|||||||
q.Release()
|
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 (?, ?, ?)")
|
q := session.Query("INSERT INTO gocqlx_test.place (country, city, code) VALUES (?, ?, ?)")
|
||||||
mustExec(q.Bind("United States", "New York", 1))
|
mustExec(q.Bind("United States", "New York", 1))
|
||||||
@@ -71,72 +75,131 @@ func TestExample(t *testing.T) {
|
|||||||
q.Release()
|
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{}
|
var people []Person
|
||||||
if err := gocqlx.Select(&people, session.Query("SELECT * FROM person")); err != nil {
|
if err := gocqlx.Select(&people, session.Query("SELECT * FROM gocqlx_test.person")); err != nil {
|
||||||
t.Fatal("select:", err)
|
t.Fatal("select:", err)
|
||||||
}
|
}
|
||||||
|
t.Log(people)
|
||||||
|
|
||||||
fmt.Printf("%#v\n%#v\n", people[0], people[1])
|
// [{John Doe [johndoeDNE@gmail.net]} {Jason Moiron [jmoiron@jmoiron.net]}]
|
||||||
// gocqlx_test.Person{FirstName:"John", LastName:"Doe", Email:[]string{"johndoeDNE@gmail.net"}}
|
|
||||||
// gocqlx_test.Person{FirstName:"Jason", LastName:"Moiron", Email:[]string{"jmoiron@jmoiron.net"}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a single result, a la QueryRow
|
// Get a single result.
|
||||||
{
|
{
|
||||||
var jason Person
|
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)
|
t.Fatal("get:", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%#v\n", jason)
|
t.Log(jason)
|
||||||
// gocqlx_test.Person{FirstName:"Jason", LastName:"Moiron", Email:[]string{"jmoiron@jmoiron.net"}}
|
|
||||||
|
// Jason Moiron [jmoiron@jmoiron.net]}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop through rows using only one struct
|
// Loop through rows using only one struct.
|
||||||
{
|
{
|
||||||
var place Place
|
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) {
|
for iter.StructScan(&place) {
|
||||||
fmt.Printf("%#v\n", place)
|
t.Log(place)
|
||||||
}
|
}
|
||||||
if err := iter.Close(); err != nil {
|
if err := iter.Close(); err != nil {
|
||||||
t.Fatal("iter:", err)
|
t.Fatal("iter:", err)
|
||||||
}
|
}
|
||||||
iter.ReleaseQuery()
|
iter.ReleaseQuery()
|
||||||
// gocqlx_test.Place{Country:"Hong Kong", City:"", TelCode:852}
|
|
||||||
// gocqlx_test.Place{Country:"United States", City:"New York", TelCode:1}
|
// {Hong Kong 852}
|
||||||
// gocqlx_test.Place{Country:"Singapore", City:"", TelCode:65}
|
// {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 {
|
if err != nil {
|
||||||
t.Fatal("compile:", err)
|
t.Fatal("compile:", err)
|
||||||
}
|
}
|
||||||
|
q := gocqlx.Query(session.Query(stmt), names)
|
||||||
|
|
||||||
q := gocqlx.Queryx{
|
// bind named parameters from a struct
|
||||||
Query: session.Query(stmt),
|
{
|
||||||
Names: names,
|
p := &Person{
|
||||||
}
|
|
||||||
|
|
||||||
if err := q.BindStruct(&Person{
|
|
||||||
"Jane",
|
"Jane",
|
||||||
"Citizen",
|
"Citizen",
|
||||||
[]string{"jane.citzen@gocqlx_test.com"},
|
[]string{"jane.citzen@gocqlx_test.com"},
|
||||||
}); err != nil {
|
}
|
||||||
|
|
||||||
|
if err := q.BindStruct(p); err != nil {
|
||||||
t.Fatal("bind:", err)
|
t.Fatal("bind:", err)
|
||||||
}
|
}
|
||||||
mustExec(q.Query)
|
mustExec(q.Query)
|
||||||
|
}
|
||||||
|
|
||||||
if err := q.BindMap(map[string]interface{}{
|
// bind named parameters from a map
|
||||||
|
{
|
||||||
|
m := map[string]interface{}{
|
||||||
"first_name": "Bin",
|
"first_name": "Bin",
|
||||||
"last_name": "Smuth",
|
"last_name": "Smuth",
|
||||||
"email": []string{"bensmith@allblacks.nz"},
|
"email": []string{"bensmith@allblacks.nz"},
|
||||||
}); err != nil {
|
}
|
||||||
|
|
||||||
|
if err := q.BindMap(m); err != nil {
|
||||||
t.Fatal("bind:", err)
|
t.Fatal("bind:", err)
|
||||||
}
|
}
|
||||||
mustExec(q.Query)
|
mustExec(q.Query)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,16 +69,12 @@ func TestSnakeCase(t *testing.T) {
|
|||||||
|
|
||||||
func BenchmarkSnakeCase(b *testing.B) {
|
func BenchmarkSnakeCase(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
for _, test := range snakeTable {
|
snakeCase(snakeTable[b.N%len(snakeTable)].N)
|
||||||
snakeCase(test.N)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkToLower(b *testing.B) {
|
func BenchmarkToLower(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
for _, test := range snakeTable {
|
strings.ToLower(snakeTable[b.N%len(snakeTable)].N)
|
||||||
strings.ToLower(test.N)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
187
qb/cmp.go
Normal file
187
qb/cmp.go
Normal 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
114
qb/cmp_test.go
Normal 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
90
qb/delete.go
Normal 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
78
qb/delete_test.go
Normal 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
4
qb/doc.go
Normal 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
65
qb/expr.go
Normal 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
82
qb/insert.go
Normal 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
71
qb/insert_test.go
Normal 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
7
qb/qb.go
Normal 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
156
qb/select.go
Normal 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
90
qb/select_test.go
Normal 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
102
qb/update.go
Normal 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
84
qb/update_test.go
Normal 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
18
qb/utils.go
Normal 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('?')
|
||||||
|
}
|
||||||
26
queryx.go
26
queryx.go
@@ -82,14 +82,18 @@ type Queryx struct {
|
|||||||
Mapper *reflectx.Mapper
|
Mapper *reflectx.Mapper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// BindStruct binds query named parameters using mapper.
|
// BindStruct binds query named parameters using mapper.
|
||||||
func (q Queryx) BindStruct(arg interface{}) error {
|
func (q Queryx) BindStruct(arg interface{}) error {
|
||||||
m := q.Mapper
|
arglist, err := bindStructArgs(q.Names, arg, q.Mapper)
|
||||||
if m == nil {
|
|
||||||
m = DefaultMapper
|
|
||||||
}
|
|
||||||
|
|
||||||
arglist, err := bindStructArgs(q.Names, arg, m)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -144,3 +148,13 @@ func bindMapArgs(names []string, arg map[string]interface{}) ([]interface{}, err
|
|||||||
}
|
}
|
||||||
return arglist, nil
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ func TestCompileQuery(t *testing.T) {
|
|||||||
Q, R string
|
Q, R string
|
||||||
V []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)`,
|
Q: `INSERT INTO foo (a,b,c,d) VALUES (:name, :age, :first, :last)`,
|
||||||
R: `INSERT INTO foo (a,b,c,d) VALUES (?, ?, ?, ?)`,
|
R: `INSERT INTO foo (a,b,c,d) VALUES (?, ?, ?, ?)`,
|
||||||
@@ -59,10 +59,10 @@ func TestCompileQuery(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkCompileNamedQuery(b *testing.B) {
|
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()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
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) {
|
func BenchmarkBindStruct(b *testing.B) {
|
||||||
q := Queryx{
|
q := Query(&gocql.Query{}, []string{"name", "age", "first", "last"})
|
||||||
Query: &gocql.Query{},
|
|
||||||
Names: []string{"name", "age", "first", "last"},
|
|
||||||
}
|
|
||||||
type t struct {
|
type t struct {
|
||||||
Name string
|
Name string
|
||||||
Age int
|
Age int
|
||||||
|
|||||||
Reference in New Issue
Block a user