diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 05fee7835..2f13ac761 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -101,6 +101,14 @@ jobs: make test make testrace + - name: Run CRUD tests with options v1 + run: | + make bench-deps test-crud-bench + + - name: Run CRUD tests with options v2 + run: | + make bench-deps test-crud-bench TAGS="go_tarantool_crud_opts_v2" + - name: Run regression tests with call_17 run: | make test TAGS="go_tarantool_call_17" diff --git a/CHANGELOG.md b/CHANGELOG.md index 48b38049e..f9f5f7cd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Support pagination (#246) - A Makefile target to test with race detector (#218) +- Support CRUD API (#108) ### Changed diff --git a/Makefile b/Makefile index 34de54707..04fe9ab45 100644 --- a/Makefile +++ b/Makefile @@ -21,12 +21,13 @@ endif .PHONY: clean clean: - ( cd ./queue; rm -rf .rocks ) + ( rm -rf queue/.rocks crud/.rocks ) rm -f $(COVERAGE_FILE) .PHONY: deps deps: clean ( cd ./queue/testdata; $(TTCTL) rocks install queue 1.2.1 ) + ( cd ./crud; $(TTCTL) rocks install crud 0.14.1 ) .PHONY: datetime-timezones datetime-timezones: @@ -99,6 +100,19 @@ test-settings: go clean -testcache go test -tags "$(TAGS)" ./settings/ -v -p 1 +.PHONY: test-crud +test-crud: + @echo "Running tests in crud package" + cd ./crud/ && tarantool -e "require('crud')" + go clean -testcache + go test -tags "$(TAGS)" ./crud/ -v -p 1 + +.PHONY: test-crud-bench +test-crud-bench: + @echo "Running bench tests in crud package" + cd ./crud/ && tarantool -e "require('crud')" + go test -v -tags "$(TAGS)" -bench=. -run=asdasdasd . -benchmem -memprofile=mem.out + .PHONY: test-main test-main: @echo "Running tests in main package" diff --git a/crud/common.go b/crud/common.go new file mode 100644 index 000000000..a70c6e48d --- /dev/null +++ b/crud/common.go @@ -0,0 +1,42 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +type Tuple = []interface{} +type Object = map[string]interface{} + +type baseRequest struct { + impl *tarantool.CallRequest +} + +func (req *baseRequest) initImpl(methodName string) { + req.impl = tarantool.NewCall17Request(methodName) +} + +// Code returns IPROTO code for CRUD request. +func (req *baseRequest) Code() int32 { + return req.impl.Code() +} + +// Ctx returns a context of CRUD request. +func (req *baseRequest) Ctx() context.Context { + return req.impl.Ctx() +} + +// Async returns is CRUD request expects a response. +func (req *baseRequest) Async() bool { + return req.impl.Async() +} + +type spaceRequest struct { + baseRequest + space string +} + +func (req *spaceRequest) setSpace(space string) { + req.space = space +} diff --git a/crud/conditions.go b/crud/conditions.go new file mode 100644 index 000000000..d9f1a6279 --- /dev/null +++ b/crud/conditions.go @@ -0,0 +1,20 @@ +package crud + +type Operator string + +const ( + EQ Operator = "=" + LT Operator = "<" + LE Operator = "<=" + GT Operator = ">" + GE Operator = ">=" +) + +type Condition struct { + // Instruct msgpack to pack this struct as array, so no custom packer + // is needed. + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Operator Operator + KeyName string + KeyValue interface{} +} diff --git a/crud/count.go b/crud/count.go new file mode 100644 index 000000000..02d9fe026 --- /dev/null +++ b/crud/count.go @@ -0,0 +1,64 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +type CountOpts struct { + Timeout OptUint `crud:"timeout"` + VshardRouter OptString `crud:"vshard_router"` + Mode OptString `crud:"mode"` + PreferReplica OptBool `crud:"prefer_replica"` + Balance OptBool `crud:"balance"` + YieldEvery OptUint `crud:"yield_every"` + BucketId OptUint `crud:"bucket_id"` + ForceMapCall OptBool `crud:"force_map_call"` + Fullscan OptBool `crud:"fullscan"` +} + +// CountRequest helps you to create request object to call `crud.count` +// for execution by a Connection. +type CountRequest struct { + spaceRequest + conditions []Condition + opts CountOpts +} + +// NewCountRequest returns a new empty CountRequest. +func NewCountRequest(space string) *CountRequest { + req := new(CountRequest) + req.initImpl("crud.count") + req.setSpace(space) + req.conditions = []Condition{} + req.opts = CountOpts{} + return req +} + +// Conditions sets the conditions for the CountRequest request. +// Note: default value is nil. +func (req *CountRequest) Conditions(conditions []Condition) *CountRequest { + req.conditions = conditions + return req +} + +// Opts sets the options for the CountRequest request. +// Note: default value is nil. +func (req *CountRequest) Opts(opts CountOpts) *CountRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *CountRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.impl.Args([]interface{}{req.space, req.conditions, convertToMap(req.opts)}) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *CountRequest) Context(ctx context.Context) *CountRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/delete.go b/crud/delete.go new file mode 100644 index 000000000..1efe03db5 --- /dev/null +++ b/crud/delete.go @@ -0,0 +1,54 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +type DeleteOpts = SimpleOperationOpts + +// DeleteRequest helps you to create request object to call `crud.delete` +// for execution by a Connection. +type DeleteRequest struct { + spaceRequest + key Tuple + opts DeleteOpts +} + +// NewDeleteRequest returns a new empty DeleteRequest. +func NewDeleteRequest(space string) *DeleteRequest { + req := new(DeleteRequest) + req.initImpl("crud.delete") + req.setSpace(space) + req.key = Tuple{} + req.opts = DeleteOpts{} + return req +} + +// Key sets the key for the DeleteRequest request. +// Note: default value is nil. +func (req *DeleteRequest) Key(key Tuple) *DeleteRequest { + req.key = key + return req +} + +// Opts sets the options for the DeleteRequest request. +// Note: default value is nil. +func (req *DeleteRequest) Opts(opts DeleteOpts) *DeleteRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *DeleteRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.impl.Args([]interface{}{req.space, req.key, convertToMap(req.opts)}) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *DeleteRequest) Context(ctx context.Context) *DeleteRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/get.go b/crud/get.go new file mode 100644 index 000000000..6378c85d3 --- /dev/null +++ b/crud/get.go @@ -0,0 +1,62 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +type GetOpts struct { + Timeout OptUint `crud:"timeout"` + VshardRouter OptString `crud:"vshard_router"` + Fields OptTuple `crud:"fields"` + BucketId OptUint `crud:"bucket_id"` + Mode OptString `crud:"mode"` + PreferReplica OptBool `crud:"prefer_replica"` + Balance OptBool `crud:"balance"` +} + +// GetRequest helps you to create request object to call `crud.get` +// for execution by a Connection. +type GetRequest struct { + spaceRequest + key Tuple + opts GetOpts +} + +// NewGetRequest returns a new empty GetRequest. +func NewGetRequest(space string) *GetRequest { + req := new(GetRequest) + req.initImpl("crud.get") + req.setSpace(space) + req.key = Tuple{} + req.opts = GetOpts{} + return req +} + +// Key sets the key for the GetRequest request. +// Note: default value is nil. +func (req *GetRequest) Key(key Tuple) *GetRequest { + req.key = key + return req +} + +// Opts sets the options for the GetRequest request. +// Note: default value is nil. +func (req *GetRequest) Opts(opts GetOpts) *GetRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *GetRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.impl.Args([]interface{}{req.space, req.key, convertToMap(req.opts)}) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *GetRequest) Context(ctx context.Context) *GetRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/insert.go b/crud/insert.go new file mode 100644 index 000000000..0dcae5d71 --- /dev/null +++ b/crud/insert.go @@ -0,0 +1,101 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +type InsertOpts = SimpleOperationOpts + +// InsertRequest helps you to create request object to call `crud.insert` +// for execution by a Connection. +type InsertRequest struct { + spaceRequest + tuple Tuple + opts InsertOpts +} + +// NewInsertRequest returns a new empty InsertRequest. +func NewInsertRequest(space string) *InsertRequest { + req := new(InsertRequest) + req.initImpl("crud.insert") + req.setSpace(space) + req.tuple = Tuple{} + req.opts = InsertOpts{} + return req +} + +// Tuple sets the tuple for the InsertRequest request. +// Note: default value is nil. +func (req *InsertRequest) Tuple(tuple Tuple) *InsertRequest { + req.tuple = tuple + return req +} + +// Opts sets the options for the insert request. +// Note: default value is nil. +func (req *InsertRequest) Opts(opts InsertOpts) *InsertRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *InsertRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.impl.Args([]interface{}{req.space, req.tuple, convertToMap(req.opts)}) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *InsertRequest) Context(ctx context.Context) *InsertRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +type InsertObjectOpts = SimpleOperationObjectOpts + +// InsertObjectRequest helps you to create request object to call +// `crud.insert_object` for execution by a Connection. +type InsertObjectRequest struct { + spaceRequest + object Object + opts InsertObjectOpts +} + +// NewInsertObjectRequest returns a new empty InsertObjectRequest. +func NewInsertObjectRequest(space string) *InsertObjectRequest { + req := new(InsertObjectRequest) + req.initImpl("crud.insert_object") + req.setSpace(space) + req.object = Object{} + req.opts = InsertObjectOpts{} + return req +} + +// Object sets the tuple for the InsertObjectRequest request. +// Note: default value is nil. +func (req *InsertObjectRequest) Object(object Object) *InsertObjectRequest { + req.object = object + return req +} + +// Opts sets the options for the InsertObjectRequest request. +// Note: default value is nil. +func (req *InsertObjectRequest) Opts(opts InsertObjectOpts) *InsertObjectRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *InsertObjectRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.impl.Args([]interface{}{req.space, req.object, convertToMap(req.opts)}) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *InsertObjectRequest) Context(ctx context.Context) *InsertObjectRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/insert_many.go b/crud/insert_many.go new file mode 100644 index 000000000..cf2d72910 --- /dev/null +++ b/crud/insert_many.go @@ -0,0 +1,101 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +type InsertManyOpts = OperationManyOpts + +// InsertManyRequest helps you to create request object to call +// `crud.insert_many` for execution by a Connection. +type InsertManyRequest struct { + spaceRequest + tuples []Tuple + opts InsertManyOpts +} + +// NewInsertManyRequest returns a new empty InsertManyRequest. +func NewInsertManyRequest(space string) *InsertManyRequest { + req := new(InsertManyRequest) + req.initImpl("crud.insert_many") + req.setSpace(space) + req.tuples = []Tuple{} + req.opts = InsertManyOpts{} + return req +} + +// Tuples sets the tuples for the InsertManyRequest request. +// Note: default value is nil. +func (req *InsertManyRequest) Tuples(tuples []Tuple) *InsertManyRequest { + req.tuples = tuples + return req +} + +// Opts sets the options for the InsertManyRequest request. +// Note: default value is nil. +func (req *InsertManyRequest) Opts(opts InsertManyOpts) *InsertManyRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *InsertManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.impl.Args([]interface{}{req.space, req.tuples, convertToMap(req.opts)}) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *InsertManyRequest) Context(ctx context.Context) *InsertManyRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +type InsertObjectManyOpts = OperationObjectManyOpts + +// InsertObjectManyRequest helps you to create request object to call +// `crud.insert_object_many` for execution by a Connection. +type InsertObjectManyRequest struct { + spaceRequest + objects interface{} + opts InsertObjectManyOpts +} + +// NewInsertObjectManyRequest returns a new empty InsertObjectManyRequest. +func NewInsertObjectManyRequest(space string) *InsertObjectManyRequest { + req := new(InsertObjectManyRequest) + req.initImpl("crud.insert_object_many") + req.setSpace(space) + req.objects = []map[string]interface{}{} + req.opts = InsertObjectManyOpts{} + return req +} + +// Objects sets the objects for the InsertObjectManyRequest request. +// Note: default value is nil. +func (req *InsertObjectManyRequest) Objects(objects interface{}) *InsertObjectManyRequest { + req.objects = objects + return req +} + +// Opts sets the options for the InsertObjectManyRequest request. +// Note: default value is nil. +func (req *InsertObjectManyRequest) Opts(opts InsertObjectManyOpts) *InsertObjectManyRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *InsertObjectManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.impl.Args([]interface{}{req.space, req.objects, convertToMap(req.opts)}) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *InsertObjectManyRequest) Context(ctx context.Context) *InsertObjectManyRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/len.go b/crud/len.go new file mode 100644 index 000000000..7610c91b7 --- /dev/null +++ b/crud/len.go @@ -0,0 +1,86 @@ +//go:build !go_tarantool_crud_opts_v2 +// +build !go_tarantool_crud_opts_v2 + +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +type LenOpts = BaseOpts + +// LenRequest helps you to create request object to call `crud.len` +// for execution by a Connection. +type LenRequest struct { + spaceRequest + opts LenOpts +} + +type lenArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space interface{} + Opts LenOpts +} + +func (opts BaseOpts) EncodeMsgpack(enc *encoder) error { + optsMap := make(map[string]interface{}) + mapLen := 0 + if value, err := opts.Timeout.Get(); err == nil { + optsMap[timeoutOptName] = value + mapLen += 1 + } + + if value, err := opts.VshardRouter.Get(); err == nil { + optsMap[vshardRouterOptName] = value + mapLen += 1 + } + + if err := enc.EncodeMapLen(mapLen); err != nil { + return err + } + + return encodeMsgpack(enc, optsMap) +} + +func encodeMsgpack(enc *encoder, opts map[string]interface{}) error { + for name, value := range opts { + if err := encodeMulti(enc, name, value); err != nil { + return err + } + } + + return nil +} + +// NewLenRequest returns a new empty LenRequest. +func NewLenRequest(space string) *LenRequest { + req := new(LenRequest) + req.initImpl("crud.len") + req.setSpace(space) + req.opts = LenOpts{} + return req +} + +// Opts sets the options for the LenRequest request. +// Note: default value is nil. +func (req *LenRequest) Opts(opts LenOpts) *LenRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *LenRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + //req.impl.Args([]interface{}{req.space, convertToMap(req.opts)}) + args := lenArgs{Space: req.space, Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *LenRequest) Context(ctx context.Context) *LenRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/len_v2.go b/crud/len_v2.go new file mode 100644 index 000000000..e7011b325 --- /dev/null +++ b/crud/len_v2.go @@ -0,0 +1,99 @@ +//go:build go_tarantool_crud_opts_v2 +// +build go_tarantool_crud_opts_v2 + +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +type LenOpts = BaseOpts + +type lenArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space interface{} + Opts LenOpts +} + +func (opts BaseOpts) EncodeMsgpack(enc *encoder) error { + var ( + timeout uint + timeoutExist bool + vshardRouter string + vshardRouterExist bool + err error + ) + mapLen := 0 + + if timeout, err = opts.Timeout.Get(); err == nil { + mapLen += 1 + timeoutExist = true + } + + if vshardRouter, err = opts.VshardRouter.Get(); err == nil { + mapLen += 1 + vshardRouterExist = true + } + + if err = enc.EncodeMapLen(mapLen); err != nil { + return err + } + + if timeoutExist { + if err = enc.EncodeString(timeoutOptName); err != nil { + return err + } + if err = enc.EncodeUint(timeout); err != nil { + return err + } + } + if vshardRouterExist { + if err = enc.EncodeString(vshardRouterOptName); err != nil { + return err + } + if err = enc.EncodeString(vshardRouter); err != nil { + return err + } + } + + return err +} + +// LenRequest helps you to create request object to call `crud.len` +// for execution by a Connection. +type LenRequest struct { + spaceRequest + opts LenOpts +} + +// NewLenRequest returns a new empty LenRequest. +func NewLenRequest(space string) *LenRequest { + req := new(LenRequest) + req.initImpl("crud.len") + req.setSpace(space) + req.opts = LenOpts{} + return req +} + +// Opts sets the options for the LenRequest request. +// Note: default value is nil. +func (req *LenRequest) Opts(opts LenOpts) *LenRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *LenRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := lenArgs{Space: req.space, Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *LenRequest) Context(ctx context.Context) *LenRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/max.go b/crud/max.go new file mode 100644 index 000000000..836efa591 --- /dev/null +++ b/crud/max.go @@ -0,0 +1,54 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +type MaxOpts = BorderOpts + +// MaxRequest helps you to create request object to call `crud.max` +// for execution by a Connection. +type MaxRequest struct { + spaceRequest + index interface{} + opts MaxOpts +} + +// NewMaxRequest returns a new empty MaxRequest. +func NewMaxRequest(space string) *MaxRequest { + req := new(MaxRequest) + req.initImpl("crud.max") + req.setSpace(space) + req.index = []interface{}{} + req.opts = MaxOpts{} + return req +} + +// Index sets the index name/id for the MaxRequest request. +// Note: default value is nil. +func (req *MaxRequest) Index(index interface{}) *MaxRequest { + req.index = index + return req +} + +// Opts sets the options for the MaxRequest request. +// Note: default value is nil. +func (req *MaxRequest) Opts(opts MaxOpts) *MaxRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *MaxRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.impl.Args([]interface{}{req.space, req.index, convertToMap(req.opts)}) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *MaxRequest) Context(ctx context.Context) *MaxRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/min.go b/crud/min.go new file mode 100644 index 000000000..6d6b7742d --- /dev/null +++ b/crud/min.go @@ -0,0 +1,54 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +type MinOpts = BorderOpts + +// MinRequest helps you to create request object to call `crud.min` +// for execution by a Connection. +type MinRequest struct { + spaceRequest + index interface{} + opts MinOpts +} + +// NewMinRequest returns a new empty MinRequest. +func NewMinRequest(space string) *MinRequest { + req := new(MinRequest) + req.initImpl("crud.min") + req.setSpace(space) + req.index = []interface{}{} + req.opts = MinOpts{} + return req +} + +// Index sets the index name/id for the MinRequest request. +// Note: default value is nil. +func (req *MinRequest) Index(index interface{}) *MinRequest { + req.index = index + return req +} + +// Opts sets the options for the MinRequest request. +// Note: default value is nil. +func (req *MinRequest) Opts(opts MinOpts) *MinRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *MinRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.impl.Args([]interface{}{req.space, req.index, convertToMap(req.opts)}) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *MinRequest) Context(ctx context.Context) *MinRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/msgpack.go b/crud/msgpack.go new file mode 100644 index 000000000..d6ff1d774 --- /dev/null +++ b/crud/msgpack.go @@ -0,0 +1,20 @@ +//go:build !go_tarantool_msgpack_v5 +// +build !go_tarantool_msgpack_v5 + +package crud + +import ( + "io" + + "gopkg.in/vmihailenco/msgpack.v2" +) + +type encoder = msgpack.Encoder + +func NewEncoder(w io.Writer) *encoder { + return msgpack.NewEncoder(w) +} + +func encodeMulti(e *encoder, v ...interface{}) error { + return e.Encode(v...) +} diff --git a/crud/msgpack_v5.go b/crud/msgpack_v5.go new file mode 100644 index 000000000..4892a920a --- /dev/null +++ b/crud/msgpack_v5.go @@ -0,0 +1,20 @@ +//go:build go_tarantool_msgpack_v5 +// +build go_tarantool_msgpack_v5 + +package crud + +import ( + "io" + + "github.com/vmihailenco/msgpack/v5" +) + +type encoder = msgpack.Encoder + +func NewEncoder(w io.Writer) *encoder { + return msgpack.NewEncoder(w) +} + +func encodeMulti(e *encoder, v ...interface{}) error { + return e.EncodeMulti(v...) +} diff --git a/crud/operations.go b/crud/operations.go new file mode 100644 index 000000000..8a9b6e566 --- /dev/null +++ b/crud/operations.go @@ -0,0 +1,22 @@ +package crud + +const ( + ADD Operator = "+" + SUB Operator = "-" + AND Operator = "&" + OR Operator = "|" + XOR Operator = "^" + SPLICE Operator = ":" + INSERT Operator = "!" + DELETE Operator = "#" + ASSIGN Operator = "=" +) + +type Operation struct { + // Instruct msgpack to pack this struct as array, so no custom packer + // is needed. + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Operator Operator + FieldId interface{} // number or string + Value interface{} +} diff --git a/crud/options.go b/crud/options.go new file mode 100644 index 000000000..6aeffcaaf --- /dev/null +++ b/crud/options.go @@ -0,0 +1,152 @@ +package crud + +import ( + "errors" + "reflect" + + "github.com/markphelps/optional" +) + +const ( + timeoutOptName = "timeout" + vshardRouterOptName = "vshard_router" + fieldsOptName = "fields" + bucketIdOptName = "bucket_id" + skipNullabilityCheckOnFlattenOptName = "skip_nullability_check_on_flatten" + stopOnErrorOptName = "stop_on_error" + rollbackOnErrorOptName = "rollback_on_error" +) + +type option interface { + getInterface() (interface{}, error) +} + +type OptUint struct { + optional.Uint +} + +func NewOptUint(value uint) OptUint { + return OptUint{optional.NewUint(value)} +} + +func (opt OptUint) getInterface() (interface{}, error) { + return opt.Get() +} + +type OptInt struct { + optional.Int +} + +func NewOptInt(value int) OptInt { + return OptInt{optional.NewInt(value)} +} + +func (opt OptInt) getInterface() (interface{}, error) { + return opt.Get() +} + +type OptString struct { + optional.String +} + +func NewOptString(value string) OptString { + return OptString{optional.NewString(value)} +} + +func (opt OptString) getInterface() (interface{}, error) { + return opt.Get() +} + +type OptBool struct { + optional.Bool +} + +func NewOptBool(value bool) OptBool { + return OptBool{optional.NewBool(value)} +} + +func (opt OptBool) getInterface() (interface{}, error) { + return opt.Get() +} + +type OptTuple struct { + tuple []interface{} +} + +func NewOptTuple(tuple []interface{}) OptTuple { + return OptTuple{tuple} +} + +func (o *OptTuple) Get() ([]interface{}, error) { + if o.tuple == nil { + return nil, errors.New("value not present") + } + return o.tuple, nil +} + +func (opt OptTuple) getInterface() (interface{}, error) { + return opt.Get() +} + +type BaseOpts struct { + Timeout OptUint `crud:"timeout"` + VshardRouter OptString `crud:"vshard_router"` +} + +type SimpleOperationOpts struct { + Timeout OptUint `crud:"timeout"` + VshardRouter OptString `crud:"vshard_router"` + Fields OptTuple `crud:"fields"` + BucketId OptUint `crud:"bucket_id"` +} + +type SimpleOperationObjectOpts struct { + Timeout OptUint `crud:"timeout"` + VshardRouter OptString `crud:"vshard_router"` + Fields OptTuple `crud:"fields"` + BucketId OptUint `crud:"bucket_id"` + SkipNullabilityCheckOnFlatten OptBool `crud:"skip_nullability_check_on_flatten"` +} + +type OperationManyOpts struct { + Timeout OptUint `crud:"timeout"` + VshardRouter OptString `crud:"vshard_router"` + Fields OptTuple `crud:"fields"` + StopOnError OptBool `crud:"stop_on_error"` + RollbackOnError OptBool `crud:"rollback_on_error"` +} + +type OperationObjectManyOpts struct { + Timeout OptUint `crud:"timeout"` + VshardRouter OptString `crud:"vshard_router"` + Fields OptTuple `crud:"fields"` + StopOnError OptBool `crud:"stop_on_error"` + RollbackOnError OptBool `crud:"rollback_on_error"` + SkipNullabilityCheckOnFlatten OptBool `crud:"skip_nullability_check_on_flatten"` +} + +type BorderOpts struct { + Timeout OptUint `crud:"timeout"` + VshardRouter OptString `crud:"vshard_router"` + Fields OptTuple `crud:"fields"` +} + +func convertToMap(opts interface{}) map[string]interface{} { + optsMap := make(map[string]interface{}) + + val := reflect.ValueOf(opts) + typ := val.Type() + + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := typ.Field(i) + + if opt, ok := field.Interface().(option); ok { + if value, err := opt.getInterface(); err == nil { + optsMap[fieldType.Tag.Get("crud")] = value + } + } + } + + return optsMap +} diff --git a/crud/replace.go b/crud/replace.go new file mode 100644 index 000000000..07657653b --- /dev/null +++ b/crud/replace.go @@ -0,0 +1,101 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +type ReplaceOpts = SimpleOperationOpts + +// ReplaceRequest helps you to create request object to call `crud.replace` +// for execution by a Connection. +type ReplaceRequest struct { + spaceRequest + tuple Tuple + opts ReplaceOpts +} + +// NewReplaceRequest returns a new empty ReplaceRequest. +func NewReplaceRequest(space string) *ReplaceRequest { + req := new(ReplaceRequest) + req.initImpl("crud.replace") + req.setSpace(space) + req.tuple = Tuple{} + req.opts = ReplaceOpts{} + return req +} + +// Tuple sets the tuple for the ReplaceRequest request. +// Note: default value is nil. +func (req *ReplaceRequest) Tuple(tuple Tuple) *ReplaceRequest { + req.tuple = tuple + return req +} + +// Opts sets the options for the ReplaceRequest request. +// Note: default value is nil. +func (req *ReplaceRequest) Opts(opts ReplaceOpts) *ReplaceRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *ReplaceRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.impl.Args([]interface{}{req.space, req.tuple, convertToMap(req.opts)}) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *ReplaceRequest) Context(ctx context.Context) *ReplaceRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +type ReplaceObjectOpts = SimpleOperationObjectOpts + +// ReplaceObjectRequest helps you to create request object to call +// `crud.replace_object` for execution by a Connection. +type ReplaceObjectRequest struct { + spaceRequest + object Object + opts ReplaceObjectOpts +} + +// NewReplaceObjectRequest returns a new empty ReplaceObjectRequest. +func NewReplaceObjectRequest(space string) *ReplaceObjectRequest { + req := new(ReplaceObjectRequest) + req.initImpl("crud.replace_object") + req.setSpace(space) + req.object = Object{} + req.opts = ReplaceObjectOpts{} + return req +} + +// Object sets the tuple for the ReplaceObjectRequest request. +// Note: default value is nil. +func (req *ReplaceObjectRequest) Object(object Object) *ReplaceObjectRequest { + req.object = object + return req +} + +// Opts sets the options for the ReplaceObjectRequest request. +// Note: default value is nil. +func (req *ReplaceObjectRequest) Opts(opts ReplaceObjectOpts) *ReplaceObjectRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *ReplaceObjectRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.impl.Args([]interface{}{req.space, req.object, convertToMap(req.opts)}) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *ReplaceObjectRequest) Context(ctx context.Context) *ReplaceObjectRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/replace_many.go b/crud/replace_many.go new file mode 100644 index 000000000..036015fe1 --- /dev/null +++ b/crud/replace_many.go @@ -0,0 +1,101 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +type ReplaceManyOpts = OperationManyOpts + +// ReplaceManyRequest helps you to create request object to call +// `crud.replace_many` for execution by a Connection. +type ReplaceManyRequest struct { + spaceRequest + tuples []Tuple + opts ReplaceManyOpts +} + +// NewReplaceManyRequest returns a new empty ReplaceManyRequest. +func NewReplaceManyRequest(space string) *ReplaceManyRequest { + req := new(ReplaceManyRequest) + req.initImpl("crud.replace_many") + req.setSpace(space) + req.tuples = []Tuple{} + req.opts = ReplaceManyOpts{} + return req +} + +// Tuples sets the tuples for the ReplaceManyRequest request. +// Note: default value is nil. +func (req *ReplaceManyRequest) Tuples(tuples []Tuple) *ReplaceManyRequest { + req.tuples = tuples + return req +} + +// Opts sets the options for the ReplaceManyRequest request. +// Note: default value is nil. +func (req *ReplaceManyRequest) Opts(opts ReplaceManyOpts) *ReplaceManyRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *ReplaceManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.impl.Args([]interface{}{req.space, req.tuples, convertToMap(req.opts)}) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *ReplaceManyRequest) Context(ctx context.Context) *ReplaceManyRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +type ReplaceObjectManyOpts = OperationObjectManyOpts + +// ReplaceObjectManyRequest helps you to create request object to call +// `crud.replace_object_many` for execution by a Connection. +type ReplaceObjectManyRequest struct { + spaceRequest + objects []Object + opts ReplaceObjectManyOpts +} + +// NewReplaceObjectManyRequest returns a new empty ReplaceObjectManyRequest. +func NewReplaceObjectManyRequest(space string) *ReplaceObjectManyRequest { + req := new(ReplaceObjectManyRequest) + req.initImpl("crud.replace_object_many") + req.setSpace(space) + req.objects = []Object{} + req.opts = ReplaceObjectManyOpts{} + return req +} + +// Objects sets the tuple for the ReplaceObjectManyRequest request. +// Note: default value is nil. +func (req *ReplaceObjectManyRequest) Objects(objects []Object) *ReplaceObjectManyRequest { + req.objects = objects + return req +} + +// Opts sets the options for the ReplaceObjectManyRequest request. +// Note: default value is nil. +func (req *ReplaceObjectManyRequest) Opts(opts ReplaceObjectManyOpts) *ReplaceObjectManyRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *ReplaceObjectManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.impl.Args([]interface{}{req.space, req.objects, convertToMap(req.opts)}) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *ReplaceObjectManyRequest) Context(ctx context.Context) *ReplaceObjectManyRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/request_test.go b/crud/request_test.go new file mode 100644 index 000000000..43b46f887 --- /dev/null +++ b/crud/request_test.go @@ -0,0 +1,533 @@ +package crud_test + +import ( + "bytes" + "context" + "errors" + "testing" + + "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/crud" + "github.com/tarantool/go-tarantool/test_helpers" +) + +const invalidSpaceMsg = "invalid space" +const invalidIndexMsg = "invalid index" + +const invalidSpace = 2 +const invalidIndex = 2 +const validSpace = "test" // Any valid value != default. +const defaultSpace = 0 // And valid too. +const defaultIndex = 0 // And valid too. + +const CrudRequestCode = tarantool.Call17RequestCode + +var reqObject = map[string]interface{}{ + "id": uint(24), +} + +var reqObjects = []map[string]interface{}{ + { + "id": uint(24), + }, + { + "id": uint(25), + }, +} + +var reqObjectsOperationData = []interface{}{ + []interface{}{ + map[string]interface{}{ + "id": uint(24), + }, + []interface{}{[]interface{}{"+", "id", uint(1020)}}, + }, + []interface{}{ + map[string]interface{}{ + "id": uint(25), + }, + []interface{}{[]interface{}{"+", "id", uint(1020)}}, + }, +} + +var expectedOpts = map[string]interface{}{ + "timeout": timeout, +} + +type ValidSchemeResolver struct { +} + +func (*ValidSchemeResolver) ResolveSpaceIndex(s, i interface{}) (spaceNo, indexNo uint32, err error) { + if s != nil { + spaceNo = uint32(s.(int)) + } else { + spaceNo = defaultSpace + } + if i != nil { + indexNo = uint32(i.(int)) + } else { + indexNo = defaultIndex + } + if spaceNo == invalidSpace { + return 0, 0, errors.New(invalidSpaceMsg) + } + if indexNo == invalidIndex { + return 0, 0, errors.New(invalidIndexMsg) + } + return spaceNo, indexNo, nil +} + +var resolver ValidSchemeResolver + +func assertBodyEqual(t testing.TB, reference tarantool.Request, req tarantool.Request) { + t.Helper() + + reqBody, err := test_helpers.ExtractRequestBody(req, &resolver, crud.NewEncoder) + if err != nil { + t.Fatalf("An unexpected Response.Body() error: %q", err.Error()) + } + + refBody, err := test_helpers.ExtractRequestBody(reference, &resolver, crud.NewEncoder) + if err != nil { + t.Fatalf("An unexpected Response.Body() error: %q", err.Error()) + } + + if !bytes.Equal(reqBody, refBody) { + t.Errorf("Encoded request %v != reference %v", reqBody, refBody) + } +} + +func TestRequestsCodes(t *testing.T) { + tests := []struct { + req tarantool.Request + code int32 + }{ + {req: crud.NewInsertRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewInsertObjectRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewInsertManyRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewInsertObjectManyRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewGetRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewUpdateRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewDeleteRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewReplaceRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewReplaceObjectRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewReplaceManyRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewReplaceObjectManyRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewUpsertRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewUpsertObjectRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewUpsertManyRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewUpsertObjectManyRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewMinRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewMaxRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewSelectRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewTruncateRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewLenRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewCountRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewStorageInfoRequest(), code: CrudRequestCode}, + {req: crud.NewStatsRequest(), code: CrudRequestCode}, + } + + for _, test := range tests { + if code := test.req.Code(); code != test.code { + t.Errorf("An invalid request code 0x%x, expected 0x%x", code, test.code) + } + } +} + +func TestRequestsAsync(t *testing.T) { + tests := []struct { + req tarantool.Request + async bool + }{ + {req: crud.NewInsertRequest(validSpace), async: false}, + {req: crud.NewInsertObjectRequest(validSpace), async: false}, + {req: crud.NewInsertManyRequest(validSpace), async: false}, + {req: crud.NewInsertObjectManyRequest(validSpace), async: false}, + {req: crud.NewGetRequest(validSpace), async: false}, + {req: crud.NewUpdateRequest(validSpace), async: false}, + {req: crud.NewDeleteRequest(validSpace), async: false}, + {req: crud.NewReplaceRequest(validSpace), async: false}, + {req: crud.NewReplaceObjectRequest(validSpace), async: false}, + {req: crud.NewReplaceManyRequest(validSpace), async: false}, + {req: crud.NewReplaceObjectManyRequest(validSpace), async: false}, + {req: crud.NewUpsertRequest(validSpace), async: false}, + {req: crud.NewUpsertObjectRequest(validSpace), async: false}, + {req: crud.NewUpsertManyRequest(validSpace), async: false}, + {req: crud.NewUpsertObjectManyRequest(validSpace), async: false}, + {req: crud.NewMinRequest(validSpace), async: false}, + {req: crud.NewMaxRequest(validSpace), async: false}, + {req: crud.NewSelectRequest(validSpace), async: false}, + {req: crud.NewTruncateRequest(validSpace), async: false}, + {req: crud.NewLenRequest(validSpace), async: false}, + {req: crud.NewCountRequest(validSpace), async: false}, + {req: crud.NewStorageInfoRequest(), async: false}, + {req: crud.NewStatsRequest(), async: false}, + } + + for _, test := range tests { + if async := test.req.Async(); async != test.async { + t.Errorf("An invalid async %t, expected %t", async, test.async) + } + } +} + +func TestRequestsCtx_default(t *testing.T) { + tests := []struct { + req tarantool.Request + expected context.Context + }{ + {req: crud.NewInsertRequest(validSpace), expected: nil}, + {req: crud.NewInsertObjectRequest(validSpace), expected: nil}, + {req: crud.NewInsertManyRequest(validSpace), expected: nil}, + {req: crud.NewInsertObjectManyRequest(validSpace), expected: nil}, + {req: crud.NewGetRequest(validSpace), expected: nil}, + {req: crud.NewUpdateRequest(validSpace), expected: nil}, + {req: crud.NewDeleteRequest(validSpace), expected: nil}, + {req: crud.NewReplaceRequest(validSpace), expected: nil}, + {req: crud.NewReplaceObjectRequest(validSpace), expected: nil}, + {req: crud.NewReplaceManyRequest(validSpace), expected: nil}, + {req: crud.NewReplaceObjectManyRequest(validSpace), expected: nil}, + {req: crud.NewUpsertRequest(validSpace), expected: nil}, + {req: crud.NewUpsertObjectRequest(validSpace), expected: nil}, + {req: crud.NewUpsertManyRequest(validSpace), expected: nil}, + {req: crud.NewUpsertObjectManyRequest(validSpace), expected: nil}, + {req: crud.NewMinRequest(validSpace), expected: nil}, + {req: crud.NewMaxRequest(validSpace), expected: nil}, + {req: crud.NewSelectRequest(validSpace), expected: nil}, + {req: crud.NewTruncateRequest(validSpace), expected: nil}, + {req: crud.NewLenRequest(validSpace), expected: nil}, + {req: crud.NewCountRequest(validSpace), expected: nil}, + {req: crud.NewStorageInfoRequest(), expected: nil}, + {req: crud.NewStatsRequest(), expected: nil}, + } + + for _, test := range tests { + if ctx := test.req.Ctx(); ctx != test.expected { + t.Errorf("An invalid ctx %t, expected %t", ctx, test.expected) + } + } +} + +func TestRequestsCtx_setter(t *testing.T) { + ctx := context.Background() + tests := []struct { + req tarantool.Request + expected context.Context + }{ + {req: crud.NewInsertRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewInsertObjectRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewInsertManyRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewInsertObjectManyRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewGetRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewUpdateRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewDeleteRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewReplaceRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewReplaceObjectRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewReplaceManyRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewReplaceObjectManyRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewUpsertRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewUpsertObjectRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewUpsertManyRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewUpsertObjectManyRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewMinRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewMaxRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewSelectRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewTruncateRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewLenRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewCountRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewStorageInfoRequest().Context(ctx), expected: ctx}, + {req: crud.NewStatsRequest().Context(ctx), expected: ctx}, + } + + for _, test := range tests { + if ctx := test.req.Ctx(); ctx != test.expected { + t.Errorf("An invalid ctx %t, expected %t", ctx, test.expected) + } + } +} + +func TestRequestsDefaultValues(t *testing.T) { + testCases := []struct { + name string + ref tarantool.Request + target tarantool.Request + }{ + { + name: "InsertRequest", + ref: tarantool.NewCall17Request("crud.insert").Args([]interface{}{validSpace, []interface{}{}, + map[string]interface{}{}}), + target: crud.NewInsertRequest(validSpace), + }, + { + name: "InsertObjectRequest", + ref: tarantool.NewCall17Request("crud.insert_object").Args([]interface{}{validSpace, map[string]interface{}{}, + map[string]interface{}{}}), + target: crud.NewInsertObjectRequest(validSpace), + }, + { + name: "InsertManyRequest", + ref: tarantool.NewCall17Request("crud.insert_many").Args([]interface{}{validSpace, []interface{}{}, + map[string]interface{}{}}), + target: crud.NewInsertManyRequest(validSpace), + }, + { + name: "InsertObjectManyRequest", + ref: tarantool.NewCall17Request("crud.insert_object_many").Args([]interface{}{validSpace, []map[string]interface{}{}, + map[string]interface{}{}}), + target: crud.NewInsertObjectManyRequest(validSpace), + }, + { + name: "GetRequest", + ref: tarantool.NewCall17Request("crud.get").Args([]interface{}{validSpace, []interface{}{}, + map[string]interface{}{}}), + target: crud.NewGetRequest(validSpace), + }, + { + name: "UpdateRequest", + ref: tarantool.NewCall17Request("crud.update").Args([]interface{}{validSpace, []interface{}{}, + []interface{}{}, map[string]interface{}{}}), + target: crud.NewUpdateRequest(validSpace), + }, + { + name: "DeleteRequest", + ref: tarantool.NewCall17Request("crud.delete").Args([]interface{}{validSpace, []interface{}{}, + map[string]interface{}{}}), + target: crud.NewDeleteRequest(validSpace), + }, + { + name: "ReplaceRequest", + ref: tarantool.NewCall17Request("crud.replace").Args([]interface{}{validSpace, []interface{}{}, + map[string]interface{}{}}), + target: crud.NewReplaceRequest(validSpace), + }, + { + name: "ReplaceObjectRequest", + ref: tarantool.NewCall17Request("crud.replace_object").Args([]interface{}{validSpace, + map[string]interface{}{}, map[string]interface{}{}}), + target: crud.NewReplaceObjectRequest(validSpace), + }, + { + name: "ReplaceManyRequest", + ref: tarantool.NewCall17Request("crud.replace_many").Args([]interface{}{validSpace, + []interface{}{}, map[string]interface{}{}}), + target: crud.NewReplaceManyRequest(validSpace), + }, + { + name: "ReplaceObjectManyRequest", + ref: tarantool.NewCall17Request("crud.replace_object_many").Args([]interface{}{validSpace, + []map[string]interface{}{}, map[string]interface{}{}}), + target: crud.NewReplaceObjectManyRequest(validSpace), + }, + { + name: "UpsertRequest", + ref: tarantool.NewCall17Request("crud.upsert").Args([]interface{}{validSpace, []interface{}{}, + []interface{}{}, map[string]interface{}{}}), + target: crud.NewUpsertRequest(validSpace), + }, + { + name: "UpsertObjectRequest", + ref: tarantool.NewCall17Request("crud.upsert_object").Args([]interface{}{validSpace, + map[string]interface{}{}, []interface{}{}, map[string]interface{}{}}), + target: crud.NewUpsertObjectRequest(validSpace), + }, + { + name: "UpsertManyRequest", + ref: tarantool.NewCall17Request("crud.upsert_many").Args([]interface{}{validSpace, + []interface{}{}, map[string]interface{}{}}), + target: crud.NewUpsertManyRequest(validSpace), + }, + { + name: "UpsertObjectManyRequest", + ref: tarantool.NewCall17Request("crud.upsert_object_many").Args([]interface{}{validSpace, + []interface{}{}, map[string]interface{}{}}), + target: crud.NewUpsertObjectManyRequest(validSpace), + }, + { + name: "SelectRequest", + ref: tarantool.NewCall17Request("crud.select").Args([]interface{}{validSpace, + []interface{}{}, map[string]interface{}{}}), + target: crud.NewSelectRequest(validSpace), + }, + { + name: "MinRequest", + ref: tarantool.NewCall17Request("crud.min").Args([]interface{}{validSpace, + []interface{}{}, map[string]interface{}{}}), + target: crud.NewMinRequest(validSpace), + }, + { + name: "MaxRequest", + ref: tarantool.NewCall17Request("crud.max").Args([]interface{}{validSpace, + []interface{}{}, map[string]interface{}{}}), + target: crud.NewMaxRequest(validSpace), + }, + { + name: "TruncateRequest", + ref: tarantool.NewCall17Request("crud.truncate").Args([]interface{}{validSpace, + map[string]interface{}{}}), + target: crud.NewTruncateRequest(validSpace), + }, + { + name: "LenRequest", + ref: tarantool.NewCall17Request("crud.len").Args([]interface{}{validSpace, + map[string]interface{}{}}), + target: crud.NewLenRequest(validSpace), + }, + { + name: "CountRequest", + ref: tarantool.NewCall17Request("crud.count").Args([]interface{}{validSpace, + []interface{}{}, map[string]interface{}{}}), + target: crud.NewCountRequest(validSpace), + }, + { + name: "StorageInfoRequest", + ref: tarantool.NewCall17Request("crud.storage_info").Args( + []interface{}{map[string]interface{}{}}), + target: crud.NewStorageInfoRequest(), + }, + { + name: "StatsRequest", + ref: tarantool.NewCall17Request("crud.stats").Args( + []interface{}{}), + target: crud.NewStatsRequest(), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assertBodyEqual(t, tc.ref, tc.target) + }) + } +} + +func TestRequestsSetters(t *testing.T) { + testCases := []struct { + name string + ref tarantool.Request + target tarantool.Request + }{ + { + name: "InsertRequest", + ref: tarantool.NewCall17Request("crud.insert").Args([]interface{}{spaceName, tuple, expectedOpts}), + target: crud.NewInsertRequest(spaceName).Tuple(tuple).Opts(simpleOperationOpts), + }, + { + name: "InsertObjectRequest", + ref: tarantool.NewCall17Request("crud.insert_object").Args([]interface{}{spaceName, reqObject, expectedOpts}), + target: crud.NewInsertObjectRequest(spaceName).Object(reqObject).Opts(simpleOperationObjectOpts), + }, + { + name: "InsertManyRequest", + ref: tarantool.NewCall17Request("crud.insert_many").Args([]interface{}{spaceName, tuples, expectedOpts}), + target: crud.NewInsertManyRequest(spaceName).Tuples(tuples).Opts(opManyOpts), + }, + { + name: "InsertObjectManyRequest", + ref: tarantool.NewCall17Request("crud.insert_object_many").Args([]interface{}{spaceName, reqObjects, expectedOpts}), + target: crud.NewInsertObjectManyRequest(spaceName).Objects(reqObjects).Opts(opObjManyOpts), + }, + { + name: "GetRequest", + ref: tarantool.NewCall17Request("crud.get").Args([]interface{}{spaceName, key, expectedOpts}), + target: crud.NewGetRequest(spaceName).Key(key).Opts(getOpts), + }, + { + name: "UpdateRequest", + ref: tarantool.NewCall17Request("crud.update").Args([]interface{}{spaceName, key, operations, expectedOpts}), + target: crud.NewUpdateRequest(spaceName).Key(key).Operations(operations).Opts(simpleOperationOpts), + }, + { + name: "DeleteRequest", + ref: tarantool.NewCall17Request("crud.delete").Args([]interface{}{spaceName, key, expectedOpts}), + target: crud.NewDeleteRequest(spaceName).Key(key).Opts(simpleOperationOpts), + }, + { + name: "ReplaceRequest", + ref: tarantool.NewCall17Request("crud.replace").Args([]interface{}{spaceName, tuple, expectedOpts}), + target: crud.NewReplaceRequest(spaceName).Tuple(tuple).Opts(simpleOperationOpts), + }, + { + name: "ReplaceObjectRequest", + ref: tarantool.NewCall17Request("crud.replace_object").Args([]interface{}{spaceName, reqObject, expectedOpts}), + target: crud.NewReplaceObjectRequest(spaceName).Object(reqObject).Opts(simpleOperationObjectOpts), + }, + { + name: "ReplaceManyRequest", + ref: tarantool.NewCall17Request("crud.replace_many").Args([]interface{}{spaceName, tuples, expectedOpts}), + target: crud.NewReplaceManyRequest(spaceName).Tuples(tuples).Opts(opManyOpts), + }, + { + name: "ReplaceObjectManyRequest", + ref: tarantool.NewCall17Request("crud.replace_object_many").Args([]interface{}{spaceName, reqObjects, expectedOpts}), + target: crud.NewReplaceObjectManyRequest(spaceName).Objects(reqObjects).Opts(opObjManyOpts), + }, + { + name: "UpsertRequest", + ref: tarantool.NewCall17Request("crud.upsert").Args([]interface{}{spaceName, tuple, operations, expectedOpts}), + target: crud.NewUpsertRequest(spaceName).Tuple(tuple).Operations(operations).Opts(simpleOperationOpts), + }, + { + name: "UpsertObjectRequest", + ref: tarantool.NewCall17Request("crud.upsert_object").Args([]interface{}{spaceName, reqObject, + operations, expectedOpts}), + target: crud.NewUpsertObjectRequest(spaceName).Object(reqObject).Operations(operations).Opts(simpleOperationOpts), + }, + { + name: "UpsertManyRequest", + ref: tarantool.NewCall17Request("crud.upsert_many").Args([]interface{}{spaceName, + tuplesOperationsData, expectedOpts}), + target: crud.NewUpsertManyRequest(spaceName).TuplesOperationData(tuplesOperationsData).Opts(opManyOpts), + }, + { + name: "UpsertObjectManyRequest", + ref: tarantool.NewCall17Request("crud.upsert_object_many").Args([]interface{}{spaceName, + reqObjectsOperationData, expectedOpts}), + target: crud.NewUpsertObjectManyRequest(spaceName).ObjectsOperationData(reqObjectsOperationData).Opts(opManyOpts), + }, + { + name: "SelectRequest", + ref: tarantool.NewCall17Request("crud.select").Args([]interface{}{spaceName, conditions, expectedOpts}), + target: crud.NewSelectRequest(spaceName).Conditions(conditions).Opts(selectOpts), + }, + { + name: "MinRequest", + ref: tarantool.NewCall17Request("crud.min").Args([]interface{}{spaceName, indexName, expectedOpts}), + target: crud.NewMinRequest(spaceName).Index(indexName).Opts(minOpts), + }, + { + name: "MaxRequest", + ref: tarantool.NewCall17Request("crud.max").Args([]interface{}{spaceName, indexName, expectedOpts}), + target: crud.NewMaxRequest(spaceName).Index(indexName).Opts(maxOpts), + }, + { + name: "TruncateRequest", + ref: tarantool.NewCall17Request("crud.truncate").Args([]interface{}{spaceName, expectedOpts}), + target: crud.NewTruncateRequest(spaceName).Opts(baseOpts), + }, + { + name: "LenRequest", + ref: tarantool.NewCall17Request("crud.len").Args([]interface{}{spaceName, expectedOpts}), + target: crud.NewLenRequest(spaceName).Opts(baseOpts), + }, + { + name: "CountRequest", + ref: tarantool.NewCall17Request("crud.count").Args([]interface{}{spaceName, conditions, expectedOpts}), + target: crud.NewCountRequest(spaceName).Conditions(conditions).Opts(countOpts), + }, + { + name: "StorageInfoRequest", + ref: tarantool.NewCall17Request("crud.storage_info").Args([]interface{}{expectedOpts}), + target: crud.NewStorageInfoRequest().Opts(baseOpts), + }, + { + name: "StatsRequest", + ref: tarantool.NewCall17Request("crud.stats").Args([]interface{}{spaceName}), + target: crud.NewStatsRequest().Space(spaceName), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assertBodyEqual(t, tc.ref, tc.target) + }) + } +} diff --git a/crud/select.go b/crud/select.go new file mode 100644 index 000000000..2c5049140 --- /dev/null +++ b/crud/select.go @@ -0,0 +1,67 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +type SelectOpts struct { + Timeout OptUint `crud:"timeout"` + VshardRouter OptString `crud:"vshard_router"` + Fields OptTuple `crud:"fields"` + BucketId OptUint `crud:"bucket_id"` + Mode OptString `crud:"mode"` + PreferReplica OptBool `crud:"prefer_replica"` + Balance OptBool `crud:"balance"` + First OptInt `crud:"first"` + After OptTuple `crud:"after"` + BatchSize OptUint `crud:"batch_size"` + ForceMapCall OptBool `crud:"force_map_call"` + Fullscan OptBool `crud:"fullscan"` +} + +// SelectRequest helps you to create request object to call `crud.select` +// for execution by a Connection. +type SelectRequest struct { + spaceRequest + conditions []Condition + opts SelectOpts +} + +// NewSelectRequest returns a new empty SelectRequest. +func NewSelectRequest(space string) *SelectRequest { + req := new(SelectRequest) + req.initImpl("crud.select") + req.setSpace(space) + req.conditions = []Condition{} + req.opts = SelectOpts{} + return req +} + +// Conditions sets the conditions for the SelectRequest request. +// Note: default value is nil. +func (req *SelectRequest) Conditions(conditions []Condition) *SelectRequest { + req.conditions = conditions + return req +} + +// Opts sets the options for the SelectRequest request. +// Note: default value is nil. +func (req *SelectRequest) Opts(opts SelectOpts) *SelectRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *SelectRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.impl.Args([]interface{}{req.space, req.conditions, convertToMap(req.opts)}) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *SelectRequest) Context(ctx context.Context) *SelectRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/stats.go b/crud/stats.go new file mode 100644 index 000000000..939ed32ce --- /dev/null +++ b/crud/stats.go @@ -0,0 +1,46 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// StatsRequest helps you to create request object to call `crud.stats` +// for execution by a Connection. +type StatsRequest struct { + baseRequest + space OptString +} + +// NewStatsRequest returns a new empty StatsRequest. +func NewStatsRequest() *StatsRequest { + req := new(StatsRequest) + req.initImpl("crud.stats") + return req +} + +// Space sets the space name for the StatsRequest request. +// Note: default value is nil. +func (req *StatsRequest) Space(space string) *StatsRequest { + req.space = NewOptString(space) + return req +} + +// Body fills an encoder with the call request body. +func (req *StatsRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := []interface{}{} + if value, err := req.space.Get(); err == nil { + args = []interface{}{value} + } + req.impl.Args(args) + + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *StatsRequest) Context(ctx context.Context) *StatsRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/storage_info.go b/crud/storage_info.go new file mode 100644 index 000000000..71bc9eb1f --- /dev/null +++ b/crud/storage_info.go @@ -0,0 +1,44 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +type StorageInfoOpts = BaseOpts + +// StorageInfoRequest helps you to create request object to call +// `crud.storage_info` for execution by a Connection. +type StorageInfoRequest struct { + baseRequest + opts StorageInfoOpts +} + +// NewStorageInfoRequest returns a new empty StorageInfoRequest. +func NewStorageInfoRequest() *StorageInfoRequest { + req := new(StorageInfoRequest) + req.initImpl("crud.storage_info") + req.opts = StorageInfoOpts{} + return req +} + +// Opts sets the options for the torageInfoRequest request. +// Note: default value is nil. +func (req *StorageInfoRequest) Opts(opts StorageInfoOpts) *StorageInfoRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *StorageInfoRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.impl.Args([]interface{}{convertToMap(req.opts)}) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *StorageInfoRequest) Context(ctx context.Context) *StorageInfoRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go new file mode 100644 index 000000000..b1b129e2d --- /dev/null +++ b/crud/tarantool_test.go @@ -0,0 +1,601 @@ +package crud_test + +import ( + "bytes" + "fmt" + "log" + "os" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/crud" + "github.com/tarantool/go-tarantool/test_helpers" +) + +var server = "127.0.0.1:3013" +var spaceNo = uint32(617) +var spaceName = "test" +var indexNo = uint32(0) +var indexName = "primary_index" +var opts = tarantool.Opts{ + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", +} + +var startOpts test_helpers.StartOpts = test_helpers.StartOpts{ + InitScript: "testdata/config.lua", + Listen: server, + User: opts.User, + Pass: opts.Pass, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 3, + RetryTimeout: 500 * time.Millisecond, +} + +var timeout = uint(1) + +var operations = []crud.Operation{ + { + Operator: crud.ASSIGN, + FieldId: "name", + Value: "bye", + }, +} + +var selectOpts = crud.SelectOpts{ + Timeout: crud.NewOptUint(timeout), +} + +var countOpts = crud.CountOpts{ + Timeout: crud.NewOptUint(timeout), +} + +var getOpts = crud.GetOpts{ + Timeout: crud.NewOptUint(timeout), +} + +var minOpts = crud.MinOpts{ + Timeout: crud.NewOptUint(timeout), +} + +var maxOpts = crud.MaxOpts{ + Timeout: crud.NewOptUint(timeout), +} + +var baseOpts = crud.BaseOpts{ + Timeout: crud.NewOptUint(timeout), +} + +var simpleOperationOpts = crud.SimpleOperationOpts{ + Timeout: crud.NewOptUint(timeout), +} + +var simpleOperationObjectOpts = crud.SimpleOperationObjectOpts{ + Timeout: crud.NewOptUint(timeout), +} + +var opManyOpts = crud.OperationManyOpts{ + Timeout: crud.NewOptUint(timeout), +} + +var opObjManyOpts = crud.OperationObjectManyOpts{ + Timeout: crud.NewOptUint(timeout), +} + +var conditions = []crud.Condition{ + { + Operator: crud.LT, + KeyName: "id", + KeyValue: uint(1020), + }, +} + +var key = []interface{}{uint(1019)} + +var tuples = generateTuples() +var objects = generateObjects() + +var tuple = []interface{}{uint(1019), nil, "bla"} +var object = map[string]interface{}{ + "id": uint(1019), + "name": "bla", +} + +func BenchmarkCrud(b *testing.B) { + var err error + + conn := test_helpers.ConnectWithValidation(b, server, opts) + defer conn.Close() + + _, err = conn.Replace(spaceName, tuple) + if err != nil { + b.Error(err) + } + req := crud.NewLenRequest(spaceName). + Opts(crud.LenOpts{ + Timeout: crud.NewOptUint(3), + VshardRouter: crud.NewOptString("asd"), + }) + + buf := bytes.Buffer{} + buf.Grow(512 * 1024 * 1024) // Avoid allocs in test. + enc := crud.NewEncoder(&buf) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + err := req.Body(nil, enc) + if err != nil { + b.Error(err) + } + } +} + +var testProcessDataCases = []struct { + name string + expectedRespLen int + req tarantool.Request +}{ + { + "Select", + 2, + crud.NewSelectRequest(spaceName). + Conditions(conditions). + Opts(selectOpts), + }, + { + "Get", + 2, + crud.NewGetRequest(spaceName). + Key(key). + Opts(getOpts), + }, + { + "Update", + 2, + crud.NewUpdateRequest(spaceName). + Key(key). + Operations(operations). + Opts(simpleOperationOpts), + }, + { + "Delete", + 2, + crud.NewDeleteRequest(spaceName). + Key(key). + Opts(simpleOperationOpts), + }, + { + "Min", + 2, + crud.NewMinRequest(spaceName).Index(indexName).Opts(minOpts), + }, + { + "Max", + 2, + crud.NewMaxRequest(spaceName).Index(indexName).Opts(maxOpts), + }, + { + "Truncate", + 1, + crud.NewTruncateRequest(spaceName).Opts(baseOpts), + }, + { + "Len", + 1, + crud.NewLenRequest(spaceName).Opts(baseOpts), + }, + { + "Count", + 2, + crud.NewCountRequest(spaceName). + Conditions(conditions). + Opts(countOpts), + }, + { + "Stats", + 1, + crud.NewStatsRequest().Space(spaceName), + }, + { + "StorageInfo", + 1, + crud.NewStorageInfoRequest().Opts(baseOpts), + }, +} + +var tuplesOperationsData = generateTuplesOperationsData(tuples, operations) +var objectsOperationData = generateObjectsOperationsData(objects, operations) + +var testGenerateDataCases = []struct { + name string + expectedRespLen int + expectedTuplesCount int + req tarantool.Request +}{ + { + "Insert", + 2, + 1, + crud.NewInsertRequest(spaceName). + Tuple(tuple). + Opts(simpleOperationOpts), + }, + { + "InsertObject", + 2, + 1, + crud.NewInsertObjectRequest(spaceName). + Object(object). + Opts(simpleOperationObjectOpts), + }, + { + "InsertMany", + 2, + 10, + crud.NewInsertManyRequest(spaceName). + Tuples(tuples). + Opts(opManyOpts), + }, + { + "InsertObjectMany", + 2, + 10, + crud.NewInsertObjectManyRequest(spaceName). + Objects(objects). + Opts(opObjManyOpts), + }, + { + "Replace", + 2, + 1, + crud.NewReplaceRequest(spaceName). + Tuple(tuple). + Opts(simpleOperationOpts), + }, + { + "ReplaceObject", + 2, + 1, + crud.NewReplaceObjectRequest(spaceName). + Object(object). + Opts(simpleOperationObjectOpts), + }, + { + "ReplaceMany", + 2, + 10, + crud.NewReplaceManyRequest(spaceName). + Tuples(tuples). + Opts(opManyOpts), + }, + { + "ReplaceObjectMany", + 2, + 10, + crud.NewReplaceObjectManyRequest(spaceName). + Objects(objects). + Opts(opObjManyOpts), + }, + { + "Upsert", + 2, + 1, + crud.NewUpsertRequest(spaceName). + Tuple(tuple). + Operations(operations). + Opts(simpleOperationOpts), + }, + { + "UpsertObject", + 2, + 1, + crud.NewUpsertObjectRequest(spaceName). + Object(object). + Operations(operations). + Opts(simpleOperationOpts), + }, + { + "UpsertMany", + 2, + 10, + crud.NewUpsertManyRequest(spaceName). + TuplesOperationData(tuplesOperationsData). + Opts(opManyOpts), + }, + { + "UpsertObjectMany", + 2, + 10, + crud.NewUpsertObjectManyRequest(spaceName). + ObjectsOperationData(objectsOperationData). + Opts(opManyOpts), + }, +} + +func generateTuples() []crud.Tuple { + tpls := []crud.Tuple{} + for i := 1010; i < 1020; i++ { + tpls = append(tpls, crud.Tuple{uint(i), nil, "bla"}) + } + + return tpls +} + +func generateTuplesOperationsData(tpls []crud.Tuple, operations []crud.Operation) []interface{} { + tuplesOperationsData := []interface{}{} + for _, tpl := range tpls { + tuplesOperationsData = append(tuplesOperationsData, []interface{}{ + tpl, + operations, + }) + } + + return tuplesOperationsData +} + +func generateObjects() []crud.Object { + objs := []crud.Object{} + for i := 1010; i < 1020; i++ { + objs = append(objs, crud.Object{ + "id": uint(i), + "name": "bla", + }) + } + + return objs +} + +func generateObjectsOperationsData(objs []crud.Object, operations []crud.Operation) []interface{} { + objectsOperationsData := []interface{}{} + for _, obj := range objs { + objectsOperationsData = append(objectsOperationsData, []interface{}{ + obj, + operations, + }) + } + + return objectsOperationsData +} + +func getCrudError(req tarantool.Request, crudError interface{}) (interface{}, error) { + var err []interface{} + var ok bool + + code := req.Code() + if crudError != nil { + if code == tarantool.Call17RequestCode { + return crudError, nil + } + + if err, ok = crudError.([]interface{}); !ok { + return nil, fmt.Errorf("Incorrect CRUD error format") + } + + if len(err) < 1 { + return nil, fmt.Errorf("Incorrect CRUD error format") + } + + if err[0] != nil { + return err[0], nil + } + } + + return nil, nil +} + +func testCrudRequestPrepareData(t *testing.T, conn tarantool.Connector) { + t.Helper() + + for i := 1010; i < 1020; i++ { + req := tarantool.NewReplaceRequest(spaceName).Tuple( + []interface{}{uint(i), nil, "bla"}) + if _, err := conn.Do(req).Get(); err != nil { + t.Fatalf("Unable to prepare tuples: %s", err) + } + } +} + +func testSelectGeneratedData(t *testing.T, conn tarantool.Connector, + expectedTuplesCount int) { + req := tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Limit(20). + Iterator(tarantool.IterGe). + Key([]interface{}{uint(1010)}) + resp, err := conn.Do(req).Get() + if err != nil { + t.Fatalf("Failed to Select: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + if len(resp.Data) != expectedTuplesCount { + t.Fatalf("Response Data len %d != %d", len(resp.Data), expectedTuplesCount) + } +} + +func testCrudRequestCheck(t *testing.T, req tarantool.Request, + resp *tarantool.Response, err error, expectedLen int) { + t.Helper() + + if err != nil { + t.Fatalf("Failed to Do CRUD request: %s", err.Error()) + } + + if resp == nil { + t.Fatalf("Response is nil after Do CRUD request") + } + + if len(resp.Data) < expectedLen { + t.Fatalf("Response Body len < %#v, actual len %#v", + expectedLen, len(resp.Data)) + } + + // resp.Data[0] - CRUD res. + // resp.Data[1] - CRUD err. + if expectedLen >= 2 { + if crudErr, err := getCrudError(req, resp.Data[1]); err != nil { + t.Fatalf("Failed to get CRUD error: %#v", err) + } else if crudErr != nil { + t.Fatalf("Failed to perform CRUD request on CRUD side: %#v", crudErr) + } + } +} + +func TestCrudGenerateData(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + for _, testCase := range testGenerateDataCases { + t.Run(testCase.name, func(t *testing.T) { + for i := 1010; i < 1020; i++ { + conn.Delete(spaceName, nil, []interface{}{uint(i)}) + } + + resp, err := conn.Do(testCase.req).Get() + testCrudRequestCheck(t, testCase.req, resp, + err, testCase.expectedRespLen) + + testSelectGeneratedData(t, conn, testCase.expectedTuplesCount) + + for i := 1010; i < 1020; i++ { + conn.Delete(spaceName, nil, []interface{}{uint(i)}) + } + }) + } +} + +func TestCrudProcessData(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + for _, testCase := range testProcessDataCases { + t.Run(testCase.name, func(t *testing.T) { + testCrudRequestPrepareData(t, conn) + resp, err := conn.Do(testCase.req).Get() + testCrudRequestCheck(t, testCase.req, resp, + err, testCase.expectedRespLen) + + for i := 1010; i < 1020; i++ { + conn.Delete(spaceName, nil, []interface{}{uint(i)}) + } + }) + } +} + +func TestUnflattenRows_IncorrectParams(t *testing.T) { + invalidMetadata := []interface{}{ + map[interface{}]interface{}{ + "name": true, + "type": "number", + }, + map[interface{}]interface{}{ + "name": "name", + "type": "string", + }, + } + + tpls := []interface{}{ + tuple, + } + + // Format tuples with invalid format with UnflattenRows. + objs, err := crud.UnflattenRows(tpls, invalidMetadata) + require.Nil(t, objs) + require.NotNil(t, err) + require.Contains(t, err.Error(), "Unexpected space format") +} + +func TestUnflattenRows(t *testing.T) { + var ( + ok bool + err error + expectedId uint64 + actualId uint64 + res map[interface{}]interface{} + metadata []interface{} + tpls []interface{} + ) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Do `replace`. + req := crud.NewReplaceRequest(spaceName). + Tuple(tuple). + Opts(simpleOperationOpts) + resp, err := conn.Do(req).Get() + testCrudRequestCheck(t, req, resp, err, 2) + + if res, ok = resp.Data[0].(map[interface{}]interface{}); !ok { + t.Fatalf("Unexpected CRUD result: %#v", resp.Data[0]) + } + + if rawMetadata, ok := res["metadata"]; !ok { + t.Fatalf("Failed to get CRUD metadata") + } else { + if metadata, ok = rawMetadata.([]interface{}); !ok { + t.Fatalf("Unexpected CRUD metadata: %#v", rawMetadata) + } + } + + if rawTuples, ok := res["rows"]; !ok { + t.Fatalf("Failed to get CRUD rows") + } else { + if tpls, ok = rawTuples.([]interface{}); !ok { + t.Fatalf("Unexpected CRUD rows: %#v", rawTuples) + } + } + + // Format `replace` result with UnflattenRows. + objs, err := crud.UnflattenRows(tpls, metadata) + if err != nil { + t.Fatalf("Failed to unflatten rows: %#v", err) + } + if len(objs) < 1 { + t.Fatalf("Unexpected unflatten rows result: %#v", objs) + } + + if _, ok := objs[0]["bucket_id"]; ok { + delete(objs[0], "bucket_id") + } else { + t.Fatalf("Expected `bucket_id` field") + } + + require.Equal(t, len(object), len(objs[0])) + if expectedId, err = test_helpers.ConvertUint64(object["id"]); err != nil { + t.Fatalf("Unexpected `id` type") + } + + if actualId, err = test_helpers.ConvertUint64(objs[0]["id"]); err != nil { + t.Fatalf("Unexpected `id` type") + } + + require.Equal(t, expectedId, actualId) + require.Equal(t, object["name"], objs[0]["name"]) +} + +// runTestMain is a body of TestMain function +// (see https://pkg.go.dev/testing#hdr-Main). +// Using defer + os.Exit is not works so TestMain body +// is a separate function, see +// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls +func runTestMain(m *testing.M) int { + inst, err := test_helpers.StartTarantool(startOpts) + defer test_helpers.StopTarantoolWithCleanup(inst) + + if err != nil { + log.Fatalf("Failed to prepare test tarantool: %s", err) + } + + return m.Run() +} + +func TestMain(m *testing.M) { + code := runTestMain(m) + os.Exit(code) +} diff --git a/crud/testdata/config.lua b/crud/testdata/config.lua new file mode 100644 index 000000000..9663d3c13 --- /dev/null +++ b/crud/testdata/config.lua @@ -0,0 +1,74 @@ +local crud = require('crud') +local vshard = require('vshard') + +-- Do not set listen for now so connector won't be +-- able to send requests until everything is configured. +box.cfg{ + work_dir = os.getenv("TEST_TNT_WORK_DIR"), +} + +box.schema.user.grant( + 'guest', + 'read,write,execute', + 'universe' +) + +local s = box.schema.space.create('test', { + id = 617, + if_not_exists = true, + format = { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned', is_nullable = true}, + {name = 'name', type = 'string'}, + } +}) +s:create_index('primary_index', { + parts = { + {field = 1, type = 'unsigned'}, + }, +}) +s:create_index('bucket_id', { + parts = { + {field = 2, type = 'unsigned'}, + }, + unique = false, +}) + +-- Setup vshard. +_G.vshard = vshard +box.once('guest', function() + box.schema.user.grant('guest', 'super') +end) +local uri = 'guest@127.0.0.1:3013' +local cfg = { + bucket_count = 300, + sharding = { + [box.info().cluster.uuid] = { + replicas = { + [box.info().uuid] = { + uri = uri, + name = 'storage', + master = true, + }, + }, + }, + }, +} +vshard.storage.cfg(cfg, box.info().uuid) +vshard.router.cfg(cfg) +vshard.router.bootstrap() + +-- Initialize crud. +crud.init_storage() +crud.init_router() +crud.cfg{stats = true} + +box.schema.user.create('test', { password = 'test' , if_not_exists = true }) +box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true }) +box.schema.user.grant('test', 'create,read,write,drop,alter', 'space', nil, { if_not_exists = true }) +box.schema.user.grant('test', 'create', 'sequence', nil, { if_not_exists = true }) + +-- Set listen only when every other thing is configured. +box.cfg{ + listen = os.getenv("TEST_TNT_LISTEN"), +} diff --git a/crud/truncate.go b/crud/truncate.go new file mode 100644 index 000000000..0165f7703 --- /dev/null +++ b/crud/truncate.go @@ -0,0 +1,45 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +type TruncateOpts = BaseOpts + +// TruncateRequest helps you to create request object to call `crud.truncate` +// for execution by a Connection. +type TruncateRequest struct { + spaceRequest + opts TruncateOpts +} + +// NewTruncateRequest returns a new empty TruncateRequest. +func NewTruncateRequest(space string) *TruncateRequest { + req := new(TruncateRequest) + req.initImpl("crud.truncate") + req.setSpace(space) + req.opts = TruncateOpts{} + return req +} + +// Opts sets the options for the TruncateRequest request. +// Note: default value is nil. +func (req *TruncateRequest) Opts(opts TruncateOpts) *TruncateRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *TruncateRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.impl.Args([]interface{}{req.space, convertToMap(req.opts)}) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *TruncateRequest) Context(ctx context.Context) *TruncateRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/unflatten_rows.go b/crud/unflatten_rows.go new file mode 100644 index 000000000..bad45041d --- /dev/null +++ b/crud/unflatten_rows.go @@ -0,0 +1,39 @@ +package crud + +import ( + "fmt" +) + +func UnflattenRows(tuples []interface{}, format []interface{}) ([]Object, error) { + var ( + ok bool + tuple Tuple + fieldName string + fieldInfo map[interface{}]interface{} + ) + + objects := []Object{} + + for _, rawTuple := range tuples { + object := make(map[string]interface{}) + if tuple, ok = rawTuple.(Tuple); !ok { + return nil, fmt.Errorf("Unexpected tuple format: %q", rawTuple) + } + + for fieldIdx, field := range tuple { + if fieldInfo, ok = format[fieldIdx].(map[interface{}]interface{}); !ok { + return nil, fmt.Errorf("Unexpected space format: %q", format) + } + + if fieldName, ok = fieldInfo["name"].(string); !ok { + return nil, fmt.Errorf("Unexpected space format: %q", format) + } + + object[fieldName] = field + } + + objects = append(objects, object) + } + + return objects, nil +} diff --git a/crud/update.go b/crud/update.go new file mode 100644 index 000000000..91ea604f9 --- /dev/null +++ b/crud/update.go @@ -0,0 +1,63 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +type UpdateOpts = SimpleOperationOpts + +// UpdateRequest helps you to create request object to call `crud.update` +// for execution by a Connection. +type UpdateRequest struct { + spaceRequest + key Tuple + operations []Operation + opts UpdateOpts +} + +// NewUpdateRequest returns a new empty UpdateRequest. +func NewUpdateRequest(space string) *UpdateRequest { + req := new(UpdateRequest) + req.initImpl("crud.update") + req.setSpace(space) + req.key = Tuple{} + req.operations = []Operation{} + req.opts = UpdateOpts{} + return req +} + +// Key sets the key for the UpdateRequest request. +// Note: default value is nil. +func (req *UpdateRequest) Key(key Tuple) *UpdateRequest { + req.key = key + return req +} + +// Operations sets the operations for UpdateRequest request. +// Note: default value is nil. +func (req *UpdateRequest) Operations(operations []Operation) *UpdateRequest { + req.operations = operations + return req +} + +// Opts sets the options for the UpdateRequest request. +// Note: default value is nil. +func (req *UpdateRequest) Opts(opts UpdateOpts) *UpdateRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *UpdateRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.impl.Args([]interface{}{req.space, req.key, req.operations, convertToMap(req.opts)}) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *UpdateRequest) Context(ctx context.Context) *UpdateRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/upsert.go b/crud/upsert.go new file mode 100644 index 000000000..1d64b88ee --- /dev/null +++ b/crud/upsert.go @@ -0,0 +1,119 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +type UpsertOpts = SimpleOperationOpts + +// UpsertRequest helps you to create request object to call `crud.upsert` +// for execution by a Connection. +type UpsertRequest struct { + spaceRequest + tuple Tuple + operations []Operation + opts UpsertOpts +} + +// NewUpsertRequest returns a new empty UpsertRequest. +func NewUpsertRequest(space string) *UpsertRequest { + req := new(UpsertRequest) + req.initImpl("crud.upsert") + req.setSpace(space) + req.tuple = Tuple{} + req.operations = []Operation{} + req.opts = UpsertOpts{} + return req +} + +// Tuple sets the tuple for the UpsertRequest request. +// Note: default value is nil. +func (req *UpsertRequest) Tuple(tuple Tuple) *UpsertRequest { + req.tuple = tuple + return req +} + +// Operations sets the operations for the UpsertRequest request. +// Note: default value is nil. +func (req *UpsertRequest) Operations(operations []Operation) *UpsertRequest { + req.operations = operations + return req +} + +// Opts sets the options for the UpsertRequest request. +// Note: default value is nil. +func (req *UpsertRequest) Opts(opts UpsertOpts) *UpsertRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *UpsertRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.impl.Args([]interface{}{req.space, req.tuple, req.operations, convertToMap(req.opts)}) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *UpsertRequest) Context(ctx context.Context) *UpsertRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +type UpsertObjectOpts = SimpleOperationOpts + +// UpsertObjectRequest helps you to create request object to call +// `crud.upsert_object` for execution by a Connection. +type UpsertObjectRequest struct { + spaceRequest + object Object + operations []Operation + opts UpsertObjectOpts +} + +// NewUpsertObjectRequest returns a new empty UpsertObjectRequest. +func NewUpsertObjectRequest(space string) *UpsertObjectRequest { + req := new(UpsertObjectRequest) + req.initImpl("crud.upsert_object") + req.setSpace(space) + req.object = map[string]interface{}{} + req.operations = []Operation{} + req.opts = UpsertObjectOpts{} + return req +} + +// Object sets the tuple for the UpsertObjectRequest request. +// Note: default value is nil. +func (req *UpsertObjectRequest) Object(object Object) *UpsertObjectRequest { + req.object = object + return req +} + +// Operations sets the operations for the UpsertObjectRequest request. +// Note: default value is nil. +func (req *UpsertObjectRequest) Operations(operations []Operation) *UpsertObjectRequest { + req.operations = operations + return req +} + +// Opts sets the options for the UpsertObjectRequest request. +// Note: default value is nil. +func (req *UpsertObjectRequest) Opts(opts UpsertObjectOpts) *UpsertObjectRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *UpsertObjectRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.impl.Args([]interface{}{req.space, req.object, req.operations, convertToMap(req.opts)}) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *UpsertObjectRequest) Context(ctx context.Context) *UpsertObjectRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/upsert_many.go b/crud/upsert_many.go new file mode 100644 index 000000000..f1bba8c57 --- /dev/null +++ b/crud/upsert_many.go @@ -0,0 +1,104 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +type UpsertManyOpts = OperationManyOpts + +// UpsertManyRequest helps you to create request object to call +// `crud.upsert_many` for execution by a Connection. +type UpsertManyRequest struct { + spaceRequest + tuplesOperationData interface{} + opts UpsertManyOpts +} + +// NewUpsertManyRequest returns a new empty UpsertManyRequest. +func NewUpsertManyRequest(space string) *UpsertManyRequest { + req := new(UpsertManyRequest) + req.initImpl("crud.upsert_many") + req.setSpace(space) + req.tuplesOperationData = []interface{}{} + req.opts = UpsertManyOpts{} + return req +} + +// TuplesOperationData sets tuples and operations for +// the UpsertManyRequest request. +// Note: default value is nil. +func (req *UpsertManyRequest) TuplesOperationData(tuplesOperationData interface{}) *UpsertManyRequest { + req.tuplesOperationData = tuplesOperationData + return req +} + +// Opts sets the options for the UpsertManyRequest request. +// Note: default value is nil. +func (req *UpsertManyRequest) Opts(opts UpsertManyOpts) *UpsertManyRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *UpsertManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.impl.Args([]interface{}{req.space, req.tuplesOperationData, convertToMap(req.opts)}) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *UpsertManyRequest) Context(ctx context.Context) *UpsertManyRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +type UpsertObjectManyOpts = OperationManyOpts + +// UpsertObjectManyRequest helps you to create request object to call +// `crud.upsert_object_many` for execution by a Connection. +type UpsertObjectManyRequest struct { + spaceRequest + objectsOperationData interface{} + opts UpsertObjectManyOpts +} + +// NewUpsertObjectManyRequest returns a new empty UpsertObjectManyRequest. +func NewUpsertObjectManyRequest(space string) *UpsertObjectManyRequest { + req := new(UpsertObjectManyRequest) + req.initImpl("crud.upsert_object_many") + req.setSpace(space) + req.objectsOperationData = []interface{}{} + req.opts = UpsertObjectManyOpts{} + return req +} + +// ObjectOperationData sets objects and operations +// for the UpsertObjectManyRequest request. +// Note: default value is nil. +func (req *UpsertObjectManyRequest) ObjectsOperationData( + objectsOperationData interface{}) *UpsertObjectManyRequest { + req.objectsOperationData = objectsOperationData + return req +} + +// Opts sets the options for the UpsertObjectManyRequest request. +// Note: default value is nil. +func (req *UpsertObjectManyRequest) Opts(opts UpsertObjectManyOpts) *UpsertObjectManyRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *UpsertObjectManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.impl.Args([]interface{}{req.space, req.objectsOperationData, convertToMap(req.opts)}) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *UpsertObjectManyRequest) Context(ctx context.Context) *UpsertObjectManyRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/go.mod b/go.mod index 2f3b8c226..73910b22a 100644 --- a/go.mod +++ b/go.mod @@ -5,14 +5,15 @@ go 1.11 require ( github.com/google/go-cmp v0.5.7 // indirect github.com/google/uuid v1.3.0 + github.com/markphelps/optional v0.10.0 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/pkg/errors v0.9.1 // indirect github.com/shopspring/decimal v1.3.1 github.com/stretchr/testify v1.7.1 // indirect github.com/tarantool/go-openssl v0.0.8-0.20220711094538-d93c1eff4f49 + github.com/vmihailenco/msgpack/v5 v5.3.5 google.golang.org/appengine v1.6.7 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/vmihailenco/msgpack.v2 v2.9.2 gotest.tools/v3 v3.2.0 // indirect - github.com/vmihailenco/msgpack/v5 v5.3.5 ) diff --git a/go.sum b/go.sum index 38e70ec44..ab6d3a98d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 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/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -7,9 +9,12 @@ github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 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/markphelps/optional v0.10.0 h1:vTaMRRTuN7aPY5X8g6K82W23qR4VMqBNyniC5BIJlqo= +github.com/markphelps/optional v0.10.0/go.mod h1:Fvjs1vxcm7/wDqJPFGEiEM1RuxFl9GCyxQlj9M9YMAQ= github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= @@ -24,6 +29,7 @@ github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+ github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -42,6 +48,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -64,10 +71,12 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/vmihailenco/msgpack.v2 v2.9.2 h1:gjPqo9orRVlSAH/065qw3MsFCDpH7fa1KpiizXyllY4= gopkg.in/vmihailenco/msgpack.v2 v2.9.2/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= diff --git a/request_test.go b/request_test.go index 32bdd87e1..8429fef98 100644 --- a/request_test.go +++ b/request_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" . "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/test_helpers" ) const invalidSpaceMsg = "invalid space" @@ -85,17 +86,13 @@ func assertBodyCall(t testing.TB, requests []Request, errorMsg string) { func assertBodyEqual(t testing.TB, reference []byte, req Request) { t.Helper() - var reqBuf bytes.Buffer - reqEnc := NewEncoder(&reqBuf) - - err := req.Body(&resolver, reqEnc) + reqBody, err := test_helpers.ExtractRequestBody(req, &resolver, NewEncoder) if err != nil { - t.Errorf("An unexpected Response.Body() error: %q", err.Error()) - } else { - reqBody := reqBuf.Bytes() - if !bytes.Equal(reqBody, reference) { - t.Errorf("Encoded request %v != reference %v", reqBody, reference) - } + t.Fatalf("An unexpected Response.Body() error: %q", err.Error()) + } + + if !bytes.Equal(reqBody, reference) { + t.Errorf("Encoded request %v != reference %v", reqBody, reference) } } diff --git a/tarantool_test.go b/tarantool_test.go index 3c11aa4ea..f53e6b528 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -67,36 +67,6 @@ func (m *Member) DecodeMsgpack(d *decoder) error { return nil } -// msgpack.v2 and msgpack.v5 return different uint types in responses. The -// function helps to unify a result. -func convertUint64(v interface{}) (result uint64, err error) { - switch v := v.(type) { - case uint: - result = uint64(v) - case uint8: - result = uint64(v) - case uint16: - result = uint64(v) - case uint32: - result = uint64(v) - case uint64: - result = uint64(v) - case int: - result = uint64(v) - case int8: - result = uint64(v) - case int16: - result = uint64(v) - case int32: - result = uint64(v) - case int64: - result = uint64(v) - default: - err = fmt.Errorf("Non-number value %T", v) - } - return -} - var server = "127.0.0.1:3013" var spaceNo = uint32(617) var spaceName = "test" @@ -874,7 +844,7 @@ func TestClient(t *testing.T) { if len(tpl) != 3 { t.Errorf("Unexpected body of Insert (tuple len)") } - if id, err := convertUint64(tpl[0]); err != nil || id != 1 { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 1 { t.Errorf("Unexpected body of Insert (0)") } if h, ok := tpl[1].(string); !ok || h != "hello" { @@ -910,7 +880,7 @@ func TestClient(t *testing.T) { if len(tpl) != 3 { t.Errorf("Unexpected body of Delete (tuple len)") } - if id, err := convertUint64(tpl[0]); err != nil || id != 1 { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 1 { t.Errorf("Unexpected body of Delete (0)") } if h, ok := tpl[1].(string); !ok || h != "hello" { @@ -958,7 +928,7 @@ func TestClient(t *testing.T) { if len(tpl) != 3 { t.Errorf("Unexpected body of Replace (tuple len)") } - if id, err := convertUint64(tpl[0]); err != nil || id != 2 { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 2 { t.Errorf("Unexpected body of Replace (0)") } if h, ok := tpl[1].(string); !ok || h != "hi" { @@ -986,7 +956,7 @@ func TestClient(t *testing.T) { if len(tpl) != 2 { t.Errorf("Unexpected body of Update (tuple len)") } - if id, err := convertUint64(tpl[0]); err != nil || id != 2 { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 2 { t.Errorf("Unexpected body of Update (0)") } if h, ok := tpl[1].(string); !ok || h != "bye" { @@ -1042,7 +1012,7 @@ func TestClient(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Select") } else { - if id, err := convertUint64(tpl[0]); err != nil || id != 10 { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 10 { t.Errorf("Unexpected body of Select (0)") } if h, ok := tpl[1].(string); !ok || h != "val 10" { @@ -1175,7 +1145,7 @@ func TestClient(t *testing.T) { if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after Eval") } - if val, err := convertUint64(resp.Data[0]); err != nil || val != 11 { + if val, err := test_helpers.ConvertUint64(resp.Data[0]); err != nil || val != 11 { t.Errorf("5 + 6 == 11, but got %v", val) } } @@ -1220,7 +1190,7 @@ func TestClientSessionPush(t *testing.T) { t.Errorf("Response is nil after CallAsync") } else if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after Call17Async") - } else if val, err := convertUint64(resp.Data[0]); err != nil || val != pushMax { + } else if val, err := test_helpers.ConvertUint64(resp.Data[0]); err != nil || val != pushMax { t.Errorf("Result is not %d: %v", pushMax, resp.Data) } @@ -1252,12 +1222,12 @@ func TestClientSessionPush(t *testing.T) { } if resp.Code == PushCode { pushCnt += 1 - if val, err := convertUint64(resp.Data[0]); err != nil || val != pushCnt { + if val, err := test_helpers.ConvertUint64(resp.Data[0]); err != nil || val != pushCnt { t.Errorf("Unexpected push data = %v", resp.Data) } } else { respCnt += 1 - if val, err := convertUint64(resp.Data[0]); err != nil || val != pushMax { + if val, err := test_helpers.ConvertUint64(resp.Data[0]); err != nil || val != pushMax { t.Errorf("Result is not %d: %v", pushMax, resp.Data) } } @@ -1281,7 +1251,7 @@ func TestClientSessionPush(t *testing.T) { resp, err := fut.Get() if err != nil { t.Errorf("Unable to call fut.Get(): %s", err) - } else if val, err := convertUint64(resp.Data[0]); err != nil || val != pushMax { + } else if val, err := test_helpers.ConvertUint64(resp.Data[0]); err != nil || val != pushMax { t.Errorf("Result is not %d: %v", pushMax, resp.Data) } @@ -2150,7 +2120,7 @@ func TestClientRequestObjects(t *testing.T) { if len(tpl) != 3 { t.Errorf("Unexpected body of Insert (tuple len)") } - if id, err := convertUint64(tpl[0]); err != nil || id != uint64(i) { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != uint64(i) { t.Errorf("Unexpected body of Insert (0)") } if h, ok := tpl[1].(string); !ok || h != fmt.Sprintf("val %d", i) { @@ -2188,7 +2158,7 @@ func TestClientRequestObjects(t *testing.T) { if len(tpl) != 3 { t.Errorf("Unexpected body of Replace (tuple len)") } - if id, err := convertUint64(tpl[0]); err != nil || id != uint64(i) { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != uint64(i) { t.Errorf("Unexpected body of Replace (0)") } if h, ok := tpl[1].(string); !ok || h != fmt.Sprintf("val %d", i) { @@ -2225,7 +2195,7 @@ func TestClientRequestObjects(t *testing.T) { if len(tpl) != 3 { t.Errorf("Unexpected body of Delete (tuple len)") } - if id, err := convertUint64(tpl[0]); err != nil || id != uint64(1016) { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != uint64(1016) { t.Errorf("Unexpected body of Delete (0)") } if h, ok := tpl[1].(string); !ok || h != "val 1016" { @@ -2259,7 +2229,7 @@ func TestClientRequestObjects(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Update") } else { - if id, err := convertUint64(tpl[0]); err != nil || id != uint64(1010) { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != uint64(1010) { t.Errorf("Unexpected body of Update (0)") } if h, ok := tpl[1].(string); !ok || h != "val 1010" { @@ -2294,13 +2264,13 @@ func TestClientRequestObjects(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Select") } else { - if id, err := convertUint64(tpl[0]); err != nil || id != 1010 { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 1010 { t.Errorf("Unexpected body of Update (0)") } if h, ok := tpl[1].(string); !ok || h != "bye" { t.Errorf("Unexpected body of Update (1)") } - if h, err := convertUint64(tpl[2]); err != nil || h != 1 { + if h, err := test_helpers.ConvertUint64(tpl[2]); err != nil || h != 1 { t.Errorf("Unexpected body of Update (2)") } } @@ -2387,7 +2357,7 @@ func TestClientRequestObjects(t *testing.T) { if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after Eval") } - if val, err := convertUint64(resp.Data[0]); err != nil || val != 11 { + if val, err := test_helpers.ConvertUint64(resp.Data[0]); err != nil || val != 11 { t.Errorf("5 + 6 == 11, but got %v", val) } @@ -2479,7 +2449,7 @@ func testConnectionDoSelectRequestCheck(t *testing.T, if tpl, ok := resp.Data[i].([]interface{}); !ok { t.Errorf("Unexpected body of Select") } else { - if id, err := convertUint64(tpl[0]); err != nil || id != key { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != key { t.Errorf("Unexpected body of Select (0) %v, expected %d", tpl[0], key) } @@ -2798,7 +2768,7 @@ func TestStream_Commit(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Fatalf("Unexpected body of Select") } else { - if id, err := convertUint64(tpl[0]); err != nil || id != 1001 { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 1001 { t.Fatalf("Unexpected body of Select (0)") } if h, ok := tpl[1].(string); !ok || h != "hello2" { @@ -2833,7 +2803,7 @@ func TestStream_Commit(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Fatalf("Unexpected body of Select") } else { - if id, err := convertUint64(tpl[0]); err != nil || id != 1001 { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 1001 { t.Fatalf("Unexpected body of Select (0)") } if h, ok := tpl[1].(string); !ok || h != "hello2" { @@ -2913,7 +2883,7 @@ func TestStream_Rollback(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Fatalf("Unexpected body of Select") } else { - if id, err := convertUint64(tpl[0]); err != nil || id != 1001 { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 1001 { t.Fatalf("Unexpected body of Select (0)") } if h, ok := tpl[1].(string); !ok || h != "hello2" { @@ -3018,7 +2988,7 @@ func TestStream_TxnIsolationLevel(t *testing.T) { require.Truef(t, ok, "unexpected body of Select") require.Equalf(t, 3, len(tpl), "unexpected body of Select") - key, err := convertUint64(tpl[0]) + key, err := test_helpers.ConvertUint64(tpl[0]) require.Nilf(t, err, "unexpected body of Select (0)") require.Equalf(t, uint64(1001), key, "unexpected body of Select (0)") diff --git a/test_helpers/main.go b/test_helpers/main.go index 3363ed375..4aaa91f50 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -372,3 +372,33 @@ func copyFile(srcFile, dstFile string) error { return nil } + +// msgpack.v2 and msgpack.v5 return different uint types in responses. The +// function helps to unify a result. +func ConvertUint64(v interface{}) (result uint64, err error) { + switch v := v.(type) { + case uint: + result = uint64(v) + case uint8: + result = uint64(v) + case uint16: + result = uint64(v) + case uint32: + result = uint64(v) + case uint64: + result = uint64(v) + case int: + result = uint64(v) + case int8: + result = uint64(v) + case int16: + result = uint64(v) + case int32: + result = uint64(v) + case int64: + result = uint64(v) + default: + err = fmt.Errorf("Non-number value %T", v) + } + return +} diff --git a/test_helpers/utils.go b/test_helpers/utils.go index 12c65009e..34a2e2980 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -1,7 +1,9 @@ package test_helpers import ( + "bytes" "fmt" + "io" "testing" "time" @@ -228,3 +230,16 @@ func CheckEqualBoxErrors(t *testing.T, expected tarantool.BoxError, actual taran } } } + +func ExtractRequestBody(req tarantool.Request, resolver tarantool.SchemaResolver, + newEncFunc func(w io.Writer) *encoder) ([]byte, error) { + var reqBuf bytes.Buffer + reqEnc := newEncFunc(&reqBuf) + + err := req.Body(resolver, reqEnc) + if err != nil { + return nil, fmt.Errorf("An unexpected Response.Body() error: %q", err.Error()) + } + + return reqBuf.Bytes(), nil +}