diff --git a/README.md b/README.md index c26d78c..1d71ebd 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,17 @@ hood it uses `sqlx/reflectx` package so `sqlx` models will also work with `gocql ## Installation - go get github.com/scylladb/gocqlx + go get -u github.com/scylladb/gocqlx ## Features -Fast, boilerplate free and flexible `SELECTS`, `INSERTS`, `UPDATES` and `DELETES`. +* Flexible `SELECT`, `INSERT`, `UPDATE` `DELETE` and `BATCH` query building using a DSL +* Support for named parameters (:identifier) in queries +* Binding parameters form struct or map +* Scanning results into structs +* Fast! + +Example, see [full example here](https://github.com/scylladb/gocqlx/blob/master/example_test.go) ```go type Person struct { @@ -38,57 +44,69 @@ p := &Person{ } } -// Insert with TTL +// Batch { - stmt, names := qb.Insert("person").Columns("first_name", "last_name", "email").TTL().ToCql() - q := gocqlx.Query(session.Query(stmt), names) + i := qb.Insert("person").Columns("first_name", "last_name", "email") - if err := q.BindStructMap(p, qb.M{"_ttl": qb.TTL(86400 * time.Second)}).Exec(); err != nil { - log.Fatal(err) - } + stmt, names := qb.Batch(). + Add("a.", i). + Add("b.", i). + ToCql() + q := gocqlx.Query(session.Query(stmt), names) + + b := struct { + A Person + B Person + }{ + A: Person{ + "Igy", + "Citizen", + []string{"ian.citzen@gocqlx_test.com"}, + }, + B: Person{ + "Ian", + "Citizen", + []string{"igy.citzen@gocqlx_test.com"}, + }, + } + + if err := q.BindStruct(&b).Exec(); err != nil { + t.Fatal(err) + } } -// Update +// Get { - p.Email = append(p.Email, "patricia1.citzen@gocqlx_test.com") - - stmt, names := qb.Update("person").Set("email").Where(qb.Eq("first_name"), qb.Eq("last_name")).ToCql() - q := gocqlx.Query(session.Query(stmt), names) - - if err := q.BindStruct(p).Exec(); err != nil { - log.Fatal(err) - } + var p Person + if err := gocqlx.Get(&p, session.Query("SELECT * FROM gocqlx_test.person WHERE first_name=?", "Patricia")); err != nil { + t.Fatal("get:", err) + } + t.Log(p) // {Patricia Citizen [patricia.citzen@gocqlx_test.com patricia1.citzen@gocqlx_test.com]} } // Select { - stmt, names := qb.Select("person").Where(qb.In("first_name")).ToCql() - q := gocqlx.Query(session.Query(stmt), names) + stmt, names := qb.Select("gocqlx_test.person").Where(qb.In("first_name")).ToCql() + q := gocqlx.Query(session.Query(stmt), names) - q.BindMap(qb.M{"first_name": []string{"Patricia", "John"}}) - if err := q.Err(); err != nil { - log.Fatal(err) - } + q.BindMap(qb.M{"first_name": []string{"Patricia", "Igy", "Ian"}}) + if err := q.Err(); err != nil { + t.Fatal(err) + } - var people []Person - if err := gocqlx.Select(&people, q.Query); err != nil { - log.Fatal("select:", err) - } - log.Println(people) - - // [{Patricia Citizen [patricia.citzen@com patricia1.citzen@com]} {John Doe [johndoeDNE@gmail.net]}] + var people []Person + if err := gocqlx.Select(&people, q.Query); err != nil { + t.Fatal("select:", err) + } + t.Log(people) // [{Ian Citizen [igy.citzen@gocqlx_test.com]} {Igy Citizen [ian.citzen@gocqlx_test.com]} {Patricia Citizen [patricia.citzen@gocqlx_test.com patricia1.citzen@gocqlx_test.com]}] } ``` -For more details 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. +Gocqlx is fast, this is a benchmark result comparing `gocqlx` to raw `gocql` +on a local machine. For query binding (insert) `gocqlx` is faster then `gocql` +thanks to smart caching, otherwise the performance is comparable. ``` BenchmarkE2EGocqlInsert-4 500000 258434 ns/op 2627 B/op 59 allocs/op @@ -99,23 +117,4 @@ BenchmarkE2EGocqlSelect-4 30000 2588562 ns/op 34605 BenchmarkE2EGocqlxSelect-4 30000 2637187 ns/op 27718 B/op 951 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! +See the [benchmark here](https://github.com/scylladb/gocqlx/blob/master/benchmark_test.go). diff --git a/example_test.go b/example_test.go index 627d7d9..15288f5 100644 --- a/example_test.go +++ b/example_test.go @@ -6,7 +6,6 @@ import ( "testing" "time" - "github.com/gocql/gocql" "github.com/scylladb/gocqlx" "github.com/scylladb/gocqlx/qb" ) @@ -19,14 +18,6 @@ CREATE TABLE IF NOT EXISTS gocqlx_test.person ( PRIMARY KEY(first_name, last_name) )` -var placeSchema = ` -CREATE TABLE IF NOT EXISTS gocqlx_test.place ( - country text, - city text, - code int, - PRIMARY KEY(country, city) -)` - // 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. type Person struct { @@ -35,175 +26,111 @@ type Person struct { Email []string } -type Place struct { - Country string - City string - TelCode int `db:"code"` -} - func TestExample(t *testing.T) { session := createSession(t) defer session.Close() - mustExec := func(q *gocql.Query) { - if err := q.Exec(); err != nil { - t.Fatal("query:", q, err) + if err := createTable(session, personSchema); err != nil { + t.Fatal("create table:", err) + } + + p := &Person{ + "Patricia", + "Citizen", + []string{"patricia.citzen@gocqlx_test.com"}, + } + + // Insert + { + stmt, names := qb.Insert("gocqlx_test.person").Columns("first_name", "last_name", "email").ToCql() + q := gocqlx.Query(session.Query(stmt), names) + + if err := q.BindStruct(p).Exec(); err != nil { + t.Fatal(err) } } - // Fill person table. + // Insert with TTL { - if err := createTable(session, personSchema); err != nil { - t.Fatal("create table:", err) - } + stmt, names := qb.Insert("gocqlx_test.person").Columns("first_name", "last_name", "email").TTL().ToCql() + q := gocqlx.Query(session.Query(stmt), names) - 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("John", "Doe", []string{"johndoeDNE@gmail.net"})) - q.Release() + if err := q.BindStructMap(p, qb.M{"_ttl": qb.TTL(86400 * time.Second)}).Exec(); err != nil { + t.Fatal(err) + } } - // Fill place table. + // Update { - if err := createTable(session, placeSchema); err != nil { - t.Fatal("create table:", err) - } + p.Email = append(p.Email, "patricia1.citzen@gocqlx_test.com") - q := session.Query("INSERT INTO gocqlx_test.place (country, city, code) VALUES (?, ?, ?)") - mustExec(q.Bind("United States", "New York", 1)) - mustExec(q.Bind("Hong Kong", "", 852)) - mustExec(q.Bind("Singapore", "", 65)) - q.Release() + stmt, names := qb.Update("gocqlx_test.person").Set("email").Where(qb.Eq("first_name"), qb.Eq("last_name")).ToCql() + q := gocqlx.Query(session.Query(stmt), names) + + if err := q.BindStruct(p).Exec(); err != nil { + t.Fatal(err) + } } - // Query the database, storing results in a []Person (wrapped in []interface{}). + // Batch { + i := qb.Insert("gocqlx_test.person").Columns("first_name", "last_name", "email") + + stmt, names := qb.Batch(). + Add("a.", i). + Add("b.", i). + ToCql() + q := gocqlx.Query(session.Query(stmt), names) + + b := struct { + A Person + B Person + }{ + A: Person{ + "Igy", + "Citizen", + []string{"ian.citzen@gocqlx_test.com"}, + }, + B: Person{ + "Ian", + "Citizen", + []string{"igy.citzen@gocqlx_test.com"}, + }, + } + + if err := q.BindStruct(&b).Exec(); err != nil { + t.Fatal(err) + } + } + + // Get + { + var p Person + if err := gocqlx.Get(&p, session.Query("SELECT * FROM gocqlx_test.person WHERE first_name=?", "Patricia")); err != nil { + t.Fatal("get:", err) + } + t.Log(p) + + // {Patricia Citizen [patricia.citzen@gocqlx_test.com patricia1.citzen@gocqlx_test.com]} + } + + // Select + { + stmt, names := qb.Select("gocqlx_test.person").Where(qb.In("first_name")).ToCql() + q := gocqlx.Query(session.Query(stmt), names) + + q.BindMap(qb.M{"first_name": []string{"Patricia", "Igy", "Ian"}}) + if err := q.Err(); err != nil { + t.Fatal(err) + } + var people []Person - if err := gocqlx.Select(&people, session.Query("SELECT * FROM gocqlx_test.person")); err != nil { + if err := gocqlx.Select(&people, q.Query); err != nil { t.Fatal("select:", err) } t.Log(people) - // [{John Doe [johndoeDNE@gmail.net]} {Jason Moiron [jmoiron@jmoiron.net]}] - } - - // Get a single result. - { - var jason Person - if err := gocqlx.Get(&jason, session.Query("SELECT * FROM gocqlx_test.person WHERE first_name=?", "Jason")); err != nil { - t.Fatal("get:", err) - } - t.Log(jason) - - // Jason Moiron [jmoiron@jmoiron.net]} - } - - // Loop through rows using only one struct. - { - var place Place - iter := gocqlx.Iter(session.Query("SELECT * FROM gocqlx_test.place")) - for iter.StructScan(&place) { - t.Log(place) - } - if err := iter.Close(); err != nil { - t.Fatal("iter:", err) - } - iter.ReleaseQuery() - - // {Hong Kong 852} - // {United States New York 1} - // {Singapore 65} - } - - // Query builder, using DSL to build queries, using `:name` as the bindvar. - { - p := &Person{ - "Patricia", - "Citizen", - []string{"patricia.citzen@gocqlx_test.com"}, - } - - // Insert - { - stmt, names := qb.Insert("gocqlx_test.person").Columns("first_name", "last_name", "email").ToCql() - q := gocqlx.Query(session.Query(stmt), names) - - if err := q.BindStruct(p).Exec(); err != nil { - t.Fatal(err) - } - } - - // Insert with TTL - { - stmt, names := qb.Insert("gocqlx_test.person").Columns("first_name", "last_name", "email").TTL().ToCql() - q := gocqlx.Query(session.Query(stmt), names) - - if err := q.BindStructMap(p, qb.M{"_ttl": qb.TTL(86400 * time.Second)}).Exec(); err != nil { - t.Fatal(err) - } - } - - // Update - { - p.Email = append(p.Email, "patricia1.citzen@gocqlx_test.com") - - stmt, names := qb.Update("gocqlx_test.person").Set("email").Where(qb.Eq("first_name"), qb.Eq("last_name")).ToCql() - q := gocqlx.Query(session.Query(stmt), names) - - if err := q.BindStruct(p).Exec(); err != nil { - t.Fatal(err) - } - } - - // Batch - { - i := qb.Insert("gocqlx_test.person").Columns("first_name", "last_name", "email") - - stmt, names := qb.Batch(). - Add("a.", i). - Add("b.", i). - ToCql() - q := gocqlx.Query(session.Query(stmt), names) - - b := struct { - A Person - B Person - }{ - A: Person{ - "Ian", - "Citizen", - []string{"ian.citzen@gocqlx_test.com"}, - }, - B: Person{ - "Igy", - "Citizen", - []string{"igy.citzen@gocqlx_test.com"}, - }, - } - - if err := q.BindStruct(&b).Exec(); err != nil { - t.Fatal(err) - } - } - - // Select - { - stmt, names := qb.Select("gocqlx_test.person").Where(qb.In("first_name")).ToCql() - q := gocqlx.Query(session.Query(stmt), names) - - q.BindMap(qb.M{"first_name": []string{"Patricia", "John"}}) - if err := q.Err(); err != nil { - t.Fatal(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]}] - } + // [{Ian Citizen [igy.citzen@gocqlx_test.com]} {Igy Citizen [ian.citzen@gocqlx_test.com]} {Patricia Citizen [patricia.citzen@gocqlx_test.com patricia1.citzen@gocqlx_test.com]}] } // Named queries, using `:name` as the bindvar.