From 8083fa27ee6ede88fc887debe24ab5c30c9d01bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Matczuk?= Date: Fri, 26 Oct 2018 17:01:00 +0200 Subject: [PATCH] table: introduced table package It adds support for super simple CRUD operations based on table schema model. --- README.md | 74 ++++++-------- example_test.go | 96 +++++++++--------- table/doc.go | 7 ++ table/example_test.go | 100 +++++++++++++++++++ table/table.go | 108 +++++++++++++++++++++ table/table_test.go | 221 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 513 insertions(+), 93 deletions(-) create mode 100644 table/doc.go create mode 100644 table/example_test.go create mode 100644 table/table.go create mode 100644 table/table_test.go diff --git a/README.md b/README.md index 01e4243..f7271c4 100644 --- a/README.md +++ b/README.md @@ -10,47 +10,36 @@ Package `gocqlx` is an idiomatic extension to `gocql` that provides usability fe * Binding query parameters form struct or map * Scanning results directly into struct or slice -* CQL query builder ([see more](https://github.com/scylladb/gocqlx/blob/master/qb)) -* Database migrations ([see more](https://github.com/scylladb/gocqlx/blob/master/migrate)) +* CQL query builder ([package qb](https://github.com/scylladb/gocqlx/blob/master/qb)) +* Super simple CRUD operations based on table model ([package table](https://github.com/scylladb/gocqlx/blob/master/table)) +* Database migrations ([package migrate](https://github.com/scylladb/gocqlx/blob/master/migrate)) * Fast! ## Example ```go -// 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. +// Person represents a row in person table. +// Field names are converted to camel case by default, no need to add special tags. +// If you want to disable a field add `db:"-"` tag, it will not be persisted. type Person struct { FirstName string LastName string Email []string } -// Bind query parameters from a struct. +// Insert, bind data from struct. { - p := Person{ - "Patricia", - "Citizen", - []string{"patricia.citzen@gocqlx_test.com"}, - } + stmt, names := qb.Insert("gocqlx_test.person").Columns("first_name", "last_name", "email").ToCql() + q := gocqlx.Query(session.Query(stmt), names).BindStruct(p) - stmt, names := qb.Insert("gocqlx_test.person"). - Columns("first_name", "last_name", "email"). - ToCql() - - err := gocqlx.Query(session.Query(stmt), names).BindStruct(&p).ExecRelease() - if err != nil { + if err := q.ExecRelease(); err != nil { t.Fatal(err) } } - -// Load the first result into a struct. +// Get first result into a struct. { - stmt, names := qb.Select("gocqlx_test.person"). - Where(qb.Eq("first_name")). - ToCql() - var p Person - + stmt, names := qb.Select("gocqlx_test.person").Where(qb.Eq("first_name")).ToCql() q := gocqlx.Query(session.Query(stmt), names).BindMap(qb.M{ "first_name": "Patricia", }) @@ -58,15 +47,10 @@ type Person struct { t.Fatal(err) } } - // Load all the results into a slice. { - stmt, names := qb.Select("gocqlx_test.person"). - Where(qb.In("first_name")). - ToCql() - var people []Person - + stmt, names := qb.Select("gocqlx_test.person").Where(qb.In("first_name")).ToCql() q := gocqlx.Query(session.Query(stmt), names).BindMap(qb.M{ "first_name": []string{"Patricia", "Igy", "Ian"}, }) @@ -75,27 +59,33 @@ type Person struct { } } -// Use named query parameters. +// metadata specifies table name and columns it must be in sync with schema. +var personMetadata = table.Metadata{ + Name: "person", + Columns: []string{"first_name", "last_name", "email"}, + PartKey: []string{"first_name"}, + SortKey: []string{"last_name"}, +} + +// personTable allows for simple CRUD operations based on personMetadata. +var personTable = table.New(personMetadata) + +// Get by primary key. { - p := &Person{ - "Jane", + p := Person{ + "Patricia", "Citizen", - []string{"jane.citzen@gocqlx_test.com"}, + nil, // no email } - - 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(err) - } - - err = gocqlx.Query(session.Query(stmt), names).BindStruct(p).ExecRelease() - if err != nil { + stmt, names := personTable.Get() // you can filter columns too + q := gocqlx.Query(session.Query(stmt), names).BindStruct(p) + if err := q.GetRelease(&p); err != nil { t.Fatal(err) } } ``` -See more examples in [example_test.go](https://github.com/scylladb/gocqlx/blob/master/example_test.go). +See more examples in [example_test.go](https://github.com/scylladb/gocqlx/blob/master/example_test.go) and [table/example_test.go](https://github.com/scylladb/gocqlx/blob/master/table/example_test.go). ## Performance diff --git a/example_test.go b/example_test.go index f425740..ddb88fc 100644 --- a/example_test.go +++ b/example_test.go @@ -15,7 +15,11 @@ import ( "github.com/scylladb/gocqlx/qb" ) -var personSchema = ` +func TestExample(t *testing.T) { + session := CreateSession(t) + defer session.Close() + + const personSchema = ` CREATE TABLE IF NOT EXISTS gocqlx_test.person ( first_name text, last_name text, @@ -23,41 +27,36 @@ CREATE TABLE IF NOT EXISTS gocqlx_test.person ( PRIMARY KEY(first_name, last_name) )` -// 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 { - FirstName string - LastName string - Email []string -} - -func TestExample(t *testing.T) { - session := CreateSession(t) - defer session.Close() - if err := ExecStmt(session, personSchema); err != nil { t.Fatal("create table:", err) } + // Person represents a row in person table. + // Field names are converted to camel case by default, no need to add special tags. + // If you want to disable a field add `db:"-"` tag, it will not be persisted. + type Person struct { + FirstName string + LastName string + Email []string + } + p := Person{ "Patricia", "Citizen", []string{"patricia.citzen@gocqlx_test.com"}, } - // Bind query parameters from struct. + // Insert, bind data from struct. { - stmt, names := qb.Insert("gocqlx_test.person"). - Columns("first_name", "last_name", "email"). - ToCql() + stmt, names := qb.Insert("gocqlx_test.person").Columns("first_name", "last_name", "email").ToCql() + q := gocqlx.Query(session.Query(stmt), names).BindStruct(p) - err := gocqlx.Query(session.Query(stmt), names).BindStruct(&p).ExecRelease() - if err != nil { + if err := q.ExecRelease(); err != nil { t.Fatal(err) } } - // Bind query parameters from struct and map. + // Insert with TTL and timestamp, bind data from struct and map. { stmt, names := qb.Insert("gocqlx_test.person"). Columns("first_name", "last_name", "email"). @@ -71,7 +70,7 @@ func TestExample(t *testing.T) { } } - // Update with query parameters from struct. + // Update email, bind data from struct. { p.Email = append(p.Email, "patricia1.citzen@gocqlx_test.com") @@ -79,23 +78,23 @@ func TestExample(t *testing.T) { Set("email"). Where(qb.Eq("first_name"), qb.Eq("last_name")). ToCql() + q := gocqlx.Query(session.Query(stmt), names).BindStruct(p) - err := gocqlx.Query(session.Query(stmt), names).BindStruct(p).ExecRelease() - if err != nil { + if err := q.ExecRelease(); err != nil { t.Fatal(err) } } - // Adding and removing elements to collections and counters. + // Add email to a list. { stmt, names := qb.Update("gocqlx_test.person"). AddNamed("email", "new_email"). Where(qb.Eq("first_name"), qb.Eq("last_name")). ToCql() - q := gocqlx.Query(session.Query(stmt), names).BindStructMap(p, qb.M{ "new_email": []string{"patricia2.citzen@gocqlx_test.com", "patricia3.citzen@gocqlx_test.com"}, }) + if err := q.ExecRelease(); err != nil { t.Fatal(err) } @@ -125,52 +124,48 @@ func TestExample(t *testing.T) { []string{"ian.citzen@gocqlx_test.com"}, }, } + q := gocqlx.Query(session.Query(stmt), names).BindStruct(&batch) - err := gocqlx.Query(session.Query(stmt), names).BindStruct(&batch).ExecRelease() - if err != nil { + if err := q.ExecRelease(); err != nil { t.Fatal(err) } } - // Load the first result into a struct. + // Get first result into a struct. { - stmt, names := qb.Select("gocqlx_test.person"). - Where(qb.Eq("first_name")). - ToCql() - var p Person + stmt, names := qb.Select("gocqlx_test.person").Where(qb.Eq("first_name")).ToCql() q := gocqlx.Query(session.Query(stmt), names).BindMap(qb.M{ "first_name": "Patricia", }) + if err := q.GetRelease(&p); err != nil { t.Fatal(err) } t.Log(p) - // {Patricia Citizen [patricia.citzen@gocqlx_test.com patricia1.citzen@gocqlx_test.com patricia2.citzen@gocqlx_test.com patricia3.citzen@gocqlx_test.com]} + // stdout: {Patricia Citizen [patricia.citzen@gocqlx_test.com patricia1.citzen@gocqlx_test.com patricia2.citzen@gocqlx_test.com patricia3.citzen@gocqlx_test.com]} } // Load all the results into a slice. { - stmt, names := qb.Select("gocqlx_test.person"). - Where(qb.In("first_name")). - ToCql() - var people []Person + stmt, names := qb.Select("gocqlx_test.person").Where(qb.In("first_name")).ToCql() q := gocqlx.Query(session.Query(stmt), names).BindMap(qb.M{ "first_name": []string{"Patricia", "Igy", "Ian"}, }) + if err := q.SelectRelease(&people); err != nil { t.Fatal(err) } t.Log(people) - // [{Ian Citizen [ian.citzen@gocqlx_test.com]} {Igy Citizen [igy.citzen@gocqlx_test.com]} {Patricia Citizen [patricia.citzen@gocqlx_test.com patricia1.citzen@gocqlx_test.com patricia2.citzen@gocqlx_test.com patricia3.citzen@gocqlx_test.com]}] + // stdout: [{Ian Citizen [ian.citzen@gocqlx_test.com]} {Igy Citizen [igy.citzen@gocqlx_test.com]} {Patricia Citizen [patricia.citzen@gocqlx_test.com patricia1.citzen@gocqlx_test.com patricia2.citzen@gocqlx_test.com patricia3.citzen@gocqlx_test.com]}] } - // Token based pagination. + // Support for token based pagination. { p := &Person{ "Ian", @@ -183,11 +178,10 @@ func TestExample(t *testing.T) { Where(qb.Token("first_name").Gt()). Limit(10). ToCql() + q := gocqlx.Query(session.Query(stmt), names).BindStruct(p) var people []Person - - err := gocqlx.Query(session.Query(stmt), names).BindStruct(p).SelectRelease(&people) - if err != nil { + if err := q.SelectRelease(&people); err != nil { t.Fatal(err) } @@ -195,21 +189,21 @@ func TestExample(t *testing.T) { // [{Patricia []} {Igy []}] } - // Use named query parameters. + // Support for named parameters in query string. { + const query = "INSERT INTO gocqlx_test.person (first_name, last_name, email) VALUES (:first_name, :last_name, :email)" + stmt, names, err := gocqlx.CompileNamedQuery([]byte(query)) + if err != nil { + t.Fatal(err) + } + p := &Person{ "Jane", "Citizen", []string{"jane.citzen@gocqlx_test.com"}, } - - 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(err) - } - - err = gocqlx.Query(session.Query(stmt), names).BindStruct(p).ExecRelease() - if err != nil { + q := gocqlx.Query(session.Query(stmt), names).BindStruct(p) + if err := q.ExecRelease(); err != nil { t.Fatal(err) } } diff --git a/table/doc.go b/table/doc.go new file mode 100644 index 0000000..fd701dd --- /dev/null +++ b/table/doc.go @@ -0,0 +1,7 @@ +// Copyright (C) 2017 ScyllaDB +// Use of this source code is governed by a ALv2-style +// license that can be found in the LICENSE file. + +// Package table adds support for super simple CRUD operations based on table +// model. +package table diff --git a/table/example_test.go b/table/example_test.go new file mode 100644 index 0000000..43bf69f --- /dev/null +++ b/table/example_test.go @@ -0,0 +1,100 @@ +// Copyright (C) 2017 ScyllaDB +// Use of this source code is governed by a ALv2-style +// license that can be found in the LICENSE file. + +// +build all integration + +package table_test + +import ( + "testing" + + "github.com/scylladb/gocqlx" + . "github.com/scylladb/gocqlx/gocqlxtest" + "github.com/scylladb/gocqlx/qb" + "github.com/scylladb/gocqlx/table" +) + +func TestExample(t *testing.T) { + session := CreateSession(t) + defer session.Close() + + const personSchema = ` +CREATE TABLE IF NOT EXISTS gocqlx_test.person ( + first_name text, + last_name text, + email list, + PRIMARY KEY(first_name, last_name) +)` + if err := ExecStmt(session, personSchema); err != nil { + t.Fatal("create table:", err) + } + + // metadata specifies table name and columns it must be in sync with schema. + var personMetadata = table.Metadata{ + Name: "person", + Columns: []string{"first_name", "last_name", "email"}, + PartKey: []string{"first_name"}, + SortKey: []string{"last_name"}, + } + + // personTable allows for simple CRUD operations based on personMetadata. + var personTable = table.New(personMetadata) + + // Person represents a row in person table. + // Field names are converted to camel case by default, no need to add special tags. + // If you want to disable a field add `db:"-"` tag, it will not be persisted. + type Person struct { + FirstName string + LastName string + Email []string + } + + // Insert, bind data from struct. + { + p := Person{ + "Patricia", + "Citizen", + []string{"patricia.citzen@gocqlx_test.com"}, + } + + stmt, names := personTable.Insert() + q := gocqlx.Query(session.Query(stmt), names).BindStruct(p) + if err := q.ExecRelease(); err != nil { + t.Fatal(err) + } + } + + // Get by primary key. + { + p := Person{ + "Patricia", + "Citizen", + nil, // no email + } + + stmt, names := personTable.Get() // you can filter columns too + q := gocqlx.Query(session.Query(stmt), names).BindStruct(p) + if err := q.GetRelease(&p); err != nil { + t.Fatal(err) + } + + t.Log(p) + // stdout: {Patricia Citizen [patricia.citzen@gocqlx_test.com]} + } + + // Load all rows in a partition to a slice. + { + var people []Person + + stmt, names := personTable.Select() // you can filter columns too + q := gocqlx.Query(session.Query(stmt), names).BindMap(qb.M{"first_name": "Patricia"}) + + if err := q.SelectRelease(&people); err != nil { + t.Fatal(err) + } + + t.Log(people) + // stdout: [{Patricia Citizen [patricia.citzen@gocqlx_test.com]}] + } +} diff --git a/table/table.go b/table/table.go new file mode 100644 index 0000000..a4307a6 --- /dev/null +++ b/table/table.go @@ -0,0 +1,108 @@ +// Copyright (C) 2017 ScyllaDB +// Use of this source code is governed by a ALv2-style +// license that can be found in the LICENSE file. + +package table + +import "github.com/scylladb/gocqlx/qb" + +// Metadata represents table schema. +type Metadata struct { + Name string + Columns []string + PartKey []string + SortKey []string +} + +type cql struct { + stmt string + names []string +} + +// Table allows for simple CRUD operations, it's backed by query builders from +// gocqlx/qb package. +type Table struct { + metadata Metadata + primaryKeyCmp []qb.Cmp + partKeyCmp []qb.Cmp + + get cql + sel cql + insert cql +} + +// New creates new Table based on table schema read from Metadata. +func New(m Metadata) *Table { + t := &Table{ + metadata: m, + } + + // prepare primary and partition key comparators + t.primaryKeyCmp = make([]qb.Cmp, 0, len(m.PartKey)+len(m.SortKey)) + for _, k := range m.PartKey { + t.primaryKeyCmp = append(t.primaryKeyCmp, qb.Eq(k)) + } + for _, k := range m.SortKey { + t.primaryKeyCmp = append(t.primaryKeyCmp, qb.Eq(k)) + } + t.partKeyCmp = t.primaryKeyCmp[:len(t.metadata.PartKey)] + + // prepare get stmt + t.get.stmt, t.get.names = qb.Select(m.Name).Where(t.primaryKeyCmp...).ToCql() + // prepare select stmt + t.sel.stmt, t.sel.names = qb.Select(m.Name).Where(t.partKeyCmp...).ToCql() + // prepare insert stmt + t.insert.stmt, t.insert.names = qb.Insert(m.Name).Columns(m.Columns...).ToCql() + + return t +} + +// Name returns table name. +func (t *Table) Name() string { + return t.metadata.Name +} + +// Get returns select by primary key statement. +func (t *Table) Get(columns ...string) (stmt string, names []string) { + if len(columns) == 0 { + return t.get.stmt, t.get.names + } + + return qb.Select(t.metadata.Name). + Columns(columns...). + Where(t.primaryKeyCmp...). + ToCql() +} + +// Select returns select by partition key statement. +func (t *Table) Select(columns ...string) (stmt string, names []string) { + if len(columns) == 0 { + return t.sel.stmt, t.sel.names + } + + return qb.Select(t.metadata.Name). + Columns(columns...). + Where(t.primaryKeyCmp[0:len(t.metadata.PartKey)]...). + ToCql() +} + +// SelectBuilder returns a builder initialised to select by partition key +// statement. +func (t *Table) SelectBuilder(columns ...string) *qb.SelectBuilder { + return qb.Select(t.metadata.Name).Columns(columns...).Where(t.partKeyCmp...) +} + +// Insert returns insert all columns statement. +func (t *Table) Insert() (stmt string, names []string) { + return t.insert.stmt, t.insert.names +} + +// Update returns update by primary key statement. +func (t *Table) Update(columns ...string) (stmt string, names []string) { + return qb.Update(t.metadata.Name).Set(columns...).Where(t.primaryKeyCmp...).ToCql() +} + +// Delete returns delete by primary key statement. +func (t *Table) Delete(columns ...string) (stmt string, names []string) { + return qb.Delete(t.metadata.Name).Columns(columns...).Where(t.primaryKeyCmp...).ToCql() +} diff --git a/table/table_test.go b/table/table_test.go new file mode 100644 index 0000000..8afea64 --- /dev/null +++ b/table/table_test.go @@ -0,0 +1,221 @@ +// Copyright (C) 2017 ScyllaDB +// Use of this source code is governed by a ALv2-style +// license that can be found in the LICENSE file. + +package table + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestTableGet(t *testing.T) { + table := []struct { + M Metadata + C []string + N []string + S string + }{ + { + M: Metadata{ + Name: "table", + Columns: []string{"a", "b", "c", "d"}, + PartKey: []string{"a"}, + SortKey: []string{"b"}, + }, + N: []string{"a", "b"}, + S: "SELECT * FROM table WHERE a=? AND b=? ", + }, + { + M: Metadata{ + Name: "table", + Columns: []string{"a", "b", "c", "d"}, + PartKey: []string{"a"}, + }, + N: []string{"a"}, + S: "SELECT * FROM table WHERE a=? ", + }, + { + M: Metadata{ + Name: "table", + Columns: []string{"a", "b", "c", "d"}, + PartKey: []string{"a"}, + }, + C: []string{"d"}, + N: []string{"a"}, + S: "SELECT d FROM table WHERE a=? ", + }, + } + + for _, test := range table { + stmt, names := New(test.M).Get(test.C...) + if diff := cmp.Diff(test.S, stmt); diff != "" { + t.Error(diff) + } + if diff := cmp.Diff(test.N, names); diff != "" { + t.Error(diff, names) + } + } +} + +func TestTableSelect(t *testing.T) { + table := []struct { + M Metadata + C []string + N []string + S string + }{ + { + M: Metadata{ + Name: "table", + Columns: []string{"a", "b", "c", "d"}, + PartKey: []string{"a"}, + SortKey: []string{"b"}, + }, + N: []string{"a"}, + S: "SELECT * FROM table WHERE a=? ", + }, + { + M: Metadata{ + Name: "table", + Columns: []string{"a", "b", "c", "d"}, + PartKey: []string{"a"}, + SortKey: []string{"b"}, + }, + C: []string{"d"}, + N: []string{"a"}, + S: "SELECT d FROM table WHERE a=? ", + }, + } + + for _, test := range table { + stmt, names := New(test.M).Select(test.C...) + if diff := cmp.Diff(test.S, stmt); diff != "" { + t.Error(diff) + } + if diff := cmp.Diff(test.N, names); diff != "" { + t.Error(diff, names) + } + } + + // run SelectBuilder on the same data set + for _, test := range table { + stmt, names := New(test.M).SelectBuilder(test.C...).ToCql() + if diff := cmp.Diff(test.S, stmt); diff != "" { + t.Error(diff) + } + if diff := cmp.Diff(test.N, names); diff != "" { + t.Error(diff, names) + } + } +} + +func TestTableInsert(t *testing.T) { + table := []struct { + M Metadata + N []string + S string + }{ + { + M: Metadata{ + Name: "table", + Columns: []string{"a", "b", "c", "d"}, + PartKey: []string{"a"}, + SortKey: []string{"b"}, + }, + N: []string{"a", "b", "c", "d"}, + S: "INSERT INTO table (a,b,c,d) VALUES (?,?,?,?) ", + }, + } + + for _, test := range table { + stmt, names := New(test.M).Insert() + if diff := cmp.Diff(test.S, stmt); diff != "" { + t.Error(diff) + } + if diff := cmp.Diff(test.N, names); diff != "" { + t.Error(diff, names) + } + } +} + +func TestTableUpdate(t *testing.T) { + table := []struct { + M Metadata + C []string + N []string + S string + }{ + { + M: Metadata{ + Name: "table", + Columns: []string{"a", "b", "c", "d"}, + PartKey: []string{"a"}, + SortKey: []string{"b"}, + }, + C: []string{"d"}, + N: []string{"d", "a", "b"}, + S: "UPDATE table SET d=? WHERE a=? AND b=? ", + }, + } + + for _, test := range table { + stmt, names := New(test.M).Update(test.C...) + if diff := cmp.Diff(test.S, stmt); diff != "" { + t.Error(diff) + } + if diff := cmp.Diff(test.N, names); diff != "" { + t.Error(diff, names) + } + } +} + +func TestTableDelete(t *testing.T) { + table := []struct { + M Metadata + C []string + N []string + S string + }{ + { + M: Metadata{ + Name: "table", + Columns: []string{"a", "b", "c", "d"}, + PartKey: []string{"a"}, + SortKey: []string{"b"}, + }, + N: []string{"a", "b"}, + S: "DELETE FROM table WHERE a=? AND b=? ", + }, + { + M: Metadata{ + Name: "table", + Columns: []string{"a", "b", "c", "d"}, + PartKey: []string{"a"}, + }, + N: []string{"a"}, + S: "DELETE FROM table WHERE a=? ", + }, + { + M: Metadata{ + Name: "table", + Columns: []string{"a", "b", "c", "d"}, + PartKey: []string{"a"}, + }, + C: []string{"d"}, + N: []string{"a"}, + S: "DELETE d FROM table WHERE a=? ", + }, + } + + for _, test := range table { + stmt, names := New(test.M).Delete(test.C...) + if diff := cmp.Diff(test.S, stmt); diff != "" { + t.Error(diff) + } + if diff := cmp.Diff(test.N, names); diff != "" { + t.Error(diff, names) + } + } +}