diff --git a/go.mod b/go.mod index 3a8fb2e..6c6fc66 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,19 @@ module github.com/Drafteame/cassandra-builder go 1.18 require ( + github.com/avast/retry-go/v4 v4.3.3 github.com/gocql/gocql v1.3.1 github.com/magefile/mage v1.14.0 github.com/scylladb/gocqlx v1.5.0 + github.com/stretchr/testify v1.8.1 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d2d0bc8..67cbeed 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,11 @@ +github.com/avast/retry-go/v4 v4.3.3 h1:G56Bp6mU0b5HE1SkaoVjscZjlQb0oy4mezwY/cGH19w= +github.com/avast/retry-go/v4 v4.3.3/go.mod h1:rg6XFaiuFYII0Xu3RDbZQkxCofFwruZKW8oEF1jpWiU= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gocql/gocql v0.0.0-20200131111108-92af2e088537/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY= github.com/gocql/gocql v1.3.1 h1:BTwM4rux+ah5G3oH6/MQa+tur/TDd/XAAOXDxBBs7rg= @@ -23,11 +26,24 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/scylladb/go-reflectx v1.0.1/go.mod h1:rWnOfDIRWBGN0miMLIcoPt/Dhi2doCMZqwMCJ3KupFc= github.com/scylladb/gocqlx v1.5.0 h1:p7NEqRaCMAtW2nvq62iyUNXmIYP29373YOC7D2Xd7Qg= github.com/scylladb/gocqlx v1.5.0/go.mod h1:QarZcw5kpYh31MXfxiN2JWWvF1cgZbYqfTfXwmwhpEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/qb/client.go b/qb/client.go index 91e99d2..a96f015 100644 --- a/qb/client.go +++ b/qb/client.go @@ -5,68 +5,45 @@ import ( "github.com/Drafteame/cassandra-builder/qb/qcount" "github.com/Drafteame/cassandra-builder/qb/qdelete" + + models "github.com/Drafteame/cassandra-builder/qb/models" "github.com/Drafteame/cassandra-builder/qb/qinsert" "github.com/Drafteame/cassandra-builder/qb/qselect" - "github.com/Drafteame/cassandra-builder/qb/query" "github.com/Drafteame/cassandra-builder/qb/qupdate" ) type client struct { + canRestart bool + config models.Config session *gocql.Session - debug bool - printQuery query.DebugPrint -} - -// NewClient creates a new cassandra client manager from config -func NewClient(conf Config) (Client, error) { - session, err := getSession(conf) - if err != nil { - return nil, err - } - - c := &client{session: session, debug: conf.Debug, printQuery: query.DefaultDebugPrint} - - if conf.PrintQuery != nil { - c.printQuery = conf.PrintQuery - } - - return c, nil -} - -// NewClientWithSession creates a new cassandra client manager from a given session. -func NewClientWithSession(session *gocql.Session, conf Config) (Client, error) { - c := &client{session: session, debug: conf.Debug, printQuery: query.DefaultDebugPrint} - - if conf.PrintQuery != nil { - c.printQuery = conf.PrintQuery - } - - return c, nil } var _ Client = &client{} func (c *client) Select(f ...string) *qselect.Query { - return qselect.New(c.session, c.debug, c.printQuery).Fields(f...) + return qselect.New(c).Fields(f...) } func (c *client) Insert(f ...string) *qinsert.Query { - return qinsert.New(c.session, c.debug, c.printQuery).Fields(f...) + return qinsert.New(c).Fields(f...) } func (c *client) Update(t string) *qupdate.Query { - return qupdate.New(c.session, c.debug, c.printQuery).Table(t) + return qupdate.New(c).Table(t) } func (c *client) Delete() *qdelete.Query { - return qdelete.New(c.session, c.debug, c.printQuery) + return qdelete.New(c) } func (c *client) Count() *qcount.Query { - return qcount.New(c.session, c.debug, c.printQuery) + return qcount.New(c) +} + +func (c *client) Debug() bool { + return c.config.Debug } -// Close finish cassandra session func (c *client) Close() { c.session.Close() } @@ -75,7 +52,24 @@ func (c *client) Session() *gocql.Session { return c.session } -func getSession(c Config) (*gocql.Session, error) { +func (c *client) Config() models.Config { + return c.config +} + +func (c *client) Restart() error { + c.Close() + + session, err := createSession(c.config) + if err != nil { + return err + } + + c.session = session + + return nil +} + +func createSession(c models.Config) (*gocql.Session, error) { cluster := gocql.NewCluster(c.ContactPoints...) cluster.Keyspace = c.KeyspaceName cluster.Consistency = gocql.Consistency(c.Consistency) @@ -112,3 +106,26 @@ func getSession(c Config) (*gocql.Session, error) { return cluster.CreateSession() } + +// NewClient creates a new cassandra client manager from config +func NewClient(conf models.Config) (Client, error) { + session, err := createSession(conf) + if err != nil { + return nil, err + } + + return &client{ + session: session, + config: conf, + canRestart: true, + }, nil +} + +// NewClientWithSession creates a new cassandra client manager from a given session. +func NewClientWithSession(session *gocql.Session, conf models.Config) Client { + return &client{ + session: session, + config: conf, + canRestart: false, + } +} diff --git a/qb/errors/errors.go b/qb/errors/errors.go new file mode 100644 index 0000000..a1f33e7 --- /dev/null +++ b/qb/errors/errors.go @@ -0,0 +1,13 @@ +package errors + +import "errors" + +var ( + ErrNilBinding = errors.New("cassandra-builder: nil bind is not allowed") + ErrNoPtrBinding = errors.New("cassandra-builder: bind value should be a pointer") + ErrNoStructOrSliceBinding = errors.New("cassandra-builder: bind value should be a struct or slice") + ErrNoSliceOfStructsBinding = errors.New("cassandra-builder: bind value should be a slice of structs") + ErrClosedConnection = errors.New("cassandra-builder: can execute on closed connection") + ErrNilIterator = errors.New("cassandra-builder: nil iterator is not allowed") + ErrParsing = errors.New("cassandra-builder: error parsing row") +) diff --git a/qb/models/config.go b/qb/models/config.go new file mode 100644 index 0000000..1bdf82c --- /dev/null +++ b/qb/models/config.go @@ -0,0 +1,36 @@ +package models + +import ( + "time" +) + +type Consistency uint16 + +const ( + Any Consistency = 0x00 + One Consistency = 0x01 + Two Consistency = 0x02 + Three Consistency = 0x03 + Quorum Consistency = 0x04 + All Consistency = 0x05 + LocalQuorum Consistency = 0x06 + EachQuorum Consistency = 0x07 + LocalOne Consistency = 0x0A +) + +// Config is the main cassandra configuration needed +type Config struct { + Port int + KeyspaceName string + Username string + Password string + ContactPoints []string + Debug bool + ProtoVersion int + Consistency Consistency + CaPath string + DisableInitialHostLookup bool + Timeout time.Duration + ConnectTimeout time.Duration + NumRetries uint +} diff --git a/qb/qb.go b/qb/qb.go index b56e9bb..87c2d64 100644 --- a/qb/qb.go +++ b/qb/qb.go @@ -1,49 +1,16 @@ package qb import ( - "time" - "github.com/gocql/gocql" + models "github.com/Drafteame/cassandra-builder/qb/models" "github.com/Drafteame/cassandra-builder/qb/qcount" delete2 "github.com/Drafteame/cassandra-builder/qb/qdelete" "github.com/Drafteame/cassandra-builder/qb/qinsert" _select "github.com/Drafteame/cassandra-builder/qb/qselect" - "github.com/Drafteame/cassandra-builder/qb/query" "github.com/Drafteame/cassandra-builder/qb/qupdate" ) -type Consistency uint16 - -const ( - Any Consistency = 0x00 - One Consistency = 0x01 - Two Consistency = 0x02 - Three Consistency = 0x03 - Quorum Consistency = 0x04 - All Consistency = 0x05 - LocalQuorum Consistency = 0x06 - EachQuorum Consistency = 0x07 - LocalOne Consistency = 0x0A -) - -// Config is the main cassandra configuration needed -type Config struct { - Port int `yaml:"port" json:"port"` - KeyspaceName string `yaml:"keyspace_name" json:"keyspace_name"` - Username string `yaml:"username" json:"username"` - Password string `yaml:"password" json:"password"` - ContactPoints []string `yaml:"contact_points" json:"contact_points"` - Debug bool `yaml:"debug" json:"debug"` - ProtoVersion int `yaml:"proto_version" json:"proto_version"` - Consistency Consistency `yaml:"consistency" json:"consistency"` - CaPath string `yaml:"ca_path" json:"ca_path"` - DisableInitialHostLookup bool `yaml:"disable_initial_host_lookup" json:"disable_initial_host_lookup"` - Timeout time.Duration `yaml:"timeout" json:"timeout"` - ConnectTimeout time.Duration `yaml:"connect_timeout" json:"connect_timeout"` - PrintQuery query.DebugPrint -} - // Client is the main cassandra client abstraction to work with the database type Client interface { // Select start a select query @@ -64,6 +31,15 @@ type Client interface { // Session return the plain session object to build some direct query Session() *gocql.Session - // Close close cassandra connection pool + // Debug return an assertion for debugging + Debug() bool + + // Restart should close and start a new connection. + Restart() error + + // Config return current client configuration + Config() models.Config + + // Close ends cassandra connection pool Close() } diff --git a/qb/qcount/count.go b/qb/qcount/count.go index b2d6708..b1fb90a 100644 --- a/qb/qcount/count.go +++ b/qb/qcount/count.go @@ -1,14 +1,13 @@ package qcount import ( - "github.com/gocql/gocql" - "github.com/Drafteame/cassandra-builder/qb/query" + "github.com/Drafteame/cassandra-builder/qb/runner" ) // Query create new select count query type Query struct { - ctx query.Query + client runner.Client table string column string where []query.WhereStm @@ -17,12 +16,8 @@ type Query struct { } // New create a new count query instance by passing a cassandra session -func New(s *gocql.Session, d bool, dp query.DebugPrint) *Query { - return &Query{ctx: query.Query{ - Session: s, - Debug: d, - PrintQuery: dp, - }} +func New(c runner.Client) *Query { + return &Query{client: c} } // Column set count column of the query diff --git a/qb/qcount/count_exec.go b/qb/qcount/count_exec.go index ca13434..c9827e1 100644 --- a/qb/qcount/count_exec.go +++ b/qb/qcount/count_exec.go @@ -3,23 +3,19 @@ package qcount import ( "strings" - "github.com/gocql/gocql" "github.com/scylladb/gocqlx/qb" "github.com/Drafteame/cassandra-builder/qb/query" + "github.com/Drafteame/cassandra-builder/qb/runner" ) // Exec release count query an return the number of rows and a possible error func (cq *Query) Exec() (int64, error) { - q := cq.build() - - var count int64 + run := runner.New(cq.client) - if err := cq.ctx.Session.Query(q, cq.args...).Consistency(gocql.One).Scan(&count); err != nil { - return 0, err - } + q := cq.build() - return count, nil + return run.QueryCount(q, cq.args) } func (cq *Query) build() string { @@ -35,9 +31,5 @@ func (cq *Query) build() string { queryStr, _ := q.ToCql() - if cq.ctx.Debug { - cq.ctx.PrintQuery(queryStr, cq.args) - } - return strings.TrimSpace(queryStr) } diff --git a/qb/qcount/count_exec_test.go b/qb/qcount/count_exec_test.go index f500dfc..87a4e3f 100644 --- a/qb/qcount/count_exec_test.go +++ b/qb/qcount/count_exec_test.go @@ -4,9 +4,8 @@ import ( "reflect" "testing" - "github.com/gocql/gocql" - "github.com/Drafteame/cassandra-builder/qb/query" + "github.com/Drafteame/cassandra-builder/qb/test/mocks" ) func TestQuery_build(t *testing.T) { @@ -76,7 +75,9 @@ func TestQuery_build(t *testing.T) { } for i, test := range tt { - q := New(&gocql.Session{}, false, nil).From(test.table).Column(test.column) + client := mocks.NewClient(t) + + q := New(client).From(test.table).Column(test.column) for _, w := range test.where { q = q.Where(w.Field, w.Op, w.Value) diff --git a/qb/qcount/count_test.go b/qb/qcount/count_test.go index da2f72e..72e89d4 100644 --- a/qb/qcount/count_test.go +++ b/qb/qcount/count_test.go @@ -4,18 +4,18 @@ import ( "reflect" "testing" - "github.com/gocql/gocql" - "github.com/Drafteame/cassandra-builder/qb/query" + "github.com/Drafteame/cassandra-builder/qb/test/mocks" + + "github.com/stretchr/testify/assert" ) func TestNew(t *testing.T) { - s := &gocql.Session{} - q := New(s, false, nil) + client := mocks.NewClient(t) + queue := New(client) + + assert.Equal(t, client, queue.client) - if !reflect.DeepEqual(q.ctx.Session, s) { - t.Errorf("associated session is different") - } } func TestQuery_From(t *testing.T) { @@ -23,12 +23,12 @@ func TestQuery_From(t *testing.T) { q.From("test") if q.table != "test" { - t.Errorf("exp: test got: %v", q.table) + t.Fatalf("exp: test got: %v", q.table) } q.From("test2") if q.table != "test2" { - t.Errorf("exp: test2 got: %v", q.table) + t.Fatalf("exp: test2 got: %v", q.table) } } @@ -37,12 +37,12 @@ func TestQuery_Column(t *testing.T) { q.Column("field") if q.column != "field" { - t.Errorf("exp: field got: %v", q.column) + t.Fatalf("exp: field got: %v", q.column) } q.Column("field2") if q.column != "field2" { - t.Errorf("exp: field2 got: %v", q.column) + t.Fatalf("exp: field2 got: %v", q.column) } } @@ -101,11 +101,11 @@ func TestQuery_Where(t *testing.T) { q.Where(test.field, test.op, test.value) if !reflect.DeepEqual(q.args, test.expArgs) { - t.Errorf("exp: %v got: %v", test.expArgs, q.args) + t.Fatalf("exp: %v got: %v", test.expArgs, q.args) } if !reflect.DeepEqual(q.where, test.expStm) { - t.Errorf("exp: %v got: %v", test.expStm, q.where) + t.Fatalf("exp: %v got: %v", test.expStm, q.where) } } } diff --git a/qb/qdelete/delete.go b/qb/qdelete/delete.go index 0b35fcd..b0b0c0a 100644 --- a/qb/qdelete/delete.go +++ b/qb/qdelete/delete.go @@ -1,26 +1,21 @@ package qdelete import ( - "github.com/gocql/gocql" - "github.com/Drafteame/cassandra-builder/qb/query" + "github.com/Drafteame/cassandra-builder/qb/runner" ) // Query represents a Cassandra delete query. Execution should not bind any value type Query struct { - ctx query.Query - table string - where []query.WhereStm - args []interface{} + client runner.Client + table string + where []query.WhereStm + args []interface{} } // New create a new delete query instance by passing a cassandra session -func New(s *gocql.Session, d bool, dp query.DebugPrint) *Query { - return &Query{ctx: query.Query{ - Session: s, - Debug: d, - PrintQuery: dp, - }} +func New(c runner.Client) *Query { + return &Query{client: c} } // From set table where be data deleted diff --git a/qb/qdelete/delete_exec.go b/qb/qdelete/delete_exec.go index 2ea2053..87c783e 100644 --- a/qb/qdelete/delete_exec.go +++ b/qb/qdelete/delete_exec.go @@ -6,13 +6,16 @@ import ( "github.com/scylladb/gocqlx/qb" "github.com/Drafteame/cassandra-builder/qb/query" + "github.com/Drafteame/cassandra-builder/qb/runner" ) // Exec execute delete query and return error on failure func (dq *Query) Exec() error { + run := runner.New(dq.client) + q := dq.build() - if err := dq.ctx.Session.Query(q, dq.args...).Exec(); err != nil { + if err := run.QueryNone(q, dq.args); err != nil { return err } @@ -28,9 +31,5 @@ func (dq *Query) build() string { queryStr, _ := q.ToCql() - if dq.ctx.Debug { - dq.ctx.PrintQuery(queryStr, dq.args) - } - return strings.TrimSpace(queryStr) } diff --git a/qb/qinsert/insert.go b/qb/qinsert/insert.go index 8138406..5f8d632 100644 --- a/qb/qinsert/insert.go +++ b/qb/qinsert/insert.go @@ -1,26 +1,21 @@ package qinsert import ( - "github.com/gocql/gocql" - "github.com/Drafteame/cassandra-builder/qb/query" + "github.com/Drafteame/cassandra-builder/qb/runner" ) // Query represent a Cassandra insert query. Execution should not bind any value type Query struct { - ctx query.Query + client runner.Client table string fields query.Columns args []interface{} } // New creates a new insert query by passing a cassandra session and debug options -func New(s *gocql.Session, d bool, dp query.DebugPrint) *Query { - return &Query{ctx: query.Query{ - Session: s, - Debug: d, - PrintQuery: dp, - }} +func New(c runner.Client) *Query { + return &Query{client: c} } // Fields save query fields that should be used for insert query diff --git a/qb/qinsert/insert_exec.go b/qb/qinsert/insert_exec.go index 2de68ad..d94bc4f 100644 --- a/qb/qinsert/insert_exec.go +++ b/qb/qinsert/insert_exec.go @@ -3,14 +3,16 @@ package qinsert import ( "strings" + "github.com/Drafteame/cassandra-builder/qb/runner" "github.com/scylladb/gocqlx/qb" ) // Exec execute insert query with args func (iq *Query) Exec() error { + run := runner.New(iq.client) q := iq.build() - if err := iq.ctx.Session.Query(q, iq.args...).Exec(); err != nil { + if err := run.QueryNone(q, iq.args); err != nil { return err } @@ -23,9 +25,5 @@ func (iq *Query) build() string { queryStr, _ := q.ToCql() - if iq.ctx.Debug { - iq.ctx.PrintQuery(queryStr, iq.args) - } - return strings.TrimSpace(queryStr) } diff --git a/qb/qinsert/insert_exec_test.go b/qb/qinsert/insert_exec_test.go index 0b4bf0c..0926354 100644 --- a/qb/qinsert/insert_exec_test.go +++ b/qb/qinsert/insert_exec_test.go @@ -3,6 +3,8 @@ package qinsert import ( "reflect" "testing" + + "github.com/Drafteame/cassandra-builder/qb/test/mocks" ) func TestInsertQuery_build(t *testing.T) { @@ -30,7 +32,9 @@ func TestInsertQuery_build(t *testing.T) { } for _, test := range tt { - q := New(nil, false, nil).Fields(test.fields...).Into(test.table).Values(test.values...) + client := mocks.NewClient(t) + + q := New(client).Fields(test.fields...).Into(test.table).Values(test.values...) query := q.build() if query != test.res { diff --git a/qb/qselect/build.go b/qb/qselect/build.go index 7093f89..6cdf381 100644 --- a/qb/qselect/build.go +++ b/qb/qselect/build.go @@ -48,9 +48,5 @@ func (q *Query) build() string { queryStr, _ := sb.Json().ToCql() - if q.ctx.Debug { - q.ctx.PrintQuery(queryStr, q.args) - } - return strings.TrimSpace(queryStr) } diff --git a/qb/qselect/select.go b/qb/qselect/select.go index ca4db80..53610f6 100644 --- a/qb/qselect/select.go +++ b/qb/qselect/select.go @@ -1,14 +1,13 @@ package qselect import ( - "github.com/gocql/gocql" - "github.com/Drafteame/cassandra-builder/qb/query" + "github.com/Drafteame/cassandra-builder/qb/runner" ) // Query represents a cassandra select statement and his options type Query struct { - ctx query.Query + client runner.Client fields query.Columns args []interface{} table string @@ -22,12 +21,8 @@ type Query struct { } // New create a new select query by passing a cassandra session and debug options -func New(s *gocql.Session, d bool, dp query.DebugPrint) *Query { - return &Query{ctx: query.Query{ - Session: s, - Debug: d, - PrintQuery: dp, - }} +func New(c runner.Client) *Query { + return &Query{client: c} } // Fields save query fields that should be used for select query diff --git a/qb/qselect/select_exec.go b/qb/qselect/select_exec.go index b6e1dba..e986c4f 100644 --- a/qb/qselect/select_exec.go +++ b/qb/qselect/select_exec.go @@ -1,18 +1,17 @@ package qselect import ( - "errors" "reflect" - "github.com/gocql/gocql" - + "github.com/Drafteame/cassandra-builder/qb/errors" "github.com/Drafteame/cassandra-builder/qb/query" + "github.com/Drafteame/cassandra-builder/qb/runner" ) // One return just one result on bind action func (q *Query) One() error { if q.bind == nil { - return errors.New("nil bind is not allowed, use None() functions instead One()") + return errors.ErrNilBinding } if err := query.VerifyBind(q.bind, reflect.Struct); err != nil { @@ -21,9 +20,10 @@ func (q *Query) One() error { sq := q.build() - var jsonRow string + run := runner.New(q.client) - if err := q.ctx.Session.Query(sq, q.args...).Consistency(gocql.One).Scan(&jsonRow); err != nil { + jsonRow, err := run.QueryOne(sq, q.args) + if err != nil { return err } @@ -44,8 +44,9 @@ func (q *Query) One() error { // All return all rows on bind action. Bind should be a slice of structs func (q *Query) All() error { + run := runner.New(q.client) if q.bind == nil { - return errors.New("nil bind is not allowed, use None() function instead All()") + return errors.ErrNilBinding } if err := query.VerifyBind(q.bind, reflect.Slice); err != nil { @@ -54,35 +55,5 @@ func (q *Query) All() error { sq := q.build() - var jsonRow string - - iter := q.ctx.Session.Query(sq, q.args...).Iter() - defer func() { _ = iter.Close() }() - - ib := reflect.Indirect(reflect.ValueOf(q.bind)) - - bv := reflect.ValueOf(ib.Interface()) - bt := bv.Type().Elem() - - for iter.Scan(&jsonRow) { - elem, err := query.BindRow([]byte(jsonRow), bt) - if err != nil { - return err - } - - ib.Set(reflect.Append(ib, reflect.Indirect(elem))) - } - - return nil -} - -// None execute a qselect query without returning values -func (q *Query) None() error { - sq := q.build() - - if err := q.ctx.Session.Query(sq, q.args...).Exec(); err != nil { - return err - } - - return nil + return run.Query(sq, q.args, q.bind) } diff --git a/qb/query/query.go b/qb/query/query.go index 45f9cfa..f843ee9 100644 --- a/qb/query/query.go +++ b/qb/query/query.go @@ -2,13 +2,11 @@ package query import ( "encoding/json" - "errors" "fmt" - "log" "reflect" "time" - "github.com/gocql/gocql" + "github.com/Drafteame/cassandra-builder/qb/errors" ) type ( @@ -19,14 +17,7 @@ type ( Order string // DebugPrint defines a callback that prints query values - DebugPrint func(q string, args []interface{}) - - // Query Base definition of query - Query struct { - Session *gocql.Session - Debug bool - PrintQuery DebugPrint - } + DebugPrint func(q string, args []interface{}, err error) ) const ( @@ -38,26 +29,20 @@ const ( DatetimeLayout string = "2006-01-02 15:04:05.000Z" ) -// DefaultDebugPrint defines a default function that prints resultant query and arguments before being executed -// and when the Debug flag is true -func DefaultDebugPrint(q string, args []interface{}) { - log.Printf("query: %v \nargs: %v\n", q, args) -} - // VerifyBind verify if an interface is bindable or not by checking it is a Ptr kind func VerifyBind(b interface{}, k reflect.Kind) error { t := reflect.TypeOf(b) v := reflect.ValueOf(b) if t.Kind() != reflect.Ptr { - return errors.New("bind value should be a pointer") + return errors.ErrNoPtrBinding } str := reflect.Indirect(v).Interface() t = reflect.TypeOf(str) if t.Kind() != k { - return errors.New("bind value needs to be a struct to get one value, or a slice of structs to get many") + return errors.ErrNoStructOrSliceBinding } return nil @@ -69,11 +54,11 @@ func BindMapToStruct(m map[string]interface{}, st reflect.Value) error { indTyp := indVal.Type() if st.Kind() != reflect.Ptr { - return errors.New("bind should be a slice of struct pointers with `gocql` tags") + return errors.ErrNoPtrBinding } if indVal.Kind() != reflect.Struct { - return errors.New("bind should be a slice of struct pointers with `gocql` tags - 2") + return errors.ErrNoSliceOfStructsBinding } numField := indTyp.NumField() @@ -86,9 +71,8 @@ func BindMapToStruct(m map[string]interface{}, st reflect.Value) error { mv, ok := m[tagField] if tagField != "" && ok { - err := CastMapValue(mv, field.Type, value) - if err != nil { - fmt.Printf("err: %v\n", err) + if err := CastMapValue(mv, field.Type, value); err != nil { + return err } } } @@ -117,7 +101,7 @@ func CastMapValue(mv interface{}, t reflect.Type, v reflect.Value) error { t, _ := time.Parse(DatetimeLayout, mv.(string)) v.Set(reflect.ValueOf(t)) default: - return fmt.Errorf("can't cast value of type %T with value %v, to type %v", mv, mv, t) + return fmt.Errorf("cassandra-builder: can't cast value of type %T with value %v, to type %v", mv, mv, t) } return nil diff --git a/qb/qupdate/update.go b/qb/qupdate/update.go index 026abd9..9fbcb4a 100644 --- a/qb/qupdate/update.go +++ b/qb/qupdate/update.go @@ -1,14 +1,13 @@ package qupdate import ( - "github.com/gocql/gocql" - "github.com/Drafteame/cassandra-builder/qb/query" + "github.com/Drafteame/cassandra-builder/qb/runner" ) // Query represent a Cassandra update query. Execution should not bind any value type Query struct { - ctx query.Query + client runner.Client table string fields query.Columns args []interface{} @@ -16,12 +15,8 @@ type Query struct { } // New create a new update query by passing a cassandra session and the affected table -func New(s *gocql.Session, d bool, dp query.DebugPrint) *Query { - return &Query{ctx: query.Query{ - Session: s, - Debug: d, - PrintQuery: dp, - }} +func New(c runner.Client) *Query { + return &Query{client: c} } // Table set the table name to affect with the update query diff --git a/qb/qupdate/update_exec.go b/qb/qupdate/update_exec.go index 5bc44db..0005567 100644 --- a/qb/qupdate/update_exec.go +++ b/qb/qupdate/update_exec.go @@ -6,20 +6,22 @@ import ( "github.com/scylladb/gocqlx/qb" "github.com/Drafteame/cassandra-builder/qb/query" + "github.com/Drafteame/cassandra-builder/qb/runner" ) // Exec run update query from builder and return an error if exists func (uq *Query) Exec() error { - q := uq.build() + run := runner.New(uq.client) + q := uq.Build() - if err := uq.ctx.Session.Query(q, uq.args...).Exec(); err != nil { + if err := run.QueryNone(q, uq.args); err != nil { return err } return nil } -func (uq *Query) build() string { +func (uq *Query) Build() string { q := qb.Update(uq.table) if len(uq.fields) > 0 { @@ -34,9 +36,5 @@ func (uq *Query) build() string { queryStr, _ := q.ToCql() - if uq.ctx.Debug { - uq.ctx.PrintQuery(queryStr, uq.args) - } - return strings.TrimSpace(queryStr) } diff --git a/qb/qupdate/update_exec_test.go b/qb/qupdate/update_exec_test.go index 10e38de..0276225 100644 --- a/qb/qupdate/update_exec_test.go +++ b/qb/qupdate/update_exec_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/Drafteame/cassandra-builder/qb/query" + "github.com/Drafteame/cassandra-builder/qb/test/mocks" ) func TestQuery_build(t *testing.T) { @@ -74,7 +75,9 @@ func TestQuery_build(t *testing.T) { } for _, test := range tt { - q := New(nil, false, nil).Table(test.table) + client := mocks.NewClient(t) + + q := New(client).Table(test.table) for _, s := range test.set { q = q.Set(s.field, s.value) @@ -84,7 +87,7 @@ func TestQuery_build(t *testing.T) { q = q.Where(w.Field, w.Op, w.Value) } - qs := q.build() + qs := q.Build() if qs != test.res { t.Errorf("query err: \nexp: '%s' \ngot: '%s'", test.res, qs) diff --git a/qb/runner/runner.go b/qb/runner/runner.go new file mode 100644 index 0000000..992119f --- /dev/null +++ b/qb/runner/runner.go @@ -0,0 +1,174 @@ +package runner + +import ( + "reflect" + + "github.com/avast/retry-go/v4" + "github.com/gocql/gocql" + + "github.com/Drafteame/cassandra-builder/qb/errors" + "github.com/Drafteame/cassandra-builder/qb/models" + "github.com/Drafteame/cassandra-builder/qb/query" +) + +//go:generate mockery --name=Client --filename=client.go --structname=Client --output=../test/mocks --outpkg=mocks + +type Client interface { + Session() *gocql.Session + Config() models.Config + Restart() error + Debug() bool +} + +type Runner struct { + client Client +} + +func (r *Runner) Query(stmt string, args []interface{}, bind interface{}) error { + execFn := func() error { + if r.client.Session() == nil || r.client.Session().Closed() { + return errors.ErrClosedConnection + } + + return r.queryAll(stmt, args, bind) + } + + opts := r.getRetryOptions() + + if err := retry.Do(execFn, opts...); err != nil { + return err + } + + return nil +} + +func (r *Runner) QueryCount(query string, args []interface{}) (int64, error) { + var count int64 + + execFn := func() error { + if r.client.Session() == nil || r.client.Session().Closed() { + return errors.ErrClosedConnection + } + + consistency := r.client.Config().Consistency + + if err := r.client.Session().Query(query, args...).Consistency(gocql.Consistency(consistency)).Scan(&count); err != nil { + if err == gocql.ErrNoConnections { + return err + } + + return retry.Unrecoverable(err) + } + + return nil + } + + opts := r.getRetryOptions() + + if err := retry.Do(execFn, opts...); err != nil { + return 0, err + } + + return count, nil +} + +func (r *Runner) QueryOne(query string, args []interface{}) (string, error) { + var jsonRow string + + execFn := func() error { + if r.client.Session() == nil || r.client.Session().Closed() { + return errors.ErrClosedConnection + } + + consistency := r.client.Config().Consistency + + if err := r.client.Session().Query(query, args...).Consistency(gocql.Consistency(consistency)).Scan(&jsonRow); err != nil { + if err == gocql.ErrNoConnections { + return err + } + + return retry.Unrecoverable(err) + } + + return nil + } + + opts := r.getRetryOptions() + + if err := retry.Do(execFn, opts...); err != nil { + return "", err + } + + return jsonRow, nil +} + +func (r *Runner) QueryNone(query string, args []interface{}) error { + execFn := func() error { + if r.client.Session() == nil || r.client.Session().Closed() { + return errors.ErrClosedConnection + } + + if err := r.client.Session().Query(query, args...).Exec(); err != nil { + if err == gocql.ErrNoConnections { + return err + } + + return retry.Unrecoverable(err) + } + + return nil + } + + opts := r.getRetryOptions() + + return retry.Do(execFn, opts...) +} + +func New(c Client) *Runner { + return &Runner{client: c} +} + +func (r *Runner) getRetryOptions() []retry.Option { + return []retry.Option{ + retry.Attempts(r.client.Config().NumRetries), + retry.OnRetry(func(n uint, err error) { + + _ = r.client.Restart() + + //TODO: handle error + }), + } +} + +func (r *Runner) queryAll(stmt string, args []interface{}, bind interface{}) error { + var jsonRow string + + iter := r.client.Session().Query(stmt, args...).Iter() + if iter == nil { + return errors.ErrNilIterator + } + + ib := reflect.Indirect(reflect.ValueOf(bind)) + + bv := reflect.ValueOf(ib.Interface()) + bt := bv.Type().Elem() + + for iter.Scan(&jsonRow) { + elem, err := query.BindRow([]byte(jsonRow), bt) + if err != nil { + return err + } + + ib.Set(reflect.Append(ib, reflect.Indirect(elem))) + } + + if err := iter.Close(); err != nil { + if err == gocql.ErrNoConnections { + return err + } + + return retry.Unrecoverable(err) + } + + return nil +} diff --git a/qb/test/mocks/client.go b/qb/test/mocks/client.go new file mode 100644 index 0000000..75576ba --- /dev/null +++ b/qb/test/mocks/client.go @@ -0,0 +1,88 @@ +// Code generated by mockery v2.16.0. DO NOT EDIT. + +package mocks + +import ( + gocql "github.com/gocql/gocql" + mock "github.com/stretchr/testify/mock" + + models "github.com/Drafteame/cassandra-builder/qb/models" +) + +// Client is an autogenerated mock type for the Client type +type Client struct { + mock.Mock +} + +// Config provides a mock function with given fields: +func (_m *Client) Config() models.Config { + ret := _m.Called() + + var r0 models.Config + if rf, ok := ret.Get(0).(func() models.Config); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(models.Config) + } + + return r0 +} + +// Debug provides a mock function with given fields: +func (_m *Client) Debug() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Restart provides a mock function with given fields: +func (_m *Client) Restart() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Session provides a mock function with given fields: +func (_m *Client) Session() *gocql.Session { + ret := _m.Called() + + var r0 *gocql.Session + if rf, ok := ret.Get(0).(func() *gocql.Session); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gocql.Session) + } + } + + return r0 +} + +type mockConstructorTestingTNewClient interface { + mock.TestingT + Cleanup(func()) +} + +// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewClient(t mockConstructorTestingTNewClient) *Client { + mock := &Client{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +}