From c86f41d2d3fb3304517e40656c6d345275d78d0c Mon Sep 17 00:00:00 2001 From: AnaNek Date: Wed, 11 Jan 2023 12:55:09 +0300 Subject: [PATCH] api: add CRUD module support This patch provides crud [1] methods as request objects to support CRUD API. The following methods are supported: * `insert` * `insert_object` * `insert_many` * `insert_object_many` * `get` * `update` * `delete` * `replace` * `replace_object` * `replace_many` * `replace_object_many` * `upsert` * `upsert_object` * `upsert_many` * `upsert_object_many` * `select` * `min` * `max` * `truncate` * `len` * `storage_info` * `count` * `stats` * `unflatten_rows` 1. https://github.com/tarantool/crud Closes #108 --- CHANGELOG.md | 1 + Makefile | 10 +- crud/common.go | 95 +++++ crud/conditions.go | 28 ++ crud/count.go | 109 ++++++ crud/delete.go | 66 ++++ crud/error.go | 86 +++++ crud/error_test.go | 27 ++ crud/get.go | 101 +++++ crud/insert.go | 125 ++++++ crud/insert_many.go | 125 ++++++ crud/len.go | 56 +++ crud/max.go | 66 ++++ crud/min.go | 66 ++++ crud/msgpack.go | 29 ++ crud/msgpack_v5.go | 29 ++ crud/operations.go | 33 ++ crud/options.go | 306 +++++++++++++++ crud/replace.go | 125 ++++++ crud/replace_many.go | 125 ++++++ crud/request_test.go | 533 ++++++++++++++++++++++++++ crud/result.go | 285 ++++++++++++++ crud/select.go | 117 ++++++ crud/stats.go | 46 +++ crud/storage_info.go | 130 +++++++ crud/tarantool_test.go | 807 +++++++++++++++++++++++++++++++++++++++ crud/testdata/config.lua | 99 +++++ crud/truncate.go | 56 +++ crud/unflatten_rows.go | 40 ++ crud/update.go | 77 ++++ crud/upsert.go | 147 +++++++ crud/upsert_many.go | 130 +++++++ go.mod | 3 +- go.sum | 9 + request_test.go | 17 +- tarantool_test.go | 74 ++-- test_helpers/main.go | 30 ++ test_helpers/utils.go | 15 + 38 files changed, 4159 insertions(+), 64 deletions(-) create mode 100644 crud/common.go create mode 100644 crud/conditions.go create mode 100644 crud/count.go create mode 100644 crud/delete.go create mode 100644 crud/error.go create mode 100644 crud/error_test.go create mode 100644 crud/get.go create mode 100644 crud/insert.go create mode 100644 crud/insert_many.go create mode 100644 crud/len.go create mode 100644 crud/max.go create mode 100644 crud/min.go create mode 100644 crud/msgpack.go create mode 100644 crud/msgpack_v5.go create mode 100644 crud/operations.go create mode 100644 crud/options.go create mode 100644 crud/replace.go create mode 100644 crud/replace_many.go create mode 100644 crud/request_test.go create mode 100644 crud/result.go create mode 100644 crud/select.go create mode 100644 crud/stats.go create mode 100644 crud/storage_info.go create mode 100644 crud/tarantool_test.go create mode 100644 crud/testdata/config.lua create mode 100644 crud/truncate.go create mode 100644 crud/unflatten_rows.go create mode 100644 crud/update.go create mode 100644 crud/upsert.go create mode 100644 crud/upsert_many.go 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..2f4dd8d93 100644 --- a/Makefile +++ b/Makefile @@ -21,12 +21,13 @@ endif .PHONY: clean clean: - ( cd ./queue; rm -rf .rocks ) + ( rm -rf queue/testdata/.rocks crud/testdata/.rocks ) rm -f $(COVERAGE_FILE) .PHONY: deps deps: clean ( cd ./queue/testdata; $(TTCTL) rocks install queue 1.2.1 ) + ( cd ./crud/testdata; $(TTCTL) rocks install crud 1.0.0 ) .PHONY: datetime-timezones datetime-timezones: @@ -99,6 +100,13 @@ 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/testdata && tarantool -e "require('crud')" + go clean -testcache + go test -tags "$(TAGS)" ./crud/ -v -p 1 + .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..877ac2d4a --- /dev/null +++ b/crud/common.go @@ -0,0 +1,95 @@ +// Package crud with support of API of Tarantool's CRUD module. +// +// Supported CRUD methods: +// +// - insert +// +// - insert_object +// +// - insert_many +// +// - insert_object_many +// +// - get +// +// - update +// +// - delete +// +// - replace +// +// - replace_object +// +// - replace_many +// +// - replace_object_many +// +// - upsert +// +// - upsert_object +// +// - upsert_many +// +// - upsert_object_many +// +// - select +// +// - min +// +// - max +// +// - truncate +// +// - len +// +// - storage_info +// +// - count +// +// - stats +// +// - unflatten_rows +// +// Since: 1.11.0. +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// Tuple is a type to describe tuple for CRUD methods. +type Tuple = []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..4d708bb72 --- /dev/null +++ b/crud/conditions.go @@ -0,0 +1,28 @@ +package crud + +// Operator is a type to describe operator of operation. +type Operator string + +const ( + // Eq - comparison operator for "equal". + Eq Operator = "=" + // Lt - comparison operator for "less than". + Lt Operator = "<" + // Le - comparison operator for "less than or equal". + Le Operator = "<=" + // Gt - comparison operator for "greater than". + Gt Operator = ">" + // Ge - comparison operator for "greater than or equal". + Ge Operator = ">=" +) + +// Condition describes CRUD condition as a table +// {operator, field-identifier, value}. +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..2a3992389 --- /dev/null +++ b/crud/count.go @@ -0,0 +1,109 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// CountResult describes result for `crud.count` method. +type CountResult = NumberResult + +// CountOpts describes options for `crud.count` method. +type CountOpts struct { + // Timeout is a `vshard.call` timeout and vshard + // master discovery timeout (in seconds). + Timeout OptUint + // VshardRouter is cartridge vshard group name or + // vshard router instance. + VshardRouter OptString + // Mode is a parameter with `write`/`read` possible values, + // if `write` is specified then operation is performed on master. + Mode OptString + // PreferReplica is a parameter to specify preferred target + // as one of the replicas. + PreferReplica OptBool + // Balance is a parameter to use replica according to vshard + // load balancing policy. + Balance OptBool + // YieldEvery describes number of tuples processed to yield after. + YieldEvery OptUint + // BucketId is a bucket ID. + BucketId OptUint + // ForceMapCall describes the map call is performed without any + // optimizations even if full primary key equal condition is specified. + ForceMapCall OptBool + // Fullscan describes if a critical log entry will be skipped on + // potentially long count. + Fullscan OptBool +} + +// EncodeMsgpack provides custom msgpack encoder. +func (opts CountOpts) EncodeMsgpack(enc *encoder) error { + const optsCnt = 9 + + options := [optsCnt]option{opts.Timeout, opts.VshardRouter, + opts.Mode, opts.PreferReplica, opts.Balance, + opts.YieldEvery, opts.BucketId, opts.ForceMapCall, + opts.Fullscan} + names := [optsCnt]string{timeoutOptName, vshardRouterOptName, + modeOptName, preferReplicaOptName, balanceOptName, + yieldEveryOptName, bucketIdOptName, forceMapCallOptName, + fullscanOptName} + values := [optsCnt]interface{}{} + + return encodeOptions(enc, options[:], names[:], values[:]) +} + +// CountRequest helps you to create request object to call `crud.count` +// for execution by a Connection. +type CountRequest struct { + spaceRequest + conditions []Condition + opts CountOpts +} + +type countArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + 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 = nil + 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 { + args := countArgs{Space: req.space, Conditions: req.conditions, 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 *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..6592d1519 --- /dev/null +++ b/crud/delete.go @@ -0,0 +1,66 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// DeleteResult describes result for `crud.delete` method. +type DeleteResult = Result + +// DeleteOpts describes options for `crud.delete` method. +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 +} + +type deleteArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + 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 { + args := deleteArgs{Space: req.space, Key: req.key, 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 *DeleteRequest) Context(ctx context.Context) *DeleteRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/error.go b/crud/error.go new file mode 100644 index 000000000..467c350a4 --- /dev/null +++ b/crud/error.go @@ -0,0 +1,86 @@ +package crud + +import "strings" + +// Error describes CRUD error object. +type Error struct { + // ClassName is an error class that implies its source (for example, "CountError"). + ClassName string + // Err is the text of reason. + Err string + // File is a source code file where the error was caught. + File string + // Line is a number of line in the source code file where the error was caught. + Line uint64 + // Stack is an information about the call stack when an error + // occurs in a string format. + Stack string + // Str is the text of reason with error class. + Str string +} + +// DecodeMsgpack provides custom msgpack decoder. +func (e *Error) DecodeMsgpack(d *decoder) error { + l, err := d.DecodeMapLen() + if err != nil { + return err + } + for i := 0; i < l; i++ { + key, err := d.DecodeString() + if err != nil { + return err + } + switch key { + case "class_name": + if e.ClassName, err = d.DecodeString(); err != nil { + return err + } + case "err": + if e.Err, err = d.DecodeString(); err != nil { + return err + } + case "file": + if e.File, err = d.DecodeString(); err != nil { + return err + } + case "line": + if e.Line, err = d.DecodeUint64(); err != nil { + return err + } + case "stack": + if e.Stack, err = d.DecodeString(); err != nil { + return err + } + case "str": + if e.Str, err = d.DecodeString(); err != nil { + return err + } + default: + if err := d.Skip(); err != nil { + return err + } + } + } + + return nil +} + +// Error converts an Error to a string. +func (err Error) Error() string { + return err.Str +} + +// ErrorMany describes CRUD error object for `_many` methods. +type ErrorMany struct { + Errors []Error +} + +// Error converts an Error to a string. +func (errs ErrorMany) Error() string { + var str []string + for _, err := range errs.Errors { + str = append(str, err.Str) + } + + return strings.Join(str, "\n") +} diff --git a/crud/error_test.go b/crud/error_test.go new file mode 100644 index 000000000..8bd973399 --- /dev/null +++ b/crud/error_test.go @@ -0,0 +1,27 @@ +package crud_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool/crud" +) + +func TestErrorMany(t *testing.T) { + errs := crud.ErrorMany{Errors: []crud.Error{ + { + ClassName: "a", + Str: "msg 1", + }, + { + ClassName: "b", + Str: "msg 2", + }, + { + ClassName: "c", + Str: "msg 3", + }, + }} + + require.Equal(t, "msg 1\nmsg 2\nmsg 3", errs.Error()) +} diff --git a/crud/get.go b/crud/get.go new file mode 100644 index 000000000..4234431f6 --- /dev/null +++ b/crud/get.go @@ -0,0 +1,101 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// GetResult describes result for `crud.get` method. +type GetResult = Result + +// GetOpts describes options for `crud.get` method. +type GetOpts struct { + // Timeout is a `vshard.call` timeout and vshard + // master discovery timeout (in seconds). + Timeout OptUint + // VshardRouter is cartridge vshard group name or + // vshard router instance. + VshardRouter OptString + // Fields is field names for getting only a subset of fields. + Fields OptTuple + // BucketId is a bucket ID. + BucketId OptUint + // Mode is a parameter with `write`/`read` possible values, + // if `write` is specified then operation is performed on master. + Mode OptString + // PreferReplica is a parameter to specify preferred target + // as one of the replicas. + PreferReplica OptBool + // Balance is a parameter to use replica according to vshard + // load balancing policy. + Balance OptBool +} + +// EncodeMsgpack provides custom msgpack encoder. +func (opts GetOpts) EncodeMsgpack(enc *encoder) error { + const optsCnt = 7 + + options := [optsCnt]option{opts.Timeout, opts.VshardRouter, + opts.Fields, opts.BucketId, opts.Mode, + opts.PreferReplica, opts.Balance} + names := [optsCnt]string{timeoutOptName, vshardRouterOptName, + fieldsOptName, bucketIdOptName, modeOptName, + preferReplicaOptName, balanceOptName} + values := [optsCnt]interface{}{} + + return encodeOptions(enc, options[:], names[:], values[:]) +} + +// GetRequest helps you to create request object to call `crud.get` +// for execution by a Connection. +type GetRequest struct { + spaceRequest + key Tuple + opts GetOpts +} + +type getArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + 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 { + args := getArgs{Space: req.space, Key: req.key, 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 *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..480300249 --- /dev/null +++ b/crud/insert.go @@ -0,0 +1,125 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// InsertResult describes result for `crud.insert` method. +type InsertResult = Result + +// InsertOpts describes options for `crud.insert` method. +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 +} + +type insertArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + 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 { + args := insertArgs{Space: req.space, Tuple: req.tuple, 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 *InsertRequest) Context(ctx context.Context) *InsertRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +// InsertObjectResult describes result for `crud.insert_object` method. +type InsertObjectResult = Result + +// InsertObjectOpts describes options for `crud.insert_object` method. +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 +} + +type insertObjectArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + 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 = MapObject{} + 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 { + args := insertObjectArgs{Space: req.space, Object: req.object, 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 *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..11784f660 --- /dev/null +++ b/crud/insert_many.go @@ -0,0 +1,125 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// InsertManyResult describes result for `crud.insert_many` method. +type InsertManyResult = ResultMany + +// InsertManyOpts describes options for `crud.insert_many` method. +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 +} + +type insertManyArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + 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 { + args := insertManyArgs{Space: req.space, Tuples: req.tuples, 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 *InsertManyRequest) Context(ctx context.Context) *InsertManyRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +// InsertObjectManyResult describes result for `crud.insert_object_many` method. +type InsertObjectManyResult = ResultMany + +// InsertObjectManyOpts describes options for `crud.insert_object_many` method. +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 []Object + opts InsertObjectManyOpts +} + +type insertObjectManyArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + Objects []Object + 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 = []Object{} + req.opts = InsertObjectManyOpts{} + return req +} + +// Objects sets the objects for the InsertObjectManyRequest request. +// Note: default value is nil. +func (req *InsertObjectManyRequest) Objects(objects []Object) *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 { + args := insertObjectManyArgs{Space: req.space, Objects: req.objects, 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 *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..6a8c85a2a --- /dev/null +++ b/crud/len.go @@ -0,0 +1,56 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// LenResult describes result for `crud.len` method. +type LenResult = NumberResult + +// LenOpts describes options for `crud.len` method. +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 string + 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..842f24d5a --- /dev/null +++ b/crud/max.go @@ -0,0 +1,66 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// MaxResult describes result for `crud.max` method. +type MaxResult = Result + +// MaxOpts describes options for `crud.max` method. +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 +} + +type maxArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + 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 { + args := maxArgs{Space: req.space, Index: req.index, 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 *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..720b6f782 --- /dev/null +++ b/crud/min.go @@ -0,0 +1,66 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// MinResult describes result for `crud.min` method. +type MinResult = Result + +// MinOpts describes options for `crud.min` method. +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 +} + +type minArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + 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 { + args := minArgs{Space: req.space, Index: req.index, 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 *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..fe65bd154 --- /dev/null +++ b/crud/msgpack.go @@ -0,0 +1,29 @@ +//go:build !go_tarantool_msgpack_v5 +// +build !go_tarantool_msgpack_v5 + +package crud + +import ( + "io" + + "gopkg.in/vmihailenco/msgpack.v2" +) + +type encoder = msgpack.Encoder +type decoder = msgpack.Decoder + +// Object is an interface to describe object for CRUD methods. +type Object interface { + EncodeMsgpack(enc *encoder) +} + +// MapObject is a type to describe object as a map. +type MapObject map[string]interface{} + +func (o MapObject) EncodeMsgpack(enc *encoder) { + enc.Encode(o) +} + +func NewEncoder(w io.Writer) *encoder { + return msgpack.NewEncoder(w) +} diff --git a/crud/msgpack_v5.go b/crud/msgpack_v5.go new file mode 100644 index 000000000..bfa936a83 --- /dev/null +++ b/crud/msgpack_v5.go @@ -0,0 +1,29 @@ +//go:build go_tarantool_msgpack_v5 +// +build go_tarantool_msgpack_v5 + +package crud + +import ( + "io" + + "github.com/vmihailenco/msgpack/v5" +) + +type encoder = msgpack.Encoder +type decoder = msgpack.Decoder + +// Object is an interface to describe object for CRUD methods. +type Object interface { + EncodeMsgpack(enc *encoder) +} + +// MapObject is a type to describe object as a map. +type MapObject map[string]interface{} + +func (o MapObject) EncodeMsgpack(enc *encoder) { + enc.Encode(o) +} + +func NewEncoder(w io.Writer) *encoder { + return msgpack.NewEncoder(w) +} diff --git a/crud/operations.go b/crud/operations.go new file mode 100644 index 000000000..bbab528b9 --- /dev/null +++ b/crud/operations.go @@ -0,0 +1,33 @@ +package crud + +const ( + // Add - operator for addition. + Add Operator = "+" + // Sub - operator for subtraction. + Sub Operator = "-" + // And - operator for bitwise AND. + And Operator = "&" + // Or - operator for bitwise OR. + Or Operator = "|" + // Xor - operator for bitwise XOR. + Xor Operator = "^" + // Splice - operator for string splice. + Splice Operator = ":" + // Insert - operator for insertion of a new field. + Insert Operator = "!" + // Delete - operator for deletion. + Delete Operator = "#" + // Assign - operator for assignment. + Assign Operator = "=" +) + +// Operation describes CRUD operation as a table +// {operator, field_identifier, value}. +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..c0a201033 --- /dev/null +++ b/crud/options.go @@ -0,0 +1,306 @@ +package crud + +import ( + "errors" + + "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" + modeOptName = "mode" + preferReplicaOptName = "prefer_replica" + balanceOptName = "balance" + yieldEveryOptName = "yield_every" + forceMapCallOptName = "force_map_call" + fullscanOptName = "fullscan" + firstOptName = "first" + afterOptName = "after" + batchSizeOptName = "batch_size" +) + +type option interface { + getInterface() (interface{}, error) +} + +// OptUint is an optional uint. +type OptUint struct { + optional.Uint +} + +// NewOptUint creates an optional uint from value. +func NewOptUint(value uint) OptUint { + return OptUint{optional.NewUint(value)} +} + +func (opt OptUint) getInterface() (interface{}, error) { + return opt.Get() +} + +// OptInt is an optional int. +type OptInt struct { + optional.Int +} + +// NewOptInt creates an optional int from value. +func NewOptInt(value int) OptInt { + return OptInt{optional.NewInt(value)} +} + +func (opt OptInt) getInterface() (interface{}, error) { + return opt.Get() +} + +// OptString is an optional string. +type OptString struct { + optional.String +} + +// NewOptString creates an optional string from value. +func NewOptString(value string) OptString { + return OptString{optional.NewString(value)} +} + +func (opt OptString) getInterface() (interface{}, error) { + return opt.Get() +} + +// OptBool is an optional bool. +type OptBool struct { + optional.Bool +} + +// NewOptBool creates an optional bool from value. +func NewOptBool(value bool) OptBool { + return OptBool{optional.NewBool(value)} +} + +func (opt OptBool) getInterface() (interface{}, error) { + return opt.Get() +} + +// OptTuple is an optional tuple. +type OptTuple struct { + tuple []interface{} +} + +// NewOptTuple creates an optional tuple from tuple. +func NewOptTuple(tuple []interface{}) OptTuple { + return OptTuple{tuple} +} + +// Get returns the tuple value or an error if not present. +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() +} + +// BaseOpts describes base options for CRUD operations. +type BaseOpts struct { + // Timeout is a `vshard.call` timeout and vshard + // master discovery timeout (in seconds). + Timeout OptUint + // VshardRouter is cartridge vshard group name or + // vshard router instance. + VshardRouter OptString +} + +// EncodeMsgpack provides custom msgpack encoder. +func (opts BaseOpts) EncodeMsgpack(enc *encoder) error { + const optsCnt = 2 + + options := [optsCnt]option{opts.Timeout, opts.VshardRouter} + names := [optsCnt]string{timeoutOptName, vshardRouterOptName} + values := [optsCnt]interface{}{} + + return encodeOptions(enc, options[:], names[:], values[:]) +} + +// SimpleOperationOpts describes options for simple CRUD operations. +type SimpleOperationOpts struct { + // Timeout is a `vshard.call` timeout and vshard + // master discovery timeout (in seconds). + Timeout OptUint + // VshardRouter is cartridge vshard group name or + // vshard router instance. + VshardRouter OptString + // Fields is field names for getting only a subset of fields. + Fields OptTuple + // BucketId is a bucket ID. + BucketId OptUint +} + +// EncodeMsgpack provides custom msgpack encoder. +func (opts SimpleOperationOpts) EncodeMsgpack(enc *encoder) error { + const optsCnt = 4 + + options := [optsCnt]option{opts.Timeout, opts.VshardRouter, + opts.Fields, opts.BucketId} + names := [optsCnt]string{timeoutOptName, vshardRouterOptName, + fieldsOptName, bucketIdOptName} + values := [optsCnt]interface{}{} + + return encodeOptions(enc, options[:], names[:], values[:]) +} + +// SimpleOperationObjectOpts describes options for simple CRUD +// operations with objects. +type SimpleOperationObjectOpts struct { + // Timeout is a `vshard.call` timeout and vshard + // master discovery timeout (in seconds). + Timeout OptUint + // VshardRouter is cartridge vshard group name or + // vshard router instance. + VshardRouter OptString + // Fields is field names for getting only a subset of fields. + Fields OptTuple + // BucketId is a bucket ID. + BucketId OptUint + // SkipNullabilityCheckOnFlatten is a parameter to allow + // setting null values to non-nullable fields. + SkipNullabilityCheckOnFlatten OptBool +} + +// EncodeMsgpack provides custom msgpack encoder. +func (opts SimpleOperationObjectOpts) EncodeMsgpack(enc *encoder) error { + const optsCnt = 5 + + options := [optsCnt]option{opts.Timeout, opts.VshardRouter, + opts.Fields, opts.BucketId, opts.SkipNullabilityCheckOnFlatten} + names := [optsCnt]string{timeoutOptName, vshardRouterOptName, + fieldsOptName, bucketIdOptName, skipNullabilityCheckOnFlattenOptName} + values := [optsCnt]interface{}{} + + return encodeOptions(enc, options[:], names[:], values[:]) +} + +// OperationManyOpts describes options for CRUD operations with many tuples. +type OperationManyOpts struct { + // Timeout is a `vshard.call` timeout and vshard + // master discovery timeout (in seconds). + Timeout OptUint + // VshardRouter is cartridge vshard group name or + // vshard router instance. + VshardRouter OptString + // Fields is field names for getting only a subset of fields. + Fields OptTuple + // StopOnError is a parameter to stop on a first error and report + // error regarding the failed operation and error about what tuples + // were not performed. + StopOnError OptBool + // RollbackOnError is a parameter because of what any failed operation + // will lead to rollback on a storage, where the operation is failed. + RollbackOnError OptBool +} + +// EncodeMsgpack provides custom msgpack encoder. +func (opts OperationManyOpts) EncodeMsgpack(enc *encoder) error { + const optsCnt = 5 + + options := [optsCnt]option{opts.Timeout, opts.VshardRouter, + opts.Fields, opts.StopOnError, opts.RollbackOnError} + names := [optsCnt]string{timeoutOptName, vshardRouterOptName, + fieldsOptName, stopOnErrorOptName, rollbackOnErrorOptName} + values := [optsCnt]interface{}{} + + return encodeOptions(enc, options[:], names[:], values[:]) +} + +// OperationObjectManyOpts describes options for CRUD operations +// with many objects. +type OperationObjectManyOpts struct { + // Timeout is a `vshard.call` timeout and vshard + // master discovery timeout (in seconds). + Timeout OptUint + // VshardRouter is cartridge vshard group name or + // vshard router instance. + VshardRouter OptString + // Fields is field names for getting only a subset of fields. + Fields OptTuple + // StopOnError is a parameter to stop on a first error and report + // error regarding the failed operation and error about what tuples + // were not performed. + StopOnError OptBool + // RollbackOnError is a parameter because of what any failed operation + // will lead to rollback on a storage, where the operation is failed. + RollbackOnError OptBool + // SkipNullabilityCheckOnFlatten is a parameter to allow + // setting null values to non-nullable fields. + SkipNullabilityCheckOnFlatten OptBool +} + +// EncodeMsgpack provides custom msgpack encoder. +func (opts OperationObjectManyOpts) EncodeMsgpack(enc *encoder) error { + const optsCnt = 6 + + options := [optsCnt]option{opts.Timeout, opts.VshardRouter, + opts.Fields, opts.StopOnError, opts.RollbackOnError, + opts.SkipNullabilityCheckOnFlatten} + names := [optsCnt]string{timeoutOptName, vshardRouterOptName, + fieldsOptName, stopOnErrorOptName, rollbackOnErrorOptName, + skipNullabilityCheckOnFlattenOptName} + values := [optsCnt]interface{}{} + + return encodeOptions(enc, options[:], names[:], values[:]) +} + +// BorderOpts describes options for `crud.min` and `crud.max`. +type BorderOpts struct { + // Timeout is a `vshard.call` timeout and vshard + // master discovery timeout (in seconds). + Timeout OptUint + // VshardRouter is cartridge vshard group name or + // vshard router instance. + VshardRouter OptString + // Fields is field names for getting only a subset of fields. + Fields OptTuple +} + +// EncodeMsgpack provides custom msgpack encoder. +func (opts BorderOpts) EncodeMsgpack(enc *encoder) error { + const optsCnt = 3 + + options := [optsCnt]option{opts.Timeout, opts.VshardRouter, opts.Fields} + names := [optsCnt]string{timeoutOptName, vshardRouterOptName, fieldsOptName} + values := [optsCnt]interface{}{} + + return encodeOptions(enc, options[:], names[:], values[:]) +} + +func encodeOptions(enc *encoder, options []option, names []string, values []interface{}) error { + mapLen := 0 + + for i, opt := range options { + if value, err := opt.getInterface(); err == nil { + values[i] = value + mapLen += 1 + } + } + + if err := enc.EncodeMapLen(mapLen); err != nil { + return err + } + + if mapLen > 0 { + for i, name := range names { + if values[i] != nil { + enc.EncodeString(name) + enc.Encode(values[i]) + } + } + } + + return nil +} diff --git a/crud/replace.go b/crud/replace.go new file mode 100644 index 000000000..811a08eb8 --- /dev/null +++ b/crud/replace.go @@ -0,0 +1,125 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// ReplaceResult describes result for `crud.replace` method. +type ReplaceResult = Result + +// ReplaceOpts describes options for `crud.replace` method. +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 +} + +type replaceArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + 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 { + args := replaceArgs{Space: req.space, Tuple: req.tuple, 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 *ReplaceRequest) Context(ctx context.Context) *ReplaceRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +// ReplaceObjectResult describes result for `crud.replace_object` method. +type ReplaceObjectResult = Result + +// ReplaceObjectOpts describes options for `crud.replace_object` method. +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 +} + +type replaceObjectArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + 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 = MapObject{} + 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 { + args := replaceObjectArgs{Space: req.space, Object: req.object, 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 *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..36350ac4b --- /dev/null +++ b/crud/replace_many.go @@ -0,0 +1,125 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// ReplaceManyResult describes result for `crud.replace_many` method. +type ReplaceManyResult = ResultMany + +// ReplaceManyOpts describes options for `crud.replace_many` method. +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 +} + +type replaceManyArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + 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 { + args := replaceManyArgs{Space: req.space, Tuples: req.tuples, 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 *ReplaceManyRequest) Context(ctx context.Context) *ReplaceManyRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +// ReplaceObjectManyResult describes result for `crud.replace_object_many` method. +type ReplaceObjectManyResult = ResultMany + +// ReplaceObjectManyOpts describes options for `crud.replace_object_many` method. +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 +} + +type replaceObjectManyArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + 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 { + args := replaceObjectManyArgs{Space: req.space, Objects: req.objects, 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 *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..c09f18a6c --- /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 = crud.MapObject{ + "id": uint(24), +} + +var reqObjects = []crud.Object{ + crud.MapObject{ + "id": uint(24), + }, + crud.MapObject{ + "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, + nil, 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, + nil, 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/result.go b/crud/result.go new file mode 100644 index 000000000..356e2a817 --- /dev/null +++ b/crud/result.go @@ -0,0 +1,285 @@ +package crud + +import ( + "fmt" +) + +// FieldFormat contains field definition: {name='...',type='...'[,is_nullable=...]}. +type FieldFormat struct { + Name string + Type string + IsNullable bool +} + +// DecodeMsgpack provides custom msgpack decoder. +func (format *FieldFormat) DecodeMsgpack(d *decoder) error { + l, err := d.DecodeMapLen() + if err != nil { + return err + } + for i := 0; i < l; i++ { + key, err := d.DecodeString() + if err != nil { + return err + } + switch key { + case "name": + if format.Name, err = d.DecodeString(); err != nil { + return err + } + case "type": + if format.Type, err = d.DecodeString(); err != nil { + return err + } + case "is_nullable": + if format.IsNullable, err = d.DecodeBool(); err != nil { + return err + } + default: + if err := d.Skip(); err != nil { + return err + } + } + } + + return nil +} + +// Result describes CRUD result as an object containing metadata and rows. +type Result struct { + Metadata []FieldFormat + Rows []interface{} +} + +// DecodeMsgpack provides custom msgpack decoder. +func (r *Result) DecodeMsgpack(d *decoder) error { + arrLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + + if arrLen < 2 { + return fmt.Errorf("array len doesn't match: %d", arrLen) + } + + l, err := d.DecodeMapLen() + if err != nil { + return err + } + + for i := 0; i < l; i++ { + key, err := d.DecodeString() + if err != nil { + return err + } + + switch key { + case "metadata": + metadataLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + + metadata := make([]FieldFormat, metadataLen) + + for i := 0; i < metadataLen; i++ { + fieldFormat := FieldFormat{} + if err = d.Decode(&fieldFormat); err != nil { + return err + } + + metadata[i] = fieldFormat + } + + r.Metadata = metadata + case "rows": + if err = d.Decode(&r.Rows); err != nil { + return err + } + default: + if err := d.Skip(); err != nil { + return err + } + } + } + + var crudErr *Error = nil + + if err := d.Decode(&crudErr); err != nil { + return err + } + + for i := 2; i < arrLen; i++ { + if err := d.Skip(); err != nil { + return err + } + } + + if crudErr != nil { + return crudErr + } + + return nil +} + +// ResultMany describes CRUD result as an object containing metadata and rows. +type ResultMany struct { + Metadata []FieldFormat + Rows []interface{} +} + +// DecodeMsgpack provides custom msgpack decoder. +func (r *ResultMany) DecodeMsgpack(d *decoder) error { + arrLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + + if arrLen < 2 { + return fmt.Errorf("array len doesn't match: %d", arrLen) + } + + l, err := d.DecodeMapLen() + if err != nil { + return err + } + + for i := 0; i < l; i++ { + key, err := d.DecodeString() + if err != nil { + return err + } + + switch key { + case "metadata": + metadataLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + + metadata := make([]FieldFormat, metadataLen) + + for i := 0; i < metadataLen; i++ { + fieldFormat := FieldFormat{} + if err = d.Decode(&fieldFormat); err != nil { + return err + } + + metadata[i] = fieldFormat + } + + r.Metadata = metadata + case "rows": + if err = d.Decode(&r.Rows); err != nil { + return err + } + default: + if err := d.Skip(); err != nil { + return err + } + } + } + + errLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + + var errs []Error + for i := 0; i < errLen; i++ { + var crudErr *Error = nil + + if err := d.Decode(&crudErr); err != nil { + return err + } else if crudErr != nil { + errs = append(errs, *crudErr) + } + } + + for i := 2; i < arrLen; i++ { + if err := d.Skip(); err != nil { + return err + } + } + + if len(errs) > 0 { + return &ErrorMany{Errors: errs} + } + + return nil +} + +// NumberResult describes CRUD result as an object containing number. +type NumberResult struct { + Value uint64 +} + +// DecodeMsgpack provides custom msgpack decoder. +func (r *NumberResult) DecodeMsgpack(d *decoder) error { + arrLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + + if arrLen < 2 { + return fmt.Errorf("array len doesn't match: %d", arrLen) + } + + if r.Value, err = d.DecodeUint64(); err != nil { + return err + } + + var crudErr *Error = nil + + if err := d.Decode(&crudErr); err != nil { + return err + } + + for i := 2; i < arrLen; i++ { + if err := d.Skip(); err != nil { + return err + } + } + + if crudErr != nil { + return crudErr + } + + return nil +} + +// BoolResult describes CRUD result as an object containing bool. +type BoolResult struct { + Value bool +} + +// DecodeMsgpack provides custom msgpack decoder. +func (r *BoolResult) DecodeMsgpack(d *decoder) error { + arrLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + if arrLen < 2 { + if r.Value, err = d.DecodeBool(); err != nil { + return err + } + + return nil + } + + if _, err = d.DecodeInterface(); err != nil { + return err + } + + var crudErr *Error = nil + + if err := d.Decode(&crudErr); err != nil { + return err + } + + if crudErr != nil { + return crudErr + } + + return nil +} diff --git a/crud/select.go b/crud/select.go new file mode 100644 index 000000000..5ada1aa99 --- /dev/null +++ b/crud/select.go @@ -0,0 +1,117 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// SelectResult describes result for `crud.select` method. +type SelectResult = Result + +// SelectOpts describes options for `crud.select` method. +type SelectOpts struct { + // Timeout is a `vshard.call` timeout and vshard + // master discovery timeout (in seconds). + Timeout OptUint + // VshardRouter is cartridge vshard group name or + // vshard router instance. + VshardRouter OptString + // Fields is field names for getting only a subset of fields. + Fields OptTuple + // BucketId is a bucket ID. + BucketId OptUint + // Mode is a parameter with `write`/`read` possible values, + // if `write` is specified then operation is performed on master. + Mode OptString + // PreferReplica is a parameter to specify preferred target + // as one of the replicas. + PreferReplica OptBool + // Balance is a parameter to use replica according to vshard + // load balancing policy. + Balance OptBool + // First describes the maximum count of the objects to return. + First OptInt + // After is a tuple after which objects should be selected. + After OptTuple + // BatchSize is a number of tuples to process per one request to storage. + BatchSize OptUint + // ForceMapCall describes the map call is performed without any + // optimizations even if full primary key equal condition is specified. + ForceMapCall OptBool + // Fullscan describes if a critical log entry will be skipped on + // potentially long select. + Fullscan OptBool +} + +// EncodeMsgpack provides custom msgpack encoder. +func (opts SelectOpts) EncodeMsgpack(enc *encoder) error { + const optsCnt = 12 + + options := [optsCnt]option{opts.Timeout, opts.VshardRouter, + opts.Fields, opts.BucketId, + opts.Mode, opts.PreferReplica, opts.Balance, + opts.First, opts.After, opts.BatchSize, + opts.ForceMapCall, opts.Fullscan} + names := [optsCnt]string{timeoutOptName, vshardRouterOptName, + fieldsOptName, bucketIdOptName, + modeOptName, preferReplicaOptName, balanceOptName, + firstOptName, afterOptName, batchSizeOptName, + forceMapCallOptName, fullscanOptName} + values := [optsCnt]interface{}{} + + return encodeOptions(enc, options[:], names[:], values[:]) +} + +// SelectRequest helps you to create request object to call `crud.select` +// for execution by a Connection. +type SelectRequest struct { + spaceRequest + conditions []Condition + opts SelectOpts +} + +type selectArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + 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 = nil + 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 { + args := selectArgs{Space: req.space, Conditions: req.conditions, 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 *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..a52ca710c --- /dev/null +++ b/crud/storage_info.go @@ -0,0 +1,130 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// StatusTable describes information for instance. +type StatusTable struct { + Status string + IsMaster bool + Message string +} + +// DecodeMsgpack provides custom msgpack decoder. +func (statusTable *StatusTable) DecodeMsgpack(d *decoder) error { + l, err := d.DecodeMapLen() + if err != nil { + return err + } + for i := 0; i < l; i++ { + key, err := d.DecodeString() + if err != nil { + return err + } + + switch key { + case "status": + if statusTable.Status, err = d.DecodeString(); err != nil { + return err + } + case "is_master": + if statusTable.IsMaster, err = d.DecodeBool(); err != nil { + return err + } + case "message": + if statusTable.Message, err = d.DecodeString(); err != nil { + return err + } + default: + if err := d.Skip(); err != nil { + return err + } + } + } + + return nil +} + +// StorageInfoResult describes result for `crud.storage_info` method. +type StorageInfoResult struct { + Info map[string]StatusTable +} + +// DecodeMsgpack provides custom msgpack decoder. +func (r *StorageInfoResult) DecodeMsgpack(d *decoder) error { + _, err := d.DecodeArrayLen() + if err != nil { + return err + } + + l, err := d.DecodeMapLen() + if err != nil { + return err + } + + info := make(map[string]StatusTable) + for i := 0; i < l; i++ { + key, err := d.DecodeString() + if err != nil { + return err + } + + statusTable := StatusTable{} + if err := d.Decode(&statusTable); err != nil { + return nil + } + + info[key] = statusTable + } + + r.Info = info + + return nil +} + +// StorageInfoOpts describes options for `crud.storage_info` method. +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 +} + +type storageInfoArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + 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 { + args := storageInfoArgs{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 *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..914a49b58 --- /dev/null +++ b/crud/tarantool_test.go @@ -0,0 +1,807 @@ +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 invalidSpaceName = "invalid" +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 = crud.MapObject{ + "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 testResultWithErrCases = []struct { + name string + resp interface{} + req tarantool.Request +}{ + { + "BaseResult", + &crud.SelectResult{}, + crud.NewSelectRequest(invalidSpaceName).Opts(selectOpts), + }, + { + "ManyResult", + &crud.ReplaceManyResult{}, + crud.NewReplaceManyRequest(invalidSpaceName).Opts(opManyOpts), + }, + { + "NumberResult", + &crud.CountResult{}, + crud.NewCountRequest(invalidSpaceName).Opts(countOpts), + }, + { + "BoolResult", + &crud.TruncateResult{}, + crud.NewTruncateRequest(invalidSpaceName).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.MapObject{ + "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"]) +} + +func TestResultWithErr(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + for _, testCase := range testResultWithErrCases { + t.Run(testCase.name, func(t *testing.T) { + err := conn.Do(testCase.req).GetTyped(testCase.resp) + if err == nil { + t.Fatalf("Expected CRUD fails with error, but error is not received") + } + require.Contains(t, err.Error(), "Space \"invalid\" doesn't exist") + }) + } +} + +func TestBoolResult(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + req := crud.NewTruncateRequest(spaceName).Opts(baseOpts) + resp := crud.TruncateResult{} + + testCrudRequestPrepareData(t, conn) + + err := conn.Do(req).GetTyped(&resp) + if err != nil { + t.Fatalf("Failed to Do CRUD request: %s", err.Error()) + } + + if resp.Value != true { + t.Fatalf("Unexpected response value: %#v != %#v", resp.Value, true) + } + + for i := 1010; i < 1020; i++ { + conn.Delete(spaceName, nil, []interface{}{uint(i)}) + } +} + +func TestNumberResult(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + req := crud.NewCountRequest(spaceName).Opts(countOpts) + resp := crud.CountResult{} + + testCrudRequestPrepareData(t, conn) + + err := conn.Do(req).GetTyped(&resp) + if err != nil { + t.Fatalf("Failed to Do CRUD request: %s", err.Error()) + } + + if resp.Value != 10 { + t.Fatalf("Unexpected response value: %#v != %#v", resp.Value, 10) + } + + for i := 1010; i < 1020; i++ { + conn.Delete(spaceName, nil, []interface{}{uint(i)}) + } +} + +func TestBaseResult(t *testing.T) { + expectedMetadata := []crud.FieldFormat{ + { + Name: "bucket_id", + Type: "unsigned", + IsNullable: true, + }, + { + Name: "id", + Type: "unsigned", + IsNullable: false, + }, + { + Name: "name", + Type: "string", + IsNullable: false, + }, + } + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + req := crud.NewSelectRequest(spaceName).Opts(selectOpts) + resp := crud.SelectResult{} + + testCrudRequestPrepareData(t, conn) + + err := conn.Do(req).GetTyped(&resp) + if err != nil { + t.Fatalf("Failed to Do CRUD request: %s", err.Error()) + } + + require.ElementsMatch(t, resp.Metadata, expectedMetadata) + + if len(resp.Rows) != 10 { + t.Fatalf("Unexpected rows: %#v", resp.Rows) + } + + for i := 1010; i < 1020; i++ { + conn.Delete(spaceName, nil, []interface{}{uint(i)}) + } +} + +func TestManyResult(t *testing.T) { + expectedMetadata := []crud.FieldFormat{ + { + Name: "bucket_id", + Type: "unsigned", + IsNullable: true, + }, + { + Name: "id", + Type: "unsigned", + IsNullable: false, + }, + { + Name: "name", + Type: "string", + IsNullable: false, + }, + } + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + req := crud.NewReplaceManyRequest(spaceName).Tuples(tuples).Opts(opManyOpts) + resp := crud.ReplaceResult{} + + testCrudRequestPrepareData(t, conn) + + err := conn.Do(req).GetTyped(&resp) + if err != nil { + t.Fatalf("Failed to Do CRUD request: %s", err.Error()) + } + + require.ElementsMatch(t, resp.Metadata, expectedMetadata) + + if len(resp.Rows) != 10 { + t.Fatalf("Unexpected rows: %#v", resp.Rows) + } + + for i := 1010; i < 1020; i++ { + conn.Delete(spaceName, nil, []interface{}{uint(i)}) + } +} + +func TestStorageInfoResult(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + req := crud.NewStorageInfoRequest().Opts(baseOpts) + resp := crud.StorageInfoResult{} + + err := conn.Do(req).GetTyped(&resp) + if err != nil { + t.Fatalf("Failed to Do CRUD request: %s", err.Error()) + } + + if resp.Info == nil { + t.Fatalf("Failed to Do CRUD storage info request") + } + + for _, info := range resp.Info { + if info.Status != "running" { + t.Fatalf("Unexpected Status: %s != running", info.Status) + } + + if info.IsMaster != true { + t.Fatalf("Unexpected IsMaster: %v != true", info.IsMaster) + } + + if msg := info.Message; msg != "" { + t.Fatalf("Unexpected Message: %s", msg) + } + } +} + +// 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..9f8b2d5db --- /dev/null +++ b/crud/testdata/config.lua @@ -0,0 +1,99 @@ +-- configure path so that you can run application +-- from outside the root directory +if package.setsearchroot ~= nil then + package.setsearchroot() +else + -- Workaround for rocks loading in tarantool 1.10 + -- It can be removed in tarantool > 2.2 + -- By default, when you do require('mymodule'), tarantool looks into + -- the current working directory and whatever is specified in + -- package.path and package.cpath. If you run your app while in the + -- root directory of that app, everything goes fine, but if you try to + -- start your app with "tarantool myapp/init.lua", it will fail to load + -- its modules, and modules from myapp/.rocks. + local fio = require('fio') + local app_dir = fio.abspath(fio.dirname(arg[0])) + package.path = app_dir .. '/?.lua;' .. package.path + package.path = app_dir .. '/?/init.lua;' .. package.path + package.path = app_dir .. '/.rocks/share/tarantool/?.lua;' .. package.path + package.path = app_dir .. '/.rocks/share/tarantool/?/init.lua;' .. package.path + package.cpath = app_dir .. '/?.so;' .. package.cpath + package.cpath = app_dir .. '/?.dylib;' .. package.cpath + package.cpath = app_dir .. '/.rocks/lib/tarantool/?.so;' .. package.cpath + package.cpath = app_dir .. '/.rocks/lib/tarantool/?.dylib;' .. package.cpath +end + +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..e2d6b029d --- /dev/null +++ b/crud/truncate.go @@ -0,0 +1,56 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// TruncateResult describes result for `crud.truncate` method. +type TruncateResult = BoolResult + +// TruncateOpts describes options for `crud.truncate` method. +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 +} + +type truncateArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + 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 { + args := truncateArgs{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 *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..2efb65999 --- /dev/null +++ b/crud/unflatten_rows.go @@ -0,0 +1,40 @@ +package crud + +import ( + "fmt" +) + +// UnflattenRows can be used to convert received tuples to objects. +func UnflattenRows(tuples []interface{}, format []interface{}) ([]MapObject, error) { + var ( + ok bool + tuple Tuple + fieldName string + fieldInfo map[interface{}]interface{} + ) + + objects := []MapObject{} + + 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..09df6612d --- /dev/null +++ b/crud/update.go @@ -0,0 +1,77 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// UpdateResult describes result for `crud.update` method. +type UpdateResult = Result + +// UpdateOpts describes options for `crud.update` method. +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 +} + +type updateArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + 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 { + args := updateArgs{Space: req.space, Key: req.key, + Operations: req.operations, 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 *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..07c373b68 --- /dev/null +++ b/crud/upsert.go @@ -0,0 +1,147 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// UpsertResult describes result for `crud.upsert` method. +type UpsertResult = Result + +// UpsertOpts describes options for `crud.upsert` method. +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 +} + +type upsertArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + 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 { + args := upsertArgs{Space: req.space, Tuple: req.tuple, + Operations: req.operations, 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 *UpsertRequest) Context(ctx context.Context) *UpsertRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +// UpsertObjectResult describes result for `crud.upsert_object` method. +type UpsertObjectResult = Result + +// UpsertObjectOpts describes options for `crud.upsert_object` method. +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 +} + +type upsertObjectArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + 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 = MapObject{} + 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 { + args := upsertObjectArgs{Space: req.space, Object: req.object, + Operations: req.operations, 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 *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..57b8e3bea --- /dev/null +++ b/crud/upsert_many.go @@ -0,0 +1,130 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// UpsertManyResult describes result for `crud.upsert_many` method. +type UpsertManyResult = ResultMany + +// UpsertManyOpts describes options for `crud.upsert_many` method. +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 +} + +type upsertManyArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + 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 { + args := upsertManyArgs{Space: req.space, TuplesOperationData: req.tuplesOperationData, + 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 *UpsertManyRequest) Context(ctx context.Context) *UpsertManyRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +// UpsertObjectManyResult describes result for `crud.upsert_object_many` method. +type UpsertObjectManyResult = ResultMany + +// UpsertObjectManyOpts describes options for `crud.upsert_object_many` method. +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 +} + +type upsertObjectManyArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + 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 { + args := upsertObjectManyArgs{Space: req.space, ObjectsOperationData: req.objectsOperationData, + 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 *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 +}