├── .codespellrc ├── .github ├── pull_request_template.md └── workflows │ ├── check.yaml │ ├── reusable_testing.yml │ └── testing.yml ├── .gitignore ├── .golangci.yaml ├── .luacheckrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MIGRATION.md ├── Makefile ├── README.md ├── arrow ├── arrow.go ├── arrow_test.go ├── example_test.go ├── request.go ├── request_test.go ├── tarantool_test.go └── testdata │ ├── config-memcs.lua │ └── config-memtx.lua ├── auth.go ├── auth_test.go ├── box ├── box.go ├── box_test.go ├── example_test.go ├── info.go ├── info_test.go ├── request.go ├── tarantool_test.go └── testdata │ └── config.lua ├── box_error.go ├── box_error_test.go ├── client_tools.go ├── client_tools_test.go ├── config.lua ├── connection.go ├── connector.go ├── const.go ├── crud ├── common.go ├── conditions.go ├── count.go ├── delete.go ├── error.go ├── error_test.go ├── example_test.go ├── get.go ├── insert.go ├── insert_many.go ├── len.go ├── max.go ├── min.go ├── object.go ├── operations.go ├── operations_test.go ├── options.go ├── replace.go ├── replace_many.go ├── request_test.go ├── result.go ├── result_test.go ├── schema.go ├── select.go ├── stats.go ├── storage_info.go ├── tarantool_test.go ├── testdata │ └── config.lua ├── truncate.go ├── tuple.go ├── unflatten_rows.go ├── update.go ├── upsert.go └── upsert_many.go ├── datetime ├── adjust.go ├── config.lua ├── datetime.go ├── datetime_test.go ├── example_test.go ├── export_test.go ├── gen-timezones.sh ├── interval.go ├── interval_test.go └── timezones.go ├── deadline_io.go ├── decimal ├── bcd.go ├── config.lua ├── decimal.go ├── decimal_test.go ├── example_test.go ├── export_test.go └── fuzzing_test.go ├── decoder.go ├── dial.go ├── dial_test.go ├── errors.go ├── example_custom_unpacking_test.go ├── example_test.go ├── export_test.go ├── future.go ├── future_test.go ├── go.mod ├── go.sum ├── header.go ├── iterator.go ├── pool ├── config.lua ├── connection_pool.go ├── connection_pool_test.go ├── connector.go ├── connector_test.go ├── const.go ├── const_test.go ├── example_test.go ├── pooler.go ├── role_string.go ├── round_robin.go ├── round_robin_test.go ├── state.go └── watcher.go ├── prepared.go ├── protocol.go ├── protocol_test.go ├── queue ├── const.go ├── example_connection_pool_test.go ├── example_msgpack_test.go ├── example_test.go ├── queue.go ├── queue_test.go ├── task.go └── testdata │ ├── config.lua │ └── pool.lua ├── request.go ├── request_test.go ├── response.go ├── response_it.go ├── response_test.go ├── schema.go ├── schema_test.go ├── settings ├── const.go ├── example_test.go ├── request.go ├── request_test.go ├── tarantool_test.go └── testdata │ └── config.lua ├── shutdown_test.go ├── smallbuf.go ├── stream.go ├── tarantool_test.go ├── test_helpers ├── doer.go ├── example_test.go ├── main.go ├── pool_helper.go ├── request.go ├── response.go ├── tcs │ ├── prepare.go │ ├── tcs.go │ └── testdata │ │ └── config.yaml └── utils.go ├── testdata └── sidecar │ └── main.go ├── uuid ├── config.lua ├── example_test.go ├── uuid.go └── uuid_test.go └── watch.go /.codespellrc: -------------------------------------------------------------------------------- 1 | [codespell] 2 | skip = */testdata,./LICENSE,./datetime/timezones.go 3 | ignore-words-list = ro,gost,warmup 4 | count = 5 | quiet-level = 3 6 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | What has been done? Why? What problem is being solved? 2 | 3 | I didn't forget about (remove if it is not applicable): 4 | 5 | - [ ] Tests (see [documentation](https://pkg.go.dev/testing) for a testing package) 6 | - [ ] Changelog (see [documentation](https://keepachangelog.com/en/1.0.0/) for changelog format) 7 | - [ ] Documentation (see [documentation](https://go.dev/blog/godoc) for documentation style guide) 8 | 9 | Related issues: 10 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | name: Run checks 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | luacheck: 9 | runs-on: ubuntu-22.04 10 | if: | 11 | github.event_name == 'push' || 12 | github.event_name == 'pull_request' && 13 | github.event.pull_request.head.repo.full_name != github.repository 14 | steps: 15 | - uses: actions/checkout@master 16 | 17 | - name: Setup Tarantool 18 | uses: tarantool/setup-tarantool@v2 19 | with: 20 | tarantool-version: '2.8' 21 | 22 | - name: Setup tt 23 | run: | 24 | curl -L https://tarantool.io/release/2/installer.sh | sudo bash 25 | sudo apt install -y tt 26 | tt version 27 | 28 | - name: Setup luacheck 29 | run: tt rocks install luacheck 0.25.0 30 | 31 | - name: Run luacheck 32 | run: ./.rocks/bin/luacheck . 33 | 34 | golangci-lint: 35 | runs-on: ubuntu-22.04 36 | if: | 37 | github.event_name == 'push' || 38 | github.event_name == 'pull_request' && 39 | github.event.pull_request.head.repo.full_name != github.repository 40 | steps: 41 | - uses: actions/setup-go@v2 42 | 43 | - uses: actions/checkout@v2 44 | 45 | - name: golangci-lint 46 | uses: golangci/golangci-lint-action@v3 47 | continue-on-error: true 48 | with: 49 | # The first run is for GitHub Actions error format. 50 | args: --config=.golangci.yaml 51 | 52 | - name: golangci-lint 53 | uses: golangci/golangci-lint-action@v3 54 | with: 55 | # The second run is for human-readable error format with a file name 56 | # and a line number. 57 | args: --out-${NO_FUTURE}format colored-line-number --config=.golangci.yaml 58 | 59 | codespell: 60 | runs-on: ubuntu-22.04 61 | if: | 62 | github.event_name == 'push' || 63 | github.event_name == 'pull_request' && 64 | github.event.pull_request.head.repo.full_name != github.repository 65 | steps: 66 | - uses: actions/checkout@master 67 | 68 | - name: Install codespell 69 | run: pip3 install codespell 70 | 71 | - name: Run codespell 72 | run: make codespell 73 | -------------------------------------------------------------------------------- /.github/workflows/reusable_testing.yml: -------------------------------------------------------------------------------- 1 | name: reusable_testing 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | artifact_name: 7 | description: The name of the tarantool build artifact 8 | default: ubuntu-focal 9 | required: false 10 | type: string 11 | 12 | jobs: 13 | run_tests: 14 | runs-on: ubuntu-22.04 15 | steps: 16 | - name: Clone the go-tarantool connector 17 | uses: actions/checkout@v4 18 | with: 19 | repository: ${{ github.repository_owner }}/go-tarantool 20 | 21 | - name: Download the tarantool build artifact 22 | uses: actions/download-artifact@v4 23 | with: 24 | name: ${{ inputs.artifact_name }} 25 | 26 | - name: Install tarantool 27 | # Now we're lucky: all dependencies are already installed. Check package 28 | # dependencies when migrating to other OS version. 29 | run: sudo dpkg -i tarantool*.deb 30 | 31 | - name: Get the tarantool version 32 | run: | 33 | TNT_VERSION=$(tarantool --version | grep -e '^Tarantool') 34 | echo "TNT_VERSION=$TNT_VERSION" >> $GITHUB_ENV 35 | 36 | - name: Setup golang for connector and tests 37 | uses: actions/setup-go@v5 38 | with: 39 | go-version: '1.20' 40 | 41 | - name: Setup tt 42 | run: | 43 | curl -L https://tarantool.io/release/2/installer.sh | sudo bash 44 | sudo apt install -y tt 45 | tt version 46 | 47 | - name: Install test dependencies 48 | run: make deps 49 | 50 | - name: Run tests 51 | run: make test 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *.swp 3 | .idea/ 4 | work_dir* 5 | .rocks 6 | bench* 7 | testdata/sidecar/main 8 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 3m 3 | 4 | linters: 5 | disable: 6 | - errcheck 7 | enable: 8 | - forbidigo 9 | - gocritic 10 | - goimports 11 | - lll 12 | - reassign 13 | - stylecheck 14 | - unconvert 15 | 16 | linters-settings: 17 | gocritic: 18 | disabled-checks: 19 | - ifElseChain 20 | lll: 21 | line-length: 100 22 | tab-width: 4 23 | stylecheck: 24 | checks: ["all", "-ST1003"] 25 | 26 | issues: 27 | exclude-rules: 28 | - linters: 29 | - lll 30 | source: "^\\s*//\\s*(\\S+\\s){0,3}https?://\\S+$" 31 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | redefined = false 2 | 3 | globals = { 4 | 'box', 5 | 'utf8', 6 | 'checkers', 7 | '_TARANTOOL' 8 | } 9 | 10 | include_files = { 11 | '**/*.lua', 12 | '*.luacheckrc', 13 | '*.rockspec' 14 | } 15 | 16 | exclude_files = { 17 | '**/*.rocks/' 18 | } 19 | 20 | max_line_length = 120 21 | 22 | ignore = { 23 | "212/self", -- Unused argument . 24 | "411", -- Redefining a local variable. 25 | "421", -- Shadowing a local variable. 26 | "431", -- Shadowing an upvalue. 27 | "432", -- Shadowing an upvalue argument. 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2014-2022, Tarantool AUTHORS 4 | Copyright (c) 2014-2017, Dmitry Smal 5 | Copyright (c) 2014-2017, Yura Sokolov aka funny_falcon 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /arrow/arrow.go: -------------------------------------------------------------------------------- 1 | package arrow 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/vmihailenco/msgpack/v5" 8 | ) 9 | 10 | // Arrow MessagePack extension type. 11 | const arrowExtId = 8 12 | 13 | // Arrow struct wraps a raw arrow data buffer. 14 | type Arrow struct { 15 | data []byte 16 | } 17 | 18 | // MakeArrow returns a new arrow.Arrow object that contains 19 | // wrapped a raw arrow data buffer. 20 | func MakeArrow(arrow []byte) (Arrow, error) { 21 | return Arrow{arrow}, nil 22 | } 23 | 24 | // Raw returns a []byte that contains Arrow raw data. 25 | func (a Arrow) Raw() []byte { 26 | return a.data 27 | } 28 | 29 | func arrowDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { 30 | arrow := Arrow{ 31 | data: make([]byte, extLen), 32 | } 33 | n, err := d.Buffered().Read(arrow.data) 34 | if err != nil { 35 | return fmt.Errorf("arrowDecoder: can't read bytes on Arrow decode: %w", err) 36 | } 37 | if n < extLen || n != len(arrow.data) { 38 | return fmt.Errorf("arrowDecoder: unexpected end of stream after %d Arrow bytes", n) 39 | } 40 | 41 | v.Set(reflect.ValueOf(arrow)) 42 | return nil 43 | } 44 | 45 | func arrowEncoder(e *msgpack.Encoder, v reflect.Value) ([]byte, error) { 46 | arr, ok := v.Interface().(Arrow) 47 | if !ok { 48 | return []byte{}, fmt.Errorf("arrowEncoder: not an Arrow type") 49 | } 50 | return arr.data, nil 51 | } 52 | 53 | func init() { 54 | msgpack.RegisterExtDecoder(arrowExtId, Arrow{}, arrowDecoder) 55 | msgpack.RegisterExtEncoder(arrowExtId, Arrow{}, arrowEncoder) 56 | } 57 | -------------------------------------------------------------------------------- /arrow/arrow_test.go: -------------------------------------------------------------------------------- 1 | package arrow_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | "github.com/tarantool/go-tarantool/v2/arrow" 10 | "github.com/vmihailenco/msgpack/v5" 11 | ) 12 | 13 | var longArrow, _ = hex.DecodeString("ffffffff70000000040000009effffff0400010004000000" + 14 | "b6ffffff0c00000004000000000000000100000004000000daffffff140000000202" + 15 | "000004000000f0ffffff4000000001000000610000000600080004000c0010000400" + 16 | "080009000c000c000c0000000400000008000a000c00040006000800ffffffff8800" + 17 | "0000040000008affffff0400030010000000080000000000000000000000acffffff" + 18 | "01000000000000003400000008000000000000000200000000000000000000000000" + 19 | "00000000000000000000000000000800000000000000000000000100000001000000" + 20 | "0000000000000000000000000a00140004000c0010000c0014000400060008000c00" + 21 | "00000000000000000000") 22 | 23 | var tests = []struct { 24 | name string 25 | arr []byte 26 | enc []byte 27 | }{ 28 | { 29 | "abc", 30 | []byte{'a', 'b', 'c'}, 31 | []byte{0xc7, 0x3, 0x8, 'a', 'b', 'c'}, 32 | }, 33 | { 34 | "empty", 35 | []byte{}, 36 | []byte{0xc7, 0x0, 0x8}, 37 | }, 38 | { 39 | "one", 40 | []byte{1}, 41 | []byte{0xd4, 0x8, 0x1}, 42 | }, 43 | { 44 | "long", 45 | longArrow, 46 | []byte{ 47 | 0xc8, 0x1, 0x10, 0x8, 0xff, 0xff, 0xff, 0xff, 0x70, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 48 | 0x0, 0x9e, 0xff, 0xff, 0xff, 0x4, 0x0, 0x1, 0x0, 0x4, 0x0, 0x0, 0x0, 0xb6, 0xff, 0xff, 49 | 0xff, 0xc, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 50 | 0x4, 0x0, 0x0, 0x0, 0xda, 0xff, 0xff, 0xff, 0x14, 0x0, 0x0, 0x0, 0x2, 0x2, 0x0, 0x0, 51 | 0x4, 0x0, 0x0, 0x0, 0xf0, 0xff, 0xff, 0xff, 0x40, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 52 | 0x61, 0x0, 0x0, 0x0, 0x6, 0x0, 0x8, 0x0, 0x4, 0x0, 0xc, 0x0, 0x10, 0x0, 0x4, 0x0, 0x8, 53 | 0x0, 0x9, 0x0, 0xc, 0x0, 0xc, 0x0, 0xc, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x8, 0x0, 54 | 0xa, 0x0, 0xc, 0x0, 0x4, 0x0, 0x6, 0x0, 0x8, 0x0, 0xff, 0xff, 0xff, 0xff, 0x88, 0x0, 55 | 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x8a, 0xff, 0xff, 0xff, 0x4, 0x0, 0x3, 0x0, 0x10, 0x0, 56 | 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xac, 0xff, 0xff, 57 | 0xff, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x34, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 58 | 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 59 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 60 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 61 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x0, 0x14, 0x0, 62 | 0x4, 0x0, 0xc, 0x0, 0x10, 0x0, 0xc, 0x0, 0x14, 0x0, 0x4, 0x0, 0x6, 0x0, 0x8, 0x0, 0xc, 63 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 64 | }, 65 | }, 66 | } 67 | 68 | func TestEncodeArrow(t *testing.T) { 69 | for _, tt := range tests { 70 | t.Run(tt.name, func(t *testing.T) { 71 | buf := bytes.NewBuffer([]byte{}) 72 | enc := msgpack.NewEncoder(buf) 73 | 74 | arr, err := arrow.MakeArrow(tt.arr) 75 | require.NoError(t, err) 76 | 77 | err = enc.Encode(arr) 78 | require.NoError(t, err) 79 | 80 | require.Equal(t, tt.enc, buf.Bytes()) 81 | }) 82 | 83 | } 84 | } 85 | 86 | func TestDecodeArrow(t *testing.T) { 87 | for _, tt := range tests { 88 | t.Run(tt.name, func(t *testing.T) { 89 | 90 | buf := bytes.NewBuffer(tt.enc) 91 | dec := msgpack.NewDecoder(buf) 92 | 93 | var arr arrow.Arrow 94 | err := dec.Decode(&arr) 95 | require.NoError(t, err) 96 | 97 | require.Equal(t, tt.arr, arr.Raw()) 98 | }) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /arrow/example_test.go: -------------------------------------------------------------------------------- 1 | // Run Tarantool Enterprise Edition instance before example execution: 2 | // 3 | // Terminal 1: 4 | // $ cd arrow 5 | // $ TEST_TNT_WORK_DIR=$(mktemp -d -t 'tarantool.XXX') tarantool testdata/config-memcs.lua 6 | // 7 | // Terminal 2: 8 | // $ go test -v example_test.go 9 | package arrow_test 10 | 11 | import ( 12 | "context" 13 | "encoding/hex" 14 | "fmt" 15 | "log" 16 | "time" 17 | 18 | "github.com/tarantool/go-tarantool/v2" 19 | "github.com/tarantool/go-tarantool/v2/arrow" 20 | ) 21 | 22 | var arrowBinData, _ = hex.DecodeString("ffffffff70000000040000009effffff0400010004000000" + 23 | "b6ffffff0c00000004000000000000000100000004000000daffffff140000000202" + 24 | "000004000000f0ffffff4000000001000000610000000600080004000c0010000400" + 25 | "080009000c000c000c0000000400000008000a000c00040006000800ffffffff8800" + 26 | "0000040000008affffff0400030010000000080000000000000000000000acffffff" + 27 | "01000000000000003400000008000000000000000200000000000000000000000000" + 28 | "00000000000000000000000000000800000000000000000000000100000001000000" + 29 | "0000000000000000000000000a00140004000c0010000c0014000400060008000c00" + 30 | "00000000000000000000") 31 | 32 | func Example() { 33 | dialer := tarantool.NetDialer{ 34 | Address: "127.0.0.1:3013", 35 | User: "test", 36 | Password: "test", 37 | } 38 | ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) 39 | client, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) 40 | cancel() 41 | if err != nil { 42 | log.Fatalf("Failed to connect: %s", err) 43 | } 44 | 45 | arr, err := arrow.MakeArrow(arrowBinData) 46 | if err != nil { 47 | log.Fatalf("Failed prepare Arrow data: %s", err) 48 | } 49 | 50 | req := arrow.NewInsertRequest("testArrow", arr) 51 | 52 | resp, err := client.Do(req).Get() 53 | if err != nil { 54 | log.Fatalf("Failed insert Arrow: %s", err) 55 | } 56 | if len(resp) > 0 { 57 | log.Fatalf("Unexpected response") 58 | } else { 59 | fmt.Printf("Batch arrow inserted") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /arrow/request.go: -------------------------------------------------------------------------------- 1 | package arrow 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/tarantool/go-iproto" 8 | "github.com/tarantool/go-tarantool/v2" 9 | "github.com/vmihailenco/msgpack/v5" 10 | ) 11 | 12 | // InsertRequest helps you to create an insert request object for execution 13 | // by a Connection. 14 | type InsertRequest struct { 15 | arrow Arrow 16 | space interface{} 17 | ctx context.Context 18 | } 19 | 20 | // NewInsertRequest returns a new InsertRequest. 21 | func NewInsertRequest(space interface{}, arrow Arrow) *InsertRequest { 22 | return &InsertRequest{ 23 | space: space, 24 | arrow: arrow, 25 | } 26 | } 27 | 28 | // Type returns a IPROTO_INSERT_ARROW type for the request. 29 | func (r *InsertRequest) Type() iproto.Type { 30 | return iproto.IPROTO_INSERT_ARROW 31 | } 32 | 33 | // Async returns false to the request return a response. 34 | func (r *InsertRequest) Async() bool { 35 | return false 36 | } 37 | 38 | // Ctx returns a context of the request. 39 | func (r *InsertRequest) Ctx() context.Context { 40 | return r.ctx 41 | } 42 | 43 | // Context sets a passed context to the request. 44 | // 45 | // Pay attention that when using context with request objects, 46 | // the timeout option for Connection does not affect the lifetime 47 | // of the request. For those purposes use context.WithTimeout() as 48 | // the root context. 49 | func (r *InsertRequest) Context(ctx context.Context) *InsertRequest { 50 | r.ctx = ctx 51 | return r 52 | } 53 | 54 | // Arrow sets the arrow for insertion the insert arrow request. 55 | // Note: default value is nil. 56 | func (r *InsertRequest) Arrow(arrow Arrow) *InsertRequest { 57 | r.arrow = arrow 58 | return r 59 | } 60 | 61 | // Body fills an msgpack.Encoder with the insert arrow request body. 62 | func (r *InsertRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 63 | if err := enc.EncodeMapLen(2); err != nil { 64 | return err 65 | } 66 | if err := tarantool.EncodeSpace(res, enc, r.space); err != nil { 67 | return err 68 | } 69 | if err := enc.EncodeUint(uint64(iproto.IPROTO_ARROW)); err != nil { 70 | return err 71 | } 72 | return enc.Encode(r.arrow) 73 | } 74 | 75 | // Response creates a response for the InsertRequest. 76 | func (r *InsertRequest) Response( 77 | header tarantool.Header, 78 | body io.Reader, 79 | ) (tarantool.Response, error) { 80 | return tarantool.DecodeBaseResponse(header, body) 81 | } 82 | -------------------------------------------------------------------------------- /arrow/request_test.go: -------------------------------------------------------------------------------- 1 | package arrow_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | "github.com/tarantool/go-iproto" 11 | "github.com/tarantool/go-tarantool/v2" 12 | "github.com/tarantool/go-tarantool/v2/arrow" 13 | "github.com/vmihailenco/msgpack/v5" 14 | ) 15 | 16 | const validSpace uint32 = 1 // Any valid value != default. 17 | 18 | func TestInsertRequestType(t *testing.T) { 19 | request := arrow.NewInsertRequest(validSpace, arrow.Arrow{}) 20 | require.Equal(t, iproto.IPROTO_INSERT_ARROW, request.Type()) 21 | } 22 | 23 | func TestInsertRequestAsync(t *testing.T) { 24 | request := arrow.NewInsertRequest(validSpace, arrow.Arrow{}) 25 | require.Equal(t, false, request.Async()) 26 | } 27 | 28 | func TestInsertRequestCtx_default(t *testing.T) { 29 | request := arrow.NewInsertRequest(validSpace, arrow.Arrow{}) 30 | require.Equal(t, nil, request.Ctx()) 31 | } 32 | 33 | func TestInsertRequestCtx_setter(t *testing.T) { 34 | ctx := context.Background() 35 | request := arrow.NewInsertRequest(validSpace, arrow.Arrow{}).Context(ctx) 36 | require.Equal(t, ctx, request.Ctx()) 37 | } 38 | 39 | func TestResponseDecode(t *testing.T) { 40 | header := tarantool.Header{} 41 | buf := bytes.NewBuffer([]byte{}) 42 | enc := msgpack.NewEncoder(buf) 43 | 44 | enc.EncodeMapLen(1) 45 | enc.EncodeUint8(uint8(iproto.IPROTO_DATA)) 46 | enc.Encode([]interface{}{'v', '2'}) 47 | 48 | request := arrow.NewInsertRequest(validSpace, arrow.Arrow{}) 49 | resp, err := request.Response(header, bytes.NewBuffer(buf.Bytes())) 50 | require.NoError(t, err) 51 | require.Equal(t, header, resp.Header()) 52 | 53 | decodedInterface, err := resp.Decode() 54 | require.NoError(t, err) 55 | require.Equal(t, []interface{}{'v', '2'}, decodedInterface) 56 | } 57 | 58 | func TestResponseDecodeTyped(t *testing.T) { 59 | header := tarantool.Header{} 60 | buf := bytes.NewBuffer([]byte{}) 61 | enc := msgpack.NewEncoder(buf) 62 | 63 | enc.EncodeMapLen(1) 64 | enc.EncodeUint8(uint8(iproto.IPROTO_DATA)) 65 | enc.EncodeBytes([]byte{'v', '2'}) 66 | 67 | request := arrow.NewInsertRequest(validSpace, arrow.Arrow{}) 68 | resp, err := request.Response(header, bytes.NewBuffer(buf.Bytes())) 69 | require.NoError(t, err) 70 | require.Equal(t, header, resp.Header()) 71 | 72 | var decoded []byte 73 | err = resp.DecodeTyped(&decoded) 74 | require.NoError(t, err) 75 | require.Equal(t, []byte{'v', '2'}, decoded) 76 | } 77 | 78 | type stubSchemeResolver struct { 79 | space interface{} 80 | } 81 | 82 | func (r stubSchemeResolver) ResolveSpace(s interface{}) (uint32, error) { 83 | if id, ok := r.space.(uint32); ok { 84 | return id, nil 85 | } 86 | if _, ok := r.space.(string); ok { 87 | return 0, nil 88 | } 89 | return 0, fmt.Errorf("stub error message: %v", r.space) 90 | } 91 | 92 | func (stubSchemeResolver) ResolveIndex(i interface{}, spaceNo uint32) (uint32, error) { 93 | return 0, nil 94 | } 95 | 96 | func (r stubSchemeResolver) NamesUseSupported() bool { 97 | _, ok := r.space.(string) 98 | return ok 99 | } 100 | 101 | func TestInsertRequestDefaultValues(t *testing.T) { 102 | buf := bytes.NewBuffer([]byte{}) 103 | enc := msgpack.NewEncoder(buf) 104 | 105 | resolver := stubSchemeResolver{validSpace} 106 | req := arrow.NewInsertRequest(resolver.space, arrow.Arrow{}) 107 | err := req.Body(&resolver, enc) 108 | require.NoError(t, err) 109 | 110 | require.Equal(t, []byte{0x82, 0x10, 0x1, 0x36, 0xc7, 0x0, 0x8}, buf.Bytes()) 111 | } 112 | 113 | func TestInsertRequestSpaceByName(t *testing.T) { 114 | buf := bytes.NewBuffer([]byte{}) 115 | enc := msgpack.NewEncoder(buf) 116 | 117 | resolver := stubSchemeResolver{"valid"} 118 | req := arrow.NewInsertRequest(resolver.space, arrow.Arrow{}) 119 | err := req.Body(&resolver, enc) 120 | require.NoError(t, err) 121 | 122 | require.Equal(t, 123 | []byte{0x82, 0x5e, 0xa5, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x36, 0xc7, 0x0, 0x8}, 124 | buf.Bytes()) 125 | } 126 | 127 | func TestInsertRequestSetters(t *testing.T) { 128 | buf := bytes.NewBuffer([]byte{}) 129 | enc := msgpack.NewEncoder(buf) 130 | 131 | arr, err := arrow.MakeArrow([]byte{'a', 'b', 'c'}) 132 | require.NoError(t, err) 133 | 134 | resolver := stubSchemeResolver{validSpace} 135 | req := arrow.NewInsertRequest(resolver.space, arr) 136 | err = req.Body(&resolver, enc) 137 | require.NoError(t, err) 138 | 139 | require.Equal(t, []byte{0x82, 0x10, 0x1, 0x36, 0xc7, 0x3, 0x8, 'a', 'b', 'c'}, buf.Bytes()) 140 | } 141 | -------------------------------------------------------------------------------- /arrow/tarantool_test.go: -------------------------------------------------------------------------------- 1 | package arrow_test 2 | 3 | import ( 4 | "encoding/hex" 5 | "log" 6 | "os" 7 | "strconv" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/require" 12 | "github.com/tarantool/go-iproto" 13 | 14 | "github.com/tarantool/go-tarantool/v2" 15 | "github.com/tarantool/go-tarantool/v2/arrow" 16 | "github.com/tarantool/go-tarantool/v2/test_helpers" 17 | ) 18 | 19 | var isArrowSupported = false 20 | 21 | var server = "127.0.0.1:3013" 22 | var dialer = tarantool.NetDialer{ 23 | Address: server, 24 | User: "test", 25 | Password: "test", 26 | } 27 | var space = "testArrow" 28 | 29 | var opts = tarantool.Opts{ 30 | Timeout: 5 * time.Second, 31 | } 32 | 33 | // TestInsert uses Arrow sequence from Tarantool's test. 34 | // See: https://github.com/tarantool/tarantool/blob/d628b71bc537a75b69c253f45ec790462cf1a5cd/test/box-luatest/gh_10508_iproto_insert_arrow_test.lua#L56 35 | func TestInsert_invalid(t *testing.T) { 36 | arrows := []struct { 37 | arrow string 38 | expected iproto.Error 39 | }{ 40 | { 41 | "", 42 | iproto.ER_INVALID_MSGPACK, 43 | }, 44 | { 45 | "00", 46 | iproto.ER_INVALID_MSGPACK, 47 | }, 48 | { 49 | "ffffffff70000000040000009effffff0400010004000000" + 50 | "b6ffffff0c00000004000000000000000100000004000000daffffff140000000202" + 51 | "000004000000f0ffffff4000000001000000610000000600080004000c0010000400" + 52 | "080009000c000c000c0000000400000008000a000c00040006000800ffffffff8800" + 53 | "0000040000008affffff0400030010000000080000000000000000000000acffffff" + 54 | "01000000000000003400000008000000000000000200000000000000000000000000" + 55 | "00000000000000000000000000000800000000000000000000000100000001000000" + 56 | "0000000000000000000000000a00140004000c0010000c0014000400060008000c00" + 57 | "00000000000000000000", 58 | iproto.ER_UNSUPPORTED, 59 | }, 60 | } 61 | 62 | conn := test_helpers.ConnectWithValidation(t, dialer, opts) 63 | defer conn.Close() 64 | 65 | for i, a := range arrows { 66 | t.Run(strconv.Itoa(i), func(t *testing.T) { 67 | data, err := hex.DecodeString(a.arrow) 68 | require.NoError(t, err) 69 | 70 | arr, err := arrow.MakeArrow(data) 71 | require.NoError(t, err) 72 | 73 | req := arrow.NewInsertRequest(space, arr) 74 | _, err = conn.Do(req).Get() 75 | ttErr := err.(tarantool.Error) 76 | 77 | require.Equal(t, a.expected, ttErr.Code) 78 | }) 79 | } 80 | 81 | } 82 | 83 | // runTestMain is a body of TestMain function 84 | // (see https://pkg.go.dev/testing#hdr-Main). 85 | // Using defer + os.Exit is not works so TestMain body 86 | // is a separate function, see 87 | // https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls 88 | func runTestMain(m *testing.M) int { 89 | isLess, err := test_helpers.IsTarantoolVersionLess(3, 3, 0) 90 | if err != nil { 91 | log.Fatalf("Failed to extract Tarantool version: %s", err) 92 | } 93 | isArrowSupported = !isLess 94 | 95 | if !isArrowSupported { 96 | log.Println("Skipping insert Arrow tests...") 97 | return 0 98 | } 99 | 100 | instance, err := test_helpers.StartTarantool(test_helpers.StartOpts{ 101 | Dialer: dialer, 102 | InitScript: "testdata/config-memtx.lua", 103 | Listen: server, 104 | WaitStart: 100 * time.Millisecond, 105 | ConnectRetry: 10, 106 | RetryTimeout: 500 * time.Millisecond, 107 | }) 108 | defer test_helpers.StopTarantoolWithCleanup(instance) 109 | 110 | if err != nil { 111 | log.Printf("Failed to prepare test Tarantool: %s", err) 112 | return 1 113 | } 114 | 115 | return m.Run() 116 | } 117 | 118 | func TestMain(m *testing.M) { 119 | code := runTestMain(m) 120 | os.Exit(code) 121 | } 122 | -------------------------------------------------------------------------------- /arrow/testdata/config-memcs.lua: -------------------------------------------------------------------------------- 1 | -- Do not set listen for now so connector won't be 2 | -- able to send requests until everything is configured. 3 | box.cfg { 4 | work_dir = os.getenv("TEST_TNT_WORK_DIR") 5 | } 6 | 7 | box.schema.user.create('test', { 8 | password = 'test', 9 | if_not_exists = true 10 | }) 11 | box.schema.user.grant('test', 'execute', 'universe', nil, { 12 | if_not_exists = true 13 | }) 14 | 15 | local s = box.schema.space.create('testArrow', { 16 | engine = 'memcs', 17 | field_count = 1, 18 | format = {{'a', 'uint64'}}, 19 | if_not_exists = true 20 | }) 21 | s:create_index('primary') 22 | s:truncate() 23 | 24 | box.schema.user.grant('test', 'read,write', 'space', 'testArrow', { 25 | if_not_exists = true 26 | }) 27 | 28 | -- Set listen only when every other thing is configured. 29 | box.cfg { 30 | listen = 3013 31 | } 32 | -------------------------------------------------------------------------------- /arrow/testdata/config-memtx.lua: -------------------------------------------------------------------------------- 1 | -- Do not set listen for now so connector won't be 2 | -- able to send requests until everything is configured. 3 | box.cfg { 4 | work_dir = os.getenv("TEST_TNT_WORK_DIR") 5 | } 6 | 7 | box.schema.user.create('test', { 8 | password = 'test', 9 | if_not_exists = true 10 | }) 11 | box.schema.user.grant('test', 'execute', 'universe', nil, { 12 | if_not_exists = true 13 | }) 14 | 15 | local s = box.schema.space.create('testArrow', { 16 | if_not_exists = true 17 | }) 18 | s:create_index('primary', { 19 | type = 'tree', 20 | parts = {{ 21 | field = 1, 22 | type = 'integer' 23 | }}, 24 | if_not_exists = true 25 | }) 26 | s:truncate() 27 | 28 | box.schema.user.grant('test', 'read,write', 'space', 'testArrow', { 29 | if_not_exists = true 30 | }) 31 | 32 | -- Set listen only when every other thing is configured. 33 | box.cfg { 34 | listen = os.getenv("TEST_TNT_LISTEN") 35 | } 36 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/base64" 6 | "fmt" 7 | ) 8 | 9 | const ( 10 | chapSha1 = "chap-sha1" 11 | papSha256 = "pap-sha256" 12 | ) 13 | 14 | // Auth is used as a parameter to set up an authentication method. 15 | type Auth int 16 | 17 | const ( 18 | // AutoAuth does not force any authentication method. A method will be 19 | // selected automatically (a value from IPROTO_ID response or 20 | // ChapSha1Auth). 21 | AutoAuth Auth = iota 22 | // ChapSha1Auth forces chap-sha1 authentication method. The method is 23 | // available both in the Tarantool Community Edition (CE) and the 24 | // Tarantool Enterprise Edition (EE) 25 | ChapSha1Auth 26 | // PapSha256Auth forces pap-sha256 authentication method. The method is 27 | // available only for the Tarantool Enterprise Edition (EE) with 28 | // SSL transport. 29 | PapSha256Auth 30 | ) 31 | 32 | // String returns a string representation of an authentication method. 33 | func (a Auth) String() string { 34 | switch a { 35 | case AutoAuth: 36 | return "auto" 37 | case ChapSha1Auth: 38 | return chapSha1 39 | case PapSha256Auth: 40 | return papSha256 41 | default: 42 | return fmt.Sprintf("unknown auth type (code %d)", a) 43 | } 44 | } 45 | 46 | func scramble(encodedSalt, pass string) (scramble []byte, err error) { 47 | /* ================================================================== 48 | According to: http://tarantool.org/doc/dev_guide/box-protocol.html 49 | 50 | salt = base64_decode(encodedSalt); 51 | step1 = sha1(password); 52 | step2 = sha1(step1); 53 | step3 = sha1(salt, step2); 54 | scramble = xor(step1, step3); 55 | return scramble; 56 | 57 | ===================================================================== */ 58 | scrambleSize := sha1.Size // == 20 59 | 60 | salt, err := base64.StdEncoding.DecodeString(encodedSalt) 61 | if err != nil { 62 | return 63 | } 64 | step1 := sha1.Sum([]byte(pass)) 65 | step2 := sha1.Sum(step1[0:]) 66 | hash := sha1.New() // May be create it once per connection? 67 | hash.Write(salt[0:scrambleSize]) 68 | hash.Write(step2[0:]) 69 | step3 := hash.Sum(nil) 70 | 71 | return xor(step1[0:], step3[0:], scrambleSize), nil 72 | } 73 | 74 | func xor(left, right []byte, size int) []byte { 75 | result := make([]byte, size) 76 | for i := 0; i < size; i++ { 77 | result[i] = left[i] ^ right[i] 78 | } 79 | return result 80 | } 81 | -------------------------------------------------------------------------------- /auth_test.go: -------------------------------------------------------------------------------- 1 | package tarantool_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | . "github.com/tarantool/go-tarantool/v2" 9 | ) 10 | 11 | func TestAuth_String(t *testing.T) { 12 | unknownId := int(PapSha256Auth) + 1 13 | tests := []struct { 14 | auth Auth 15 | expected string 16 | }{ 17 | {AutoAuth, "auto"}, 18 | {ChapSha1Auth, "chap-sha1"}, 19 | {PapSha256Auth, "pap-sha256"}, 20 | {Auth(unknownId), fmt.Sprintf("unknown auth type (code %d)", unknownId)}, 21 | } 22 | 23 | for _, tc := range tests { 24 | t.Run(tc.expected, func(t *testing.T) { 25 | assert.Equal(t, tc.auth.String(), tc.expected) 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /box/box.go: -------------------------------------------------------------------------------- 1 | package box 2 | 3 | import ( 4 | "github.com/tarantool/go-tarantool/v2" 5 | ) 6 | 7 | // Box is a helper that wraps box.* requests. 8 | // It holds a connection to the Tarantool instance via the Doer interface. 9 | type Box struct { 10 | conn tarantool.Doer // Connection interface for interacting with Tarantool. 11 | } 12 | 13 | // New returns a new instance of the box structure, which implements the Box interface. 14 | func New(conn tarantool.Doer) *Box { 15 | return &Box{ 16 | conn: conn, // Assigns the provided Tarantool connection. 17 | } 18 | } 19 | 20 | // Info retrieves the current information of the Tarantool instance. 21 | // It calls the "box.info" function and parses the result into the Info structure. 22 | func (b *Box) Info() (Info, error) { 23 | var infoResp InfoResponse 24 | 25 | // Call "box.info" to get instance information from Tarantool. 26 | fut := b.conn.Do(NewInfoRequest()) 27 | 28 | // Parse the result into the Info structure. 29 | err := fut.GetTyped(&infoResp) 30 | if err != nil { 31 | return Info{}, err 32 | } 33 | 34 | // Return the parsed info and any potential error. 35 | return infoResp.Info, err 36 | } 37 | -------------------------------------------------------------------------------- /box/box_test.go: -------------------------------------------------------------------------------- 1 | package box_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/tarantool/go-tarantool/v2/box" 8 | ) 9 | 10 | func TestNew(t *testing.T) { 11 | // Create a box instance with a nil connection. This should lead to a panic later. 12 | b := box.New(nil) 13 | 14 | // Ensure the box instance is not nil (which it shouldn't be), but this is not meaningful 15 | // since we will panic when we call the Info method with the nil connection. 16 | require.NotNil(t, b) 17 | 18 | // We expect a panic because we are passing a nil connection (nil Doer) to the By function. 19 | // The library does not control this zone, and the nil connection would cause a runtime error 20 | // when we attempt to call methods (like Info) on it. 21 | // This test ensures that such an invalid state is correctly handled by causing a panic, 22 | // as it's outside the library's responsibility. 23 | require.Panics(t, func() { 24 | 25 | // Calling Info on a box with a nil connection will result in a panic, since the underlying 26 | // connection (Doer) cannot perform the requested action (it's nil). 27 | _, _ = b.Info() 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /box/example_test.go: -------------------------------------------------------------------------------- 1 | // Run Tarantool Common Edition before example execution: 2 | // 3 | // Terminal 1: 4 | // $ cd box 5 | // $ TEST_TNT_LISTEN=127.0.0.1:3013 tarantool testdata/config.lua 6 | // 7 | // Terminal 2: 8 | // $ go test -v example_test.go 9 | package box_test 10 | 11 | import ( 12 | "context" 13 | "fmt" 14 | "log" 15 | "time" 16 | 17 | "github.com/tarantool/go-tarantool/v2" 18 | "github.com/tarantool/go-tarantool/v2/box" 19 | ) 20 | 21 | func Example() { 22 | dialer := tarantool.NetDialer{ 23 | Address: "127.0.0.1:3013", 24 | User: "test", 25 | Password: "test", 26 | } 27 | ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) 28 | client, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) 29 | cancel() 30 | if err != nil { 31 | log.Fatalf("Failed to connect: %s", err) 32 | } 33 | 34 | // You can use Info Request type. 35 | 36 | fut := client.Do(box.NewInfoRequest()) 37 | 38 | resp := &box.InfoResponse{} 39 | 40 | err = fut.GetTyped(resp) 41 | if err != nil { 42 | log.Fatalf("Failed get box info: %s", err) 43 | } 44 | 45 | // Or use simple Box implementation. 46 | 47 | b := box.New(client) 48 | 49 | info, err := b.Info() 50 | if err != nil { 51 | log.Fatalf("Failed get box info: %s", err) 52 | } 53 | 54 | if info.UUID != resp.Info.UUID { 55 | log.Fatalf("Box info uuids are not equal") 56 | } 57 | 58 | fmt.Printf("Box info uuids are equal") 59 | fmt.Printf("Current box info: %+v\n", resp.Info) 60 | } 61 | -------------------------------------------------------------------------------- /box/info_test.go: -------------------------------------------------------------------------------- 1 | package box 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/vmihailenco/msgpack/v5" 8 | ) 9 | 10 | func TestInfo(t *testing.T) { 11 | id := 1 12 | cases := []struct { 13 | Name string 14 | Struct Info 15 | Data map[string]interface{} 16 | }{ 17 | { 18 | Name: "Case: base info struct", 19 | Struct: Info{ 20 | Version: "2.11.4-0-g8cebbf2cad", 21 | ID: &id, 22 | RO: false, 23 | UUID: "69360e9b-4641-4ec3-ab51-297f46749849", 24 | PID: 1, 25 | Status: "running", 26 | LSN: 8, 27 | }, 28 | Data: map[string]interface{}{ 29 | "version": "2.11.4-0-g8cebbf2cad", 30 | "id": 1, 31 | "ro": false, 32 | "uuid": "69360e9b-4641-4ec3-ab51-297f46749849", 33 | "pid": 1, 34 | "status": "running", 35 | "lsn": 8, 36 | }, 37 | }, 38 | { 39 | Name: "Case: info struct with replication", 40 | Struct: Info{ 41 | Version: "2.11.4-0-g8cebbf2cad", 42 | ID: &id, 43 | RO: false, 44 | UUID: "69360e9b-4641-4ec3-ab51-297f46749849", 45 | PID: 1, 46 | Status: "running", 47 | LSN: 8, 48 | Replication: map[int]Replication{ 49 | 1: { 50 | ID: 1, 51 | UUID: "69360e9b-4641-4ec3-ab51-297f46749849", 52 | LSN: 8, 53 | }, 54 | 2: { 55 | ID: 2, 56 | UUID: "75f5f5aa-89f0-4d95-b5a9-96a0eaa0ce36", 57 | LSN: 0, 58 | Upstream: Upstream{ 59 | Status: "follow", 60 | Idle: 2.4564633660484, 61 | Peer: "other.tarantool:3301", 62 | Lag: 0.00011920928955078, 63 | Message: "'getaddrinfo: Name or service not known'", 64 | SystemMessage: "Input/output error", 65 | }, 66 | Downstream: Downstream{ 67 | Status: "follow", 68 | Idle: 2.8306158290943, 69 | VClock: map[int]uint64{1: 8}, 70 | Lag: 0, 71 | Message: "'unexpected EOF when reading from socket'", 72 | SystemMessage: "Broken pipe", 73 | }, 74 | }, 75 | }, 76 | }, 77 | Data: map[string]interface{}{ 78 | "version": "2.11.4-0-g8cebbf2cad", 79 | "id": 1, 80 | "ro": false, 81 | "uuid": "69360e9b-4641-4ec3-ab51-297f46749849", 82 | "pid": 1, 83 | "status": "running", 84 | "lsn": 8, 85 | "replication": map[interface{}]interface{}{ 86 | 1: map[string]interface{}{ 87 | "id": 1, 88 | "uuid": "69360e9b-4641-4ec3-ab51-297f46749849", 89 | "lsn": 8, 90 | }, 91 | 2: map[string]interface{}{ 92 | "id": 2, 93 | "uuid": "75f5f5aa-89f0-4d95-b5a9-96a0eaa0ce36", 94 | "lsn": 0, 95 | "upstream": map[string]interface{}{ 96 | "status": "follow", 97 | "idle": 2.4564633660484, 98 | "peer": "other.tarantool:3301", 99 | "lag": 0.00011920928955078, 100 | "message": "'getaddrinfo: Name or service not known'", 101 | "system_message": "Input/output error", 102 | }, 103 | "downstream": map[string]interface{}{ 104 | "status": "follow", 105 | "idle": 2.8306158290943, 106 | "vclock": map[interface{}]interface{}{1: 8}, 107 | "lag": 0, 108 | "message": "'unexpected EOF when reading from socket'", 109 | "system_message": "Broken pipe", 110 | }, 111 | }, 112 | }, 113 | }, 114 | }, 115 | } 116 | for _, tc := range cases { 117 | data, err := msgpack.Marshal(tc.Data) 118 | require.NoError(t, err, tc.Name) 119 | 120 | var result Info 121 | err = msgpack.Unmarshal(data, &result) 122 | require.NoError(t, err, tc.Name) 123 | 124 | require.Equal(t, tc.Struct, result) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /box/request.go: -------------------------------------------------------------------------------- 1 | package box 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/tarantool/go-iproto" 8 | "github.com/tarantool/go-tarantool/v2" 9 | ) 10 | 11 | type baseRequest struct { 12 | impl *tarantool.CallRequest 13 | } 14 | 15 | func newCall(method string) *tarantool.CallRequest { 16 | return tarantool.NewCallRequest(method) 17 | } 18 | 19 | // Type returns IPROTO type for request. 20 | func (req baseRequest) Type() iproto.Type { 21 | return req.impl.Type() 22 | } 23 | 24 | // Ctx returns a context of request. 25 | func (req baseRequest) Ctx() context.Context { 26 | return req.impl.Ctx() 27 | } 28 | 29 | // Async returns request expects a response. 30 | func (req baseRequest) Async() bool { 31 | return req.impl.Async() 32 | } 33 | 34 | // Response creates a response for the baseRequest. 35 | func (req baseRequest) Response(header tarantool.Header, 36 | body io.Reader) (tarantool.Response, error) { 37 | return req.impl.Response(header, body) 38 | } 39 | -------------------------------------------------------------------------------- /box/tarantool_test.go: -------------------------------------------------------------------------------- 1 | package box_test 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/google/uuid" 11 | "github.com/stretchr/testify/require" 12 | "github.com/tarantool/go-tarantool/v2" 13 | "github.com/tarantool/go-tarantool/v2/box" 14 | "github.com/tarantool/go-tarantool/v2/test_helpers" 15 | ) 16 | 17 | var server = "127.0.0.1:3013" 18 | var dialer = tarantool.NetDialer{ 19 | Address: server, 20 | User: "test", 21 | Password: "test", 22 | } 23 | 24 | func validateInfo(t testing.TB, info box.Info) { 25 | var err error 26 | 27 | // Check all fields run correctly. 28 | _, err = uuid.Parse(info.UUID) 29 | require.NoErrorf(t, err, "validate instance uuid is valid") 30 | 31 | require.NotEmpty(t, info.Version) 32 | // Check that pid parsed correctly. 33 | require.NotEqual(t, info.PID, 0) 34 | 35 | // Check replication is parsed correctly. 36 | require.NotEmpty(t, info.Replication) 37 | 38 | // Check one replica uuid is equal system uuid. 39 | require.Equal(t, info.UUID, info.Replication[1].UUID) 40 | } 41 | 42 | func TestBox_Sugar_Info(t *testing.T) { 43 | ctx := context.TODO() 44 | 45 | conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) 46 | require.NoError(t, err) 47 | 48 | info, err := box.New(conn).Info() 49 | require.NoError(t, err) 50 | 51 | validateInfo(t, info) 52 | } 53 | 54 | func TestBox_Info(t *testing.T) { 55 | ctx := context.TODO() 56 | 57 | conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) 58 | require.NoError(t, err) 59 | 60 | fut := conn.Do(box.NewInfoRequest()) 61 | require.NotNil(t, fut) 62 | 63 | resp := &box.InfoResponse{} 64 | err = fut.GetTyped(resp) 65 | require.NoError(t, err) 66 | 67 | validateInfo(t, resp.Info) 68 | } 69 | 70 | func runTestMain(m *testing.M) int { 71 | instance, err := test_helpers.StartTarantool(test_helpers.StartOpts{ 72 | Dialer: dialer, 73 | InitScript: "testdata/config.lua", 74 | Listen: server, 75 | WaitStart: 100 * time.Millisecond, 76 | ConnectRetry: 10, 77 | RetryTimeout: 500 * time.Millisecond, 78 | }) 79 | defer test_helpers.StopTarantoolWithCleanup(instance) 80 | 81 | if err != nil { 82 | log.Printf("Failed to prepare test Tarantool: %s", err) 83 | return 1 84 | } 85 | 86 | return m.Run() 87 | } 88 | 89 | func TestMain(m *testing.M) { 90 | code := runTestMain(m) 91 | os.Exit(code) 92 | } 93 | -------------------------------------------------------------------------------- /box/testdata/config.lua: -------------------------------------------------------------------------------- 1 | -- Do not set listen for now so connector won't be 2 | -- able to send requests until everything is configured. 3 | box.cfg{ 4 | work_dir = os.getenv("TEST_TNT_WORK_DIR"), 5 | } 6 | 7 | box.schema.user.create('test', { password = 'test' , if_not_exists = true }) 8 | box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true }) 9 | 10 | -- Set listen only when every other thing is configured. 11 | box.cfg{ 12 | listen = os.getenv("TEST_TNT_LISTEN"), 13 | replication = { 14 | os.getenv("TEST_TNT_LISTEN"), 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /client_tools_test.go: -------------------------------------------------------------------------------- 1 | package tarantool_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/vmihailenco/msgpack/v5" 8 | 9 | "github.com/tarantool/go-tarantool/v2" 10 | ) 11 | 12 | func TestOperations_EncodeMsgpack(t *testing.T) { 13 | ops := tarantool.NewOperations(). 14 | Add(1, 2). 15 | Subtract(1, 2). 16 | BitwiseAnd(1, 2). 17 | BitwiseOr(1, 2). 18 | BitwiseXor(1, 2). 19 | Splice(1, 2, 3, "a"). 20 | Insert(1, 2). 21 | Delete(1, 2). 22 | Assign(1, 2) 23 | refOps := []interface{}{ 24 | []interface{}{"+", 1, 2}, 25 | []interface{}{"-", 1, 2}, 26 | []interface{}{"&", 1, 2}, 27 | []interface{}{"|", 1, 2}, 28 | []interface{}{"^", 1, 2}, 29 | []interface{}{":", 1, 2, 3, "a"}, 30 | []interface{}{"!", 1, 2}, 31 | []interface{}{"#", 1, 2}, 32 | []interface{}{"=", 1, 2}, 33 | } 34 | 35 | var refBuf bytes.Buffer 36 | encRef := msgpack.NewEncoder(&refBuf) 37 | if err := encRef.Encode(refOps); err != nil { 38 | t.Errorf("error while encoding: %v", err.Error()) 39 | } 40 | 41 | var buf bytes.Buffer 42 | enc := msgpack.NewEncoder(&buf) 43 | 44 | if err := enc.Encode(ops); err != nil { 45 | t.Errorf("error while encoding: %v", err.Error()) 46 | } 47 | if !bytes.Equal(refBuf.Bytes(), buf.Bytes()) { 48 | t.Errorf("encode response is wrong:\n expected %v\n got: %v", 49 | refBuf, buf.Bytes()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /const.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "github.com/tarantool/go-iproto" 5 | ) 6 | 7 | const ( 8 | packetLengthBytes = 5 9 | ) 10 | 11 | const ( 12 | // ErrorNo indicates that no error has occurred. It could be used to 13 | // check that a response has an error without the response body decoding. 14 | ErrorNo = iproto.ER_UNKNOWN 15 | ) 16 | -------------------------------------------------------------------------------- /crud/common.go: -------------------------------------------------------------------------------- 1 | // Package crud with support of API of Tarantool's CRUD module. 2 | // 3 | // Supported CRUD methods: 4 | // 5 | // - insert 6 | // 7 | // - insert_object 8 | // 9 | // - insert_many 10 | // 11 | // - insert_object_many 12 | // 13 | // - get 14 | // 15 | // - update 16 | // 17 | // - delete 18 | // 19 | // - replace 20 | // 21 | // - replace_object 22 | // 23 | // - replace_many 24 | // 25 | // - replace_object_many 26 | // 27 | // - upsert 28 | // 29 | // - upsert_object 30 | // 31 | // - upsert_many 32 | // 33 | // - upsert_object_many 34 | // 35 | // - select 36 | // 37 | // - min 38 | // 39 | // - max 40 | // 41 | // - truncate 42 | // 43 | // - len 44 | // 45 | // - storage_info 46 | // 47 | // - count 48 | // 49 | // - stats 50 | // 51 | // - unflatten_rows 52 | // 53 | // Since: 1.11.0. 54 | package crud 55 | 56 | import ( 57 | "context" 58 | "io" 59 | 60 | "github.com/tarantool/go-iproto" 61 | 62 | "github.com/tarantool/go-tarantool/v2" 63 | ) 64 | 65 | type baseRequest struct { 66 | impl *tarantool.CallRequest 67 | } 68 | 69 | func newCall(method string) *tarantool.CallRequest { 70 | return tarantool.NewCall17Request(method) 71 | } 72 | 73 | // Type returns IPROTO type for CRUD request. 74 | func (req baseRequest) Type() iproto.Type { 75 | return req.impl.Type() 76 | } 77 | 78 | // Ctx returns a context of CRUD request. 79 | func (req baseRequest) Ctx() context.Context { 80 | return req.impl.Ctx() 81 | } 82 | 83 | // Async returns is CRUD request expects a response. 84 | func (req baseRequest) Async() bool { 85 | return req.impl.Async() 86 | } 87 | 88 | // Response creates a response for the baseRequest. 89 | func (req baseRequest) Response(header tarantool.Header, 90 | body io.Reader) (tarantool.Response, error) { 91 | return req.impl.Response(header, body) 92 | } 93 | 94 | type spaceRequest struct { 95 | baseRequest 96 | space string 97 | } 98 | -------------------------------------------------------------------------------- /crud/conditions.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | // Operator is a type to describe operator of operation. 4 | type Operator string 5 | 6 | const ( 7 | // Eq - comparison operator for "equal". 8 | Eq Operator = "=" 9 | // Lt - comparison operator for "less than". 10 | Lt Operator = "<" 11 | // Le - comparison operator for "less than or equal". 12 | Le Operator = "<=" 13 | // Gt - comparison operator for "greater than". 14 | Gt Operator = ">" 15 | // Ge - comparison operator for "greater than or equal". 16 | Ge Operator = ">=" 17 | ) 18 | 19 | // Condition describes CRUD condition as a table 20 | // {operator, field-identifier, value}. 21 | type Condition struct { 22 | // Instruct msgpack to pack this struct as array, so no custom packer 23 | // is needed. 24 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 25 | Operator Operator 26 | Field string // Field name or index name. 27 | Value interface{} 28 | } 29 | -------------------------------------------------------------------------------- /crud/count.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/vmihailenco/msgpack/v5" 7 | 8 | "github.com/tarantool/go-tarantool/v2" 9 | ) 10 | 11 | // CountResult describes result for `crud.count` method. 12 | type CountResult = NumberResult 13 | 14 | // CountOpts describes options for `crud.count` method. 15 | type CountOpts struct { 16 | // Timeout is a `vshard.call` timeout and vshard 17 | // master discovery timeout (in seconds). 18 | Timeout OptFloat64 19 | // VshardRouter is cartridge vshard group name or 20 | // vshard router instance. 21 | VshardRouter OptString 22 | // Mode is a parameter with `write`/`read` possible values, 23 | // if `write` is specified then operation is performed on master. 24 | Mode OptString 25 | // PreferReplica is a parameter to specify preferred target 26 | // as one of the replicas. 27 | PreferReplica OptBool 28 | // Balance is a parameter to use replica according to vshard 29 | // load balancing policy. 30 | Balance OptBool 31 | // YieldEvery describes number of tuples processed to yield after. 32 | // Should be positive. 33 | YieldEvery OptUint 34 | // BucketId is a bucket ID. 35 | BucketId OptUint 36 | // ForceMapCall describes the map call is performed without any 37 | // optimizations even if full primary key equal condition is specified. 38 | ForceMapCall OptBool 39 | // Fullscan describes if a critical log entry will be skipped on 40 | // potentially long count. 41 | Fullscan OptBool 42 | } 43 | 44 | // EncodeMsgpack provides custom msgpack encoder. 45 | func (opts CountOpts) EncodeMsgpack(enc *msgpack.Encoder) error { 46 | const optsCnt = 9 47 | 48 | names := [optsCnt]string{timeoutOptName, vshardRouterOptName, 49 | modeOptName, preferReplicaOptName, balanceOptName, 50 | yieldEveryOptName, bucketIdOptName, forceMapCallOptName, 51 | fullscanOptName} 52 | values := [optsCnt]interface{}{} 53 | exists := [optsCnt]bool{} 54 | values[0], exists[0] = opts.Timeout.Get() 55 | values[1], exists[1] = opts.VshardRouter.Get() 56 | values[2], exists[2] = opts.Mode.Get() 57 | values[3], exists[3] = opts.PreferReplica.Get() 58 | values[4], exists[4] = opts.Balance.Get() 59 | values[5], exists[5] = opts.YieldEvery.Get() 60 | values[6], exists[6] = opts.BucketId.Get() 61 | values[7], exists[7] = opts.ForceMapCall.Get() 62 | values[8], exists[8] = opts.Fullscan.Get() 63 | 64 | return encodeOptions(enc, names[:], values[:], exists[:]) 65 | } 66 | 67 | // CountRequest helps you to create request object to call `crud.count` 68 | // for execution by a Connection. 69 | type CountRequest struct { 70 | spaceRequest 71 | conditions []Condition 72 | opts CountOpts 73 | } 74 | 75 | type countArgs struct { 76 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 77 | Space string 78 | Conditions []Condition 79 | Opts CountOpts 80 | } 81 | 82 | // MakeCountRequest returns a new empty CountRequest. 83 | func MakeCountRequest(space string) CountRequest { 84 | req := CountRequest{} 85 | req.impl = newCall("crud.count") 86 | req.space = space 87 | req.conditions = nil 88 | req.opts = CountOpts{} 89 | return req 90 | } 91 | 92 | // Conditions sets the conditions for the CountRequest request. 93 | // Note: default value is nil. 94 | func (req CountRequest) Conditions(conditions []Condition) CountRequest { 95 | req.conditions = conditions 96 | return req 97 | } 98 | 99 | // Opts sets the options for the CountRequest request. 100 | // Note: default value is nil. 101 | func (req CountRequest) Opts(opts CountOpts) CountRequest { 102 | req.opts = opts 103 | return req 104 | } 105 | 106 | // Body fills an encoder with the call request body. 107 | func (req CountRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 108 | args := countArgs{Space: req.space, Conditions: req.conditions, Opts: req.opts} 109 | req.impl = req.impl.Args(args) 110 | return req.impl.Body(res, enc) 111 | } 112 | 113 | // Context sets a passed context to CRUD request. 114 | func (req CountRequest) Context(ctx context.Context) CountRequest { 115 | req.impl = req.impl.Context(ctx) 116 | 117 | return req 118 | } 119 | -------------------------------------------------------------------------------- /crud/delete.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/vmihailenco/msgpack/v5" 7 | 8 | "github.com/tarantool/go-tarantool/v2" 9 | ) 10 | 11 | // DeleteOpts describes options for `crud.delete` method. 12 | type DeleteOpts = SimpleOperationOpts 13 | 14 | // DeleteRequest helps you to create request object to call `crud.delete` 15 | // for execution by a Connection. 16 | type DeleteRequest struct { 17 | spaceRequest 18 | key Tuple 19 | opts DeleteOpts 20 | } 21 | 22 | type deleteArgs struct { 23 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 24 | Space string 25 | Key Tuple 26 | Opts DeleteOpts 27 | } 28 | 29 | // MakeDeleteRequest returns a new empty DeleteRequest. 30 | func MakeDeleteRequest(space string) DeleteRequest { 31 | req := DeleteRequest{} 32 | req.impl = newCall("crud.delete") 33 | req.space = space 34 | req.opts = DeleteOpts{} 35 | return req 36 | } 37 | 38 | // Key sets the key for the DeleteRequest request. 39 | // Note: default value is nil. 40 | func (req DeleteRequest) Key(key Tuple) DeleteRequest { 41 | req.key = key 42 | return req 43 | } 44 | 45 | // Opts sets the options for the DeleteRequest request. 46 | // Note: default value is nil. 47 | func (req DeleteRequest) Opts(opts DeleteOpts) DeleteRequest { 48 | req.opts = opts 49 | return req 50 | } 51 | 52 | // Body fills an encoder with the call request body. 53 | func (req DeleteRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 54 | if req.key == nil { 55 | req.key = []interface{}{} 56 | } 57 | args := deleteArgs{Space: req.space, Key: req.key, Opts: req.opts} 58 | req.impl = req.impl.Args(args) 59 | return req.impl.Body(res, enc) 60 | } 61 | 62 | // Context sets a passed context to CRUD request. 63 | func (req DeleteRequest) Context(ctx context.Context) DeleteRequest { 64 | req.impl = req.impl.Context(ctx) 65 | 66 | return req 67 | } 68 | -------------------------------------------------------------------------------- /crud/error.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | 7 | "github.com/vmihailenco/msgpack/v5" 8 | ) 9 | 10 | // Error describes CRUD error object. 11 | type Error struct { 12 | // ClassName is an error class that implies its source (for example, "CountError"). 13 | ClassName string 14 | // Err is the text of reason. 15 | Err string 16 | // File is a source code file where the error was caught. 17 | File string 18 | // Line is a number of line in the source code file where the error was caught. 19 | Line uint64 20 | // Stack is an information about the call stack when an error 21 | // occurs in a string format. 22 | Stack string 23 | // Str is the text of reason with error class. 24 | Str string 25 | // OperationData is the object/tuple with which an error occurred. 26 | OperationData interface{} 27 | // operationDataType contains the type of OperationData. 28 | operationDataType reflect.Type 29 | } 30 | 31 | // newError creates an Error object with a custom operation data type to decoding. 32 | func newError(operationDataType reflect.Type) *Error { 33 | return &Error{operationDataType: operationDataType} 34 | } 35 | 36 | // DecodeMsgpack provides custom msgpack decoder. 37 | func (e *Error) DecodeMsgpack(d *msgpack.Decoder) error { 38 | l, err := d.DecodeMapLen() 39 | if err != nil { 40 | return err 41 | } 42 | for i := 0; i < l; i++ { 43 | key, err := d.DecodeString() 44 | if err != nil { 45 | return err 46 | } 47 | switch key { 48 | case "class_name": 49 | if e.ClassName, err = d.DecodeString(); err != nil { 50 | return err 51 | } 52 | case "err": 53 | if e.Err, err = d.DecodeString(); err != nil { 54 | return err 55 | } 56 | case "file": 57 | if e.File, err = d.DecodeString(); err != nil { 58 | return err 59 | } 60 | case "line": 61 | if e.Line, err = d.DecodeUint64(); err != nil { 62 | return err 63 | } 64 | case "stack": 65 | if e.Stack, err = d.DecodeString(); err != nil { 66 | return err 67 | } 68 | case "str": 69 | if e.Str, err = d.DecodeString(); err != nil { 70 | return err 71 | } 72 | case "operation_data": 73 | if e.operationDataType != nil { 74 | tuple := reflect.New(e.operationDataType) 75 | if err = d.DecodeValue(tuple); err != nil { 76 | return err 77 | } 78 | e.OperationData = tuple.Elem().Interface() 79 | } else { 80 | if err = d.Decode(&e.OperationData); err != nil { 81 | return err 82 | } 83 | } 84 | default: 85 | if err := d.Skip(); err != nil { 86 | return err 87 | } 88 | } 89 | } 90 | 91 | return nil 92 | } 93 | 94 | // Error converts an Error to a string. 95 | func (e Error) Error() string { 96 | return e.Str 97 | } 98 | 99 | // ErrorMany describes CRUD error object for `_many` methods. 100 | type ErrorMany struct { 101 | Errors []Error 102 | // operationDataType contains the type of OperationData for each Error. 103 | operationDataType reflect.Type 104 | } 105 | 106 | // newErrorMany creates an ErrorMany object with a custom operation data type to decoding. 107 | func newErrorMany(operationDataType reflect.Type) *ErrorMany { 108 | return &ErrorMany{operationDataType: operationDataType} 109 | } 110 | 111 | // DecodeMsgpack provides custom msgpack decoder. 112 | func (e *ErrorMany) DecodeMsgpack(d *msgpack.Decoder) error { 113 | l, err := d.DecodeArrayLen() 114 | if err != nil { 115 | return err 116 | } 117 | 118 | var errs []Error 119 | for i := 0; i < l; i++ { 120 | crudErr := newError(e.operationDataType) 121 | if err := d.Decode(&crudErr); err != nil { 122 | return err 123 | } 124 | errs = append(errs, *crudErr) 125 | } 126 | 127 | if len(errs) > 0 { 128 | e.Errors = errs 129 | } 130 | 131 | return nil 132 | } 133 | 134 | // Error converts an Error to a string. 135 | func (e ErrorMany) Error() string { 136 | var str []string 137 | for _, err := range e.Errors { 138 | str = append(str, err.Str) 139 | } 140 | 141 | return strings.Join(str, "\n") 142 | } 143 | -------------------------------------------------------------------------------- /crud/error_test.go: -------------------------------------------------------------------------------- 1 | package crud_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/tarantool/go-tarantool/v2/crud" 8 | ) 9 | 10 | func TestErrorMany(t *testing.T) { 11 | errs := crud.ErrorMany{Errors: []crud.Error{ 12 | { 13 | ClassName: "a", 14 | Str: "msg 1", 15 | }, 16 | { 17 | ClassName: "b", 18 | Str: "msg 2", 19 | }, 20 | { 21 | ClassName: "c", 22 | Str: "msg 3", 23 | }, 24 | }} 25 | 26 | require.Equal(t, "msg 1\nmsg 2\nmsg 3", errs.Error()) 27 | } 28 | -------------------------------------------------------------------------------- /crud/get.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/vmihailenco/msgpack/v5" 7 | 8 | "github.com/tarantool/go-tarantool/v2" 9 | ) 10 | 11 | // GetOpts describes options for `crud.get` method. 12 | type GetOpts struct { 13 | // Timeout is a `vshard.call` timeout and vshard 14 | // master discovery timeout (in seconds). 15 | Timeout OptFloat64 16 | // VshardRouter is cartridge vshard group name or 17 | // vshard router instance. 18 | VshardRouter OptString 19 | // Fields is field names for getting only a subset of fields. 20 | Fields OptTuple 21 | // BucketId is a bucket ID. 22 | BucketId OptUint 23 | // Mode is a parameter with `write`/`read` possible values, 24 | // if `write` is specified then operation is performed on master. 25 | Mode OptString 26 | // PreferReplica is a parameter to specify preferred target 27 | // as one of the replicas. 28 | PreferReplica OptBool 29 | // Balance is a parameter to use replica according to vshard 30 | // load balancing policy. 31 | Balance OptBool 32 | // FetchLatestMetadata guarantees the up-to-date metadata (space format) 33 | // in first return value, otherwise it may not take into account 34 | // the latest migration of the data format. Performance overhead is up to 15%. 35 | // Disabled by default. 36 | FetchLatestMetadata OptBool 37 | } 38 | 39 | // EncodeMsgpack provides custom msgpack encoder. 40 | func (opts GetOpts) EncodeMsgpack(enc *msgpack.Encoder) error { 41 | const optsCnt = 8 42 | 43 | names := [optsCnt]string{timeoutOptName, vshardRouterOptName, 44 | fieldsOptName, bucketIdOptName, modeOptName, 45 | preferReplicaOptName, balanceOptName, fetchLatestMetadataOptName} 46 | values := [optsCnt]interface{}{} 47 | exists := [optsCnt]bool{} 48 | values[0], exists[0] = opts.Timeout.Get() 49 | values[1], exists[1] = opts.VshardRouter.Get() 50 | values[2], exists[2] = opts.Fields.Get() 51 | values[3], exists[3] = opts.BucketId.Get() 52 | values[4], exists[4] = opts.Mode.Get() 53 | values[5], exists[5] = opts.PreferReplica.Get() 54 | values[6], exists[6] = opts.Balance.Get() 55 | values[7], exists[7] = opts.FetchLatestMetadata.Get() 56 | 57 | return encodeOptions(enc, names[:], values[:], exists[:]) 58 | } 59 | 60 | // GetRequest helps you to create request object to call `crud.get` 61 | // for execution by a Connection. 62 | type GetRequest struct { 63 | spaceRequest 64 | key Tuple 65 | opts GetOpts 66 | } 67 | 68 | type getArgs struct { 69 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 70 | Space string 71 | Key Tuple 72 | Opts GetOpts 73 | } 74 | 75 | // MakeGetRequest returns a new empty GetRequest. 76 | func MakeGetRequest(space string) GetRequest { 77 | req := GetRequest{} 78 | req.impl = newCall("crud.get") 79 | req.space = space 80 | req.opts = GetOpts{} 81 | return req 82 | } 83 | 84 | // Key sets the key for the GetRequest request. 85 | // Note: default value is nil. 86 | func (req GetRequest) Key(key Tuple) GetRequest { 87 | req.key = key 88 | return req 89 | } 90 | 91 | // Opts sets the options for the GetRequest request. 92 | // Note: default value is nil. 93 | func (req GetRequest) Opts(opts GetOpts) GetRequest { 94 | req.opts = opts 95 | return req 96 | } 97 | 98 | // Body fills an encoder with the call request body. 99 | func (req GetRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 100 | if req.key == nil { 101 | req.key = []interface{}{} 102 | } 103 | args := getArgs{Space: req.space, Key: req.key, Opts: req.opts} 104 | req.impl = req.impl.Args(args) 105 | return req.impl.Body(res, enc) 106 | } 107 | 108 | // Context sets a passed context to CRUD request. 109 | func (req GetRequest) Context(ctx context.Context) GetRequest { 110 | req.impl = req.impl.Context(ctx) 111 | 112 | return req 113 | } 114 | -------------------------------------------------------------------------------- /crud/insert.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/vmihailenco/msgpack/v5" 7 | 8 | "github.com/tarantool/go-tarantool/v2" 9 | ) 10 | 11 | // InsertOpts describes options for `crud.insert` method. 12 | type InsertOpts = SimpleOperationOpts 13 | 14 | // InsertRequest helps you to create request object to call `crud.insert` 15 | // for execution by a Connection. 16 | type InsertRequest struct { 17 | spaceRequest 18 | tuple Tuple 19 | opts InsertOpts 20 | } 21 | 22 | type insertArgs struct { 23 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 24 | Space string 25 | Tuple Tuple 26 | Opts InsertOpts 27 | } 28 | 29 | // MakeInsertRequest returns a new empty InsertRequest. 30 | func MakeInsertRequest(space string) InsertRequest { 31 | req := InsertRequest{} 32 | req.impl = newCall("crud.insert") 33 | req.space = space 34 | req.opts = InsertOpts{} 35 | return req 36 | } 37 | 38 | // Tuple sets the tuple for the InsertRequest request. 39 | // Note: default value is nil. 40 | func (req InsertRequest) Tuple(tuple Tuple) InsertRequest { 41 | req.tuple = tuple 42 | return req 43 | } 44 | 45 | // Opts sets the options for the insert request. 46 | // Note: default value is nil. 47 | func (req InsertRequest) Opts(opts InsertOpts) InsertRequest { 48 | req.opts = opts 49 | return req 50 | } 51 | 52 | // Body fills an encoder with the call request body. 53 | func (req InsertRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 54 | if req.tuple == nil { 55 | req.tuple = []interface{}{} 56 | } 57 | args := insertArgs{Space: req.space, Tuple: req.tuple, Opts: req.opts} 58 | req.impl = req.impl.Args(args) 59 | return req.impl.Body(res, enc) 60 | } 61 | 62 | // Context sets a passed context to CRUD request. 63 | func (req InsertRequest) Context(ctx context.Context) InsertRequest { 64 | req.impl = req.impl.Context(ctx) 65 | 66 | return req 67 | } 68 | 69 | // InsertObjectOpts describes options for `crud.insert_object` method. 70 | type InsertObjectOpts = SimpleOperationObjectOpts 71 | 72 | // InsertObjectRequest helps you to create request object to call 73 | // `crud.insert_object` for execution by a Connection. 74 | type InsertObjectRequest struct { 75 | spaceRequest 76 | object Object 77 | opts InsertObjectOpts 78 | } 79 | 80 | type insertObjectArgs struct { 81 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 82 | Space string 83 | Object Object 84 | Opts InsertObjectOpts 85 | } 86 | 87 | // MakeInsertObjectRequest returns a new empty InsertObjectRequest. 88 | func MakeInsertObjectRequest(space string) InsertObjectRequest { 89 | req := InsertObjectRequest{} 90 | req.impl = newCall("crud.insert_object") 91 | req.space = space 92 | req.opts = InsertObjectOpts{} 93 | return req 94 | } 95 | 96 | // Object sets the tuple for the InsertObjectRequest request. 97 | // Note: default value is nil. 98 | func (req InsertObjectRequest) Object(object Object) InsertObjectRequest { 99 | req.object = object 100 | return req 101 | } 102 | 103 | // Opts sets the options for the InsertObjectRequest request. 104 | // Note: default value is nil. 105 | func (req InsertObjectRequest) Opts(opts InsertObjectOpts) InsertObjectRequest { 106 | req.opts = opts 107 | return req 108 | } 109 | 110 | // Body fills an encoder with the call request body. 111 | func (req InsertObjectRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 112 | if req.object == nil { 113 | req.object = MapObject{} 114 | } 115 | args := insertObjectArgs{Space: req.space, Object: req.object, Opts: req.opts} 116 | req.impl = req.impl.Args(args) 117 | return req.impl.Body(res, enc) 118 | } 119 | 120 | // Context sets a passed context to CRUD request. 121 | func (req InsertObjectRequest) Context(ctx context.Context) InsertObjectRequest { 122 | req.impl = req.impl.Context(ctx) 123 | 124 | return req 125 | } 126 | -------------------------------------------------------------------------------- /crud/insert_many.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/vmihailenco/msgpack/v5" 7 | 8 | "github.com/tarantool/go-tarantool/v2" 9 | ) 10 | 11 | // InsertManyOpts describes options for `crud.insert_many` method. 12 | type InsertManyOpts = OperationManyOpts 13 | 14 | // InsertManyRequest helps you to create request object to call 15 | // `crud.insert_many` for execution by a Connection. 16 | type InsertManyRequest struct { 17 | spaceRequest 18 | tuples Tuples 19 | opts InsertManyOpts 20 | } 21 | 22 | type insertManyArgs struct { 23 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 24 | Space string 25 | Tuples Tuples 26 | Opts InsertManyOpts 27 | } 28 | 29 | // MakeInsertManyRequest returns a new empty InsertManyRequest. 30 | func MakeInsertManyRequest(space string) InsertManyRequest { 31 | req := InsertManyRequest{} 32 | req.impl = newCall("crud.insert_many") 33 | req.space = space 34 | req.opts = InsertManyOpts{} 35 | return req 36 | } 37 | 38 | // Tuples sets the tuples for the InsertManyRequest request. 39 | // Note: default value is nil. 40 | func (req InsertManyRequest) Tuples(tuples Tuples) InsertManyRequest { 41 | req.tuples = tuples 42 | return req 43 | } 44 | 45 | // Opts sets the options for the InsertManyRequest request. 46 | // Note: default value is nil. 47 | func (req InsertManyRequest) Opts(opts InsertManyOpts) InsertManyRequest { 48 | req.opts = opts 49 | return req 50 | } 51 | 52 | // Body fills an encoder with the call request body. 53 | func (req InsertManyRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 54 | if req.tuples == nil { 55 | req.tuples = []Tuple{} 56 | } 57 | args := insertManyArgs{Space: req.space, Tuples: req.tuples, Opts: req.opts} 58 | req.impl = req.impl.Args(args) 59 | return req.impl.Body(res, enc) 60 | } 61 | 62 | // Context sets a passed context to CRUD request. 63 | func (req InsertManyRequest) Context(ctx context.Context) InsertManyRequest { 64 | req.impl = req.impl.Context(ctx) 65 | 66 | return req 67 | } 68 | 69 | // InsertObjectManyOpts describes options for `crud.insert_object_many` method. 70 | type InsertObjectManyOpts = OperationObjectManyOpts 71 | 72 | // InsertObjectManyRequest helps you to create request object to call 73 | // `crud.insert_object_many` for execution by a Connection. 74 | type InsertObjectManyRequest struct { 75 | spaceRequest 76 | objects Objects 77 | opts InsertObjectManyOpts 78 | } 79 | 80 | type insertObjectManyArgs struct { 81 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 82 | Space string 83 | Objects Objects 84 | Opts InsertObjectManyOpts 85 | } 86 | 87 | // MakeInsertObjectManyRequest returns a new empty InsertObjectManyRequest. 88 | func MakeInsertObjectManyRequest(space string) InsertObjectManyRequest { 89 | req := InsertObjectManyRequest{} 90 | req.impl = newCall("crud.insert_object_many") 91 | req.space = space 92 | req.opts = InsertObjectManyOpts{} 93 | return req 94 | } 95 | 96 | // Objects sets the objects for the InsertObjectManyRequest request. 97 | // Note: default value is nil. 98 | func (req InsertObjectManyRequest) Objects(objects Objects) InsertObjectManyRequest { 99 | req.objects = objects 100 | return req 101 | } 102 | 103 | // Opts sets the options for the InsertObjectManyRequest request. 104 | // Note: default value is nil. 105 | func (req InsertObjectManyRequest) Opts(opts InsertObjectManyOpts) InsertObjectManyRequest { 106 | req.opts = opts 107 | return req 108 | } 109 | 110 | // Body fills an encoder with the call request body. 111 | func (req InsertObjectManyRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 112 | if req.objects == nil { 113 | req.objects = []Object{} 114 | } 115 | args := insertObjectManyArgs{Space: req.space, Objects: req.objects, Opts: req.opts} 116 | req.impl = req.impl.Args(args) 117 | return req.impl.Body(res, enc) 118 | } 119 | 120 | // Context sets a passed context to CRUD request. 121 | func (req InsertObjectManyRequest) Context(ctx context.Context) InsertObjectManyRequest { 122 | req.impl = req.impl.Context(ctx) 123 | 124 | return req 125 | } 126 | -------------------------------------------------------------------------------- /crud/len.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/vmihailenco/msgpack/v5" 7 | 8 | "github.com/tarantool/go-tarantool/v2" 9 | ) 10 | 11 | // LenResult describes result for `crud.len` method. 12 | type LenResult = NumberResult 13 | 14 | // LenOpts describes options for `crud.len` method. 15 | type LenOpts = BaseOpts 16 | 17 | // LenRequest helps you to create request object to call `crud.len` 18 | // for execution by a Connection. 19 | type LenRequest struct { 20 | spaceRequest 21 | opts LenOpts 22 | } 23 | 24 | type lenArgs struct { 25 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 26 | Space string 27 | Opts LenOpts 28 | } 29 | 30 | // MakeLenRequest returns a new empty LenRequest. 31 | func MakeLenRequest(space string) LenRequest { 32 | req := LenRequest{} 33 | req.impl = newCall("crud.len") 34 | req.space = space 35 | req.opts = LenOpts{} 36 | return req 37 | } 38 | 39 | // Opts sets the options for the LenRequest request. 40 | // Note: default value is nil. 41 | func (req LenRequest) Opts(opts LenOpts) LenRequest { 42 | req.opts = opts 43 | return req 44 | } 45 | 46 | // Body fills an encoder with the call request body. 47 | func (req LenRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 48 | args := lenArgs{Space: req.space, Opts: req.opts} 49 | req.impl = req.impl.Args(args) 50 | return req.impl.Body(res, enc) 51 | } 52 | 53 | // Context sets a passed context to CRUD request. 54 | func (req LenRequest) Context(ctx context.Context) LenRequest { 55 | req.impl = req.impl.Context(ctx) 56 | 57 | return req 58 | } 59 | -------------------------------------------------------------------------------- /crud/max.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/vmihailenco/msgpack/v5" 7 | 8 | "github.com/tarantool/go-tarantool/v2" 9 | ) 10 | 11 | // MaxOpts describes options for `crud.max` method. 12 | type MaxOpts = BorderOpts 13 | 14 | // MaxRequest helps you to create request object to call `crud.max` 15 | // for execution by a Connection. 16 | type MaxRequest struct { 17 | spaceRequest 18 | index interface{} 19 | opts MaxOpts 20 | } 21 | 22 | type maxArgs struct { 23 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 24 | Space string 25 | Index interface{} 26 | Opts MaxOpts 27 | } 28 | 29 | // MakeMaxRequest returns a new empty MaxRequest. 30 | func MakeMaxRequest(space string) MaxRequest { 31 | req := MaxRequest{} 32 | req.impl = newCall("crud.max") 33 | req.space = space 34 | req.opts = MaxOpts{} 35 | return req 36 | } 37 | 38 | // Index sets the index name/id for the MaxRequest request. 39 | // Note: default value is nil. 40 | func (req MaxRequest) Index(index interface{}) MaxRequest { 41 | req.index = index 42 | return req 43 | } 44 | 45 | // Opts sets the options for the MaxRequest request. 46 | // Note: default value is nil. 47 | func (req MaxRequest) Opts(opts MaxOpts) MaxRequest { 48 | req.opts = opts 49 | return req 50 | } 51 | 52 | // Body fills an encoder with the call request body. 53 | func (req MaxRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 54 | args := maxArgs{Space: req.space, Index: req.index, Opts: req.opts} 55 | req.impl = req.impl.Args(args) 56 | return req.impl.Body(res, enc) 57 | } 58 | 59 | // Context sets a passed context to CRUD request. 60 | func (req MaxRequest) Context(ctx context.Context) MaxRequest { 61 | req.impl = req.impl.Context(ctx) 62 | 63 | return req 64 | } 65 | -------------------------------------------------------------------------------- /crud/min.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/vmihailenco/msgpack/v5" 7 | 8 | "github.com/tarantool/go-tarantool/v2" 9 | ) 10 | 11 | // MinOpts describes options for `crud.min` method. 12 | type MinOpts = BorderOpts 13 | 14 | // MinRequest helps you to create request object to call `crud.min` 15 | // for execution by a Connection. 16 | type MinRequest struct { 17 | spaceRequest 18 | index interface{} 19 | opts MinOpts 20 | } 21 | 22 | type minArgs struct { 23 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 24 | Space string 25 | Index interface{} 26 | Opts MinOpts 27 | } 28 | 29 | // MakeMinRequest returns a new empty MinRequest. 30 | func MakeMinRequest(space string) MinRequest { 31 | req := MinRequest{} 32 | req.impl = newCall("crud.min") 33 | req.space = space 34 | req.opts = MinOpts{} 35 | return req 36 | } 37 | 38 | // Index sets the index name/id for the MinRequest request. 39 | // Note: default value is nil. 40 | func (req MinRequest) Index(index interface{}) MinRequest { 41 | req.index = index 42 | return req 43 | } 44 | 45 | // Opts sets the options for the MinRequest request. 46 | // Note: default value is nil. 47 | func (req MinRequest) Opts(opts MinOpts) MinRequest { 48 | req.opts = opts 49 | return req 50 | } 51 | 52 | // Body fills an encoder with the call request body. 53 | func (req MinRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 54 | args := minArgs{Space: req.space, Index: req.index, Opts: req.opts} 55 | req.impl = req.impl.Args(args) 56 | return req.impl.Body(res, enc) 57 | } 58 | 59 | // Context sets a passed context to CRUD request. 60 | func (req MinRequest) Context(ctx context.Context) MinRequest { 61 | req.impl = req.impl.Context(ctx) 62 | 63 | return req 64 | } 65 | -------------------------------------------------------------------------------- /crud/object.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "github.com/vmihailenco/msgpack/v5" 5 | ) 6 | 7 | // Object is an interface to describe object for CRUD methods. It can be any 8 | // type that msgpack can encode as a map. 9 | type Object = interface{} 10 | 11 | // Objects is a type to describe an array of object for CRUD methods. It can be 12 | // any type that msgpack can encode, but encoded data must be an array of 13 | // objects. 14 | // 15 | // See the reason why not just []Object: 16 | // https://github.com/tarantool/go-tarantool/issues/365 17 | type Objects = interface{} 18 | 19 | // MapObject is a type to describe object as a map. 20 | type MapObject map[string]interface{} 21 | 22 | func (o MapObject) EncodeMsgpack(enc *msgpack.Encoder) { 23 | enc.Encode(o) 24 | } 25 | -------------------------------------------------------------------------------- /crud/operations.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "github.com/vmihailenco/msgpack/v5" 5 | ) 6 | 7 | const ( 8 | // Add - operator for addition. 9 | Add Operator = "+" 10 | // Sub - operator for subtraction. 11 | Sub Operator = "-" 12 | // And - operator for bitwise AND. 13 | And Operator = "&" 14 | // Or - operator for bitwise OR. 15 | Or Operator = "|" 16 | // Xor - operator for bitwise XOR. 17 | Xor Operator = "^" 18 | // Splice - operator for string splice. 19 | Splice Operator = ":" 20 | // Insert - operator for insertion of a new field. 21 | Insert Operator = "!" 22 | // Delete - operator for deletion. 23 | Delete Operator = "#" 24 | // Assign - operator for assignment. 25 | Assign Operator = "=" 26 | ) 27 | 28 | // Operation describes CRUD operation as a table 29 | // {operator, field_identifier, value}. 30 | // Splice operation described as a table 31 | // {operator, field_identifier, position, length, replace_string}. 32 | type Operation struct { 33 | Operator Operator 34 | Field interface{} // Number or string. 35 | Value interface{} 36 | // Pos, Len, Replace fields used in the Splice operation. 37 | Pos int 38 | Len int 39 | Replace string 40 | } 41 | 42 | // EncodeMsgpack encodes Operation. 43 | func (o Operation) EncodeMsgpack(enc *msgpack.Encoder) error { 44 | isSpliceOperation := o.Operator == Splice 45 | argsLen := 3 46 | if isSpliceOperation { 47 | argsLen = 5 48 | } 49 | if err := enc.EncodeArrayLen(argsLen); err != nil { 50 | return err 51 | } 52 | if err := enc.EncodeString(string(o.Operator)); err != nil { 53 | return err 54 | } 55 | if err := enc.Encode(o.Field); err != nil { 56 | return err 57 | } 58 | 59 | if isSpliceOperation { 60 | if err := enc.EncodeInt(int64(o.Pos)); err != nil { 61 | return err 62 | } 63 | if err := enc.EncodeInt(int64(o.Len)); err != nil { 64 | return err 65 | } 66 | return enc.EncodeString(o.Replace) 67 | } 68 | 69 | return enc.Encode(o.Value) 70 | } 71 | -------------------------------------------------------------------------------- /crud/operations_test.go: -------------------------------------------------------------------------------- 1 | package crud_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/vmihailenco/msgpack/v5" 8 | 9 | "github.com/tarantool/go-tarantool/v2/crud" 10 | ) 11 | 12 | func TestOperation_EncodeMsgpack(t *testing.T) { 13 | testCases := []struct { 14 | name string 15 | op crud.Operation 16 | ref []interface{} 17 | }{ 18 | { 19 | "Add", 20 | crud.Operation{ 21 | Operator: crud.Add, 22 | Field: 1, 23 | Value: 2, 24 | }, 25 | []interface{}{"+", 1, 2}, 26 | }, 27 | { 28 | "Sub", 29 | crud.Operation{ 30 | Operator: crud.Sub, 31 | Field: 1, 32 | Value: 2, 33 | }, 34 | []interface{}{"-", 1, 2}, 35 | }, 36 | { 37 | "And", 38 | crud.Operation{ 39 | Operator: crud.And, 40 | Field: 1, 41 | Value: 2, 42 | }, 43 | []interface{}{"&", 1, 2}, 44 | }, 45 | { 46 | "Or", 47 | crud.Operation{ 48 | Operator: crud.Or, 49 | Field: 1, 50 | Value: 2, 51 | }, 52 | []interface{}{"|", 1, 2}, 53 | }, 54 | { 55 | "Xor", 56 | crud.Operation{ 57 | Operator: crud.Xor, 58 | Field: 1, 59 | Value: 2, 60 | }, 61 | []interface{}{"^", 1, 2}, 62 | }, 63 | { 64 | "Splice", 65 | crud.Operation{ 66 | Operator: crud.Splice, 67 | Field: 1, 68 | Pos: 2, 69 | Len: 3, 70 | Replace: "a", 71 | }, 72 | []interface{}{":", 1, 2, 3, "a"}, 73 | }, 74 | { 75 | "Insert", 76 | crud.Operation{ 77 | Operator: crud.Insert, 78 | Field: 1, 79 | Value: 2, 80 | }, 81 | []interface{}{"!", 1, 2}, 82 | }, 83 | { 84 | "Delete", 85 | crud.Operation{ 86 | Operator: crud.Delete, 87 | Field: 1, 88 | Value: 2, 89 | }, 90 | []interface{}{"#", 1, 2}, 91 | }, 92 | { 93 | "Assign", 94 | crud.Operation{ 95 | Operator: crud.Assign, 96 | Field: 1, 97 | Value: 2, 98 | }, 99 | []interface{}{"=", 1, 2}, 100 | }, 101 | } 102 | 103 | for _, test := range testCases { 104 | t.Run(test.name, func(t *testing.T) { 105 | var refBuf bytes.Buffer 106 | encRef := msgpack.NewEncoder(&refBuf) 107 | if err := encRef.Encode(test.ref); err != nil { 108 | t.Errorf("error while encoding: %v", err.Error()) 109 | } 110 | 111 | var buf bytes.Buffer 112 | enc := msgpack.NewEncoder(&buf) 113 | 114 | if err := enc.Encode(test.op); err != nil { 115 | t.Errorf("error while encoding: %v", err.Error()) 116 | } 117 | if !bytes.Equal(refBuf.Bytes(), buf.Bytes()) { 118 | t.Errorf("encode response is wrong:\n expected %v\n got: %v", 119 | refBuf, buf.Bytes()) 120 | } 121 | }) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /crud/replace.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/vmihailenco/msgpack/v5" 7 | 8 | "github.com/tarantool/go-tarantool/v2" 9 | ) 10 | 11 | // ReplaceOpts describes options for `crud.replace` method. 12 | type ReplaceOpts = SimpleOperationOpts 13 | 14 | // ReplaceRequest helps you to create request object to call `crud.replace` 15 | // for execution by a Connection. 16 | type ReplaceRequest struct { 17 | spaceRequest 18 | tuple Tuple 19 | opts ReplaceOpts 20 | } 21 | 22 | type replaceArgs struct { 23 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 24 | Space string 25 | Tuple Tuple 26 | Opts ReplaceOpts 27 | } 28 | 29 | // MakeReplaceRequest returns a new empty ReplaceRequest. 30 | func MakeReplaceRequest(space string) ReplaceRequest { 31 | req := ReplaceRequest{} 32 | req.impl = newCall("crud.replace") 33 | req.space = space 34 | req.opts = ReplaceOpts{} 35 | return req 36 | } 37 | 38 | // Tuple sets the tuple for the ReplaceRequest request. 39 | // Note: default value is nil. 40 | func (req ReplaceRequest) Tuple(tuple Tuple) ReplaceRequest { 41 | req.tuple = tuple 42 | return req 43 | } 44 | 45 | // Opts sets the options for the ReplaceRequest request. 46 | // Note: default value is nil. 47 | func (req ReplaceRequest) Opts(opts ReplaceOpts) ReplaceRequest { 48 | req.opts = opts 49 | return req 50 | } 51 | 52 | // Body fills an encoder with the call request body. 53 | func (req ReplaceRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 54 | if req.tuple == nil { 55 | req.tuple = []interface{}{} 56 | } 57 | args := replaceArgs{Space: req.space, Tuple: req.tuple, Opts: req.opts} 58 | req.impl = req.impl.Args(args) 59 | return req.impl.Body(res, enc) 60 | } 61 | 62 | // Context sets a passed context to CRUD request. 63 | func (req ReplaceRequest) Context(ctx context.Context) ReplaceRequest { 64 | req.impl = req.impl.Context(ctx) 65 | 66 | return req 67 | } 68 | 69 | // ReplaceObjectOpts describes options for `crud.replace_object` method. 70 | type ReplaceObjectOpts = SimpleOperationObjectOpts 71 | 72 | // ReplaceObjectRequest helps you to create request object to call 73 | // `crud.replace_object` for execution by a Connection. 74 | type ReplaceObjectRequest struct { 75 | spaceRequest 76 | object Object 77 | opts ReplaceObjectOpts 78 | } 79 | 80 | type replaceObjectArgs struct { 81 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 82 | Space string 83 | Object Object 84 | Opts ReplaceObjectOpts 85 | } 86 | 87 | // MakeReplaceObjectRequest returns a new empty ReplaceObjectRequest. 88 | func MakeReplaceObjectRequest(space string) ReplaceObjectRequest { 89 | req := ReplaceObjectRequest{} 90 | req.impl = newCall("crud.replace_object") 91 | req.space = space 92 | req.opts = ReplaceObjectOpts{} 93 | return req 94 | } 95 | 96 | // Object sets the tuple for the ReplaceObjectRequest request. 97 | // Note: default value is nil. 98 | func (req ReplaceObjectRequest) Object(object Object) ReplaceObjectRequest { 99 | req.object = object 100 | return req 101 | } 102 | 103 | // Opts sets the options for the ReplaceObjectRequest request. 104 | // Note: default value is nil. 105 | func (req ReplaceObjectRequest) Opts(opts ReplaceObjectOpts) ReplaceObjectRequest { 106 | req.opts = opts 107 | return req 108 | } 109 | 110 | // Body fills an encoder with the call request body. 111 | func (req ReplaceObjectRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 112 | if req.object == nil { 113 | req.object = MapObject{} 114 | } 115 | args := replaceObjectArgs{Space: req.space, Object: req.object, Opts: req.opts} 116 | req.impl = req.impl.Args(args) 117 | return req.impl.Body(res, enc) 118 | } 119 | 120 | // Context sets a passed context to CRUD request. 121 | func (req ReplaceObjectRequest) Context(ctx context.Context) ReplaceObjectRequest { 122 | req.impl = req.impl.Context(ctx) 123 | 124 | return req 125 | } 126 | -------------------------------------------------------------------------------- /crud/replace_many.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/vmihailenco/msgpack/v5" 7 | 8 | "github.com/tarantool/go-tarantool/v2" 9 | ) 10 | 11 | // ReplaceManyOpts describes options for `crud.replace_many` method. 12 | type ReplaceManyOpts = OperationManyOpts 13 | 14 | // ReplaceManyRequest helps you to create request object to call 15 | // `crud.replace_many` for execution by a Connection. 16 | type ReplaceManyRequest struct { 17 | spaceRequest 18 | tuples Tuples 19 | opts ReplaceManyOpts 20 | } 21 | 22 | type replaceManyArgs struct { 23 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 24 | Space string 25 | Tuples Tuples 26 | Opts ReplaceManyOpts 27 | } 28 | 29 | // MakeReplaceManyRequest returns a new empty ReplaceManyRequest. 30 | func MakeReplaceManyRequest(space string) ReplaceManyRequest { 31 | req := ReplaceManyRequest{} 32 | req.impl = newCall("crud.replace_many") 33 | req.space = space 34 | req.opts = ReplaceManyOpts{} 35 | return req 36 | } 37 | 38 | // Tuples sets the tuples for the ReplaceManyRequest request. 39 | // Note: default value is nil. 40 | func (req ReplaceManyRequest) Tuples(tuples Tuples) ReplaceManyRequest { 41 | req.tuples = tuples 42 | return req 43 | } 44 | 45 | // Opts sets the options for the ReplaceManyRequest request. 46 | // Note: default value is nil. 47 | func (req ReplaceManyRequest) Opts(opts ReplaceManyOpts) ReplaceManyRequest { 48 | req.opts = opts 49 | return req 50 | } 51 | 52 | // Body fills an encoder with the call request body. 53 | func (req ReplaceManyRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 54 | if req.tuples == nil { 55 | req.tuples = []Tuple{} 56 | } 57 | args := replaceManyArgs{Space: req.space, Tuples: req.tuples, Opts: req.opts} 58 | req.impl = req.impl.Args(args) 59 | return req.impl.Body(res, enc) 60 | } 61 | 62 | // Context sets a passed context to CRUD request. 63 | func (req ReplaceManyRequest) Context(ctx context.Context) ReplaceManyRequest { 64 | req.impl = req.impl.Context(ctx) 65 | 66 | return req 67 | } 68 | 69 | // ReplaceObjectManyOpts describes options for `crud.replace_object_many` method. 70 | type ReplaceObjectManyOpts = OperationObjectManyOpts 71 | 72 | // ReplaceObjectManyRequest helps you to create request object to call 73 | // `crud.replace_object_many` for execution by a Connection. 74 | type ReplaceObjectManyRequest struct { 75 | spaceRequest 76 | objects Objects 77 | opts ReplaceObjectManyOpts 78 | } 79 | 80 | type replaceObjectManyArgs struct { 81 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 82 | Space string 83 | Objects Objects 84 | Opts ReplaceObjectManyOpts 85 | } 86 | 87 | // MakeReplaceObjectManyRequest returns a new empty ReplaceObjectManyRequest. 88 | func MakeReplaceObjectManyRequest(space string) ReplaceObjectManyRequest { 89 | req := ReplaceObjectManyRequest{} 90 | req.impl = newCall("crud.replace_object_many") 91 | req.space = space 92 | req.opts = ReplaceObjectManyOpts{} 93 | return req 94 | } 95 | 96 | // Objects sets the tuple for the ReplaceObjectManyRequest request. 97 | // Note: default value is nil. 98 | func (req ReplaceObjectManyRequest) Objects(objects Objects) ReplaceObjectManyRequest { 99 | req.objects = objects 100 | return req 101 | } 102 | 103 | // Opts sets the options for the ReplaceObjectManyRequest request. 104 | // Note: default value is nil. 105 | func (req ReplaceObjectManyRequest) Opts(opts ReplaceObjectManyOpts) ReplaceObjectManyRequest { 106 | req.opts = opts 107 | return req 108 | } 109 | 110 | // Body fills an encoder with the call request body. 111 | func (req ReplaceObjectManyRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 112 | if req.objects == nil { 113 | req.objects = []Object{} 114 | } 115 | args := replaceObjectManyArgs{Space: req.space, Objects: req.objects, Opts: req.opts} 116 | req.impl = req.impl.Args(args) 117 | return req.impl.Body(res, enc) 118 | } 119 | 120 | // Context sets a passed context to CRUD request. 121 | func (req ReplaceObjectManyRequest) Context(ctx context.Context) ReplaceObjectManyRequest { 122 | req.impl = req.impl.Context(ctx) 123 | 124 | return req 125 | } 126 | -------------------------------------------------------------------------------- /crud/result_test.go: -------------------------------------------------------------------------------- 1 | package crud_test 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | "github.com/tarantool/go-tarantool/v2/crud" 10 | "github.com/vmihailenco/msgpack/v5" 11 | ) 12 | 13 | func TestResult_DecodeMsgpack(t *testing.T) { 14 | sampleCrudResponse := []interface{}{ 15 | map[string]interface{}{ 16 | "rows": []interface{}{"1", "2", "3"}, 17 | }, 18 | nil, 19 | } 20 | responses := []interface{}{sampleCrudResponse, sampleCrudResponse} 21 | 22 | b := bytes.NewBuffer([]byte{}) 23 | enc := msgpack.NewEncoder(b) 24 | err := enc.Encode(responses) 25 | require.NoError(t, err) 26 | 27 | var results []crud.Result 28 | decoder := msgpack.NewDecoder(b) 29 | err = decoder.DecodeValue(reflect.ValueOf(&results)) 30 | require.NoError(t, err) 31 | require.Equal(t, results[0].Rows, []interface{}{"1", "2", "3"}) 32 | require.Equal(t, results[1].Rows, []interface{}{"1", "2", "3"}) 33 | } 34 | -------------------------------------------------------------------------------- /crud/select.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/vmihailenco/msgpack/v5" 7 | 8 | "github.com/tarantool/go-tarantool/v2" 9 | ) 10 | 11 | // SelectOpts describes options for `crud.select` method. 12 | type SelectOpts struct { 13 | // Timeout is a `vshard.call` timeout and vshard 14 | // master discovery timeout (in seconds). 15 | Timeout OptFloat64 16 | // VshardRouter is cartridge vshard group name or 17 | // vshard router instance. 18 | VshardRouter OptString 19 | // Fields is field names for getting only a subset of fields. 20 | Fields OptTuple 21 | // BucketId is a bucket ID. 22 | BucketId OptUint 23 | // Mode is a parameter with `write`/`read` possible values, 24 | // if `write` is specified then operation is performed on master. 25 | Mode OptString 26 | // PreferReplica is a parameter to specify preferred target 27 | // as one of the replicas. 28 | PreferReplica OptBool 29 | // Balance is a parameter to use replica according to vshard 30 | // load balancing policy. 31 | Balance OptBool 32 | // First describes the maximum count of the objects to return. 33 | First OptInt 34 | // After is a tuple after which objects should be selected. 35 | After OptTuple 36 | // BatchSize is a number of tuples to process per one request to storage. 37 | BatchSize OptUint 38 | // ForceMapCall describes the map call is performed without any 39 | // optimizations even if full primary key equal condition is specified. 40 | ForceMapCall OptBool 41 | // Fullscan describes if a critical log entry will be skipped on 42 | // potentially long select. 43 | Fullscan OptBool 44 | // FetchLatestMetadata guarantees the up-to-date metadata (space format) 45 | // in first return value, otherwise it may not take into account 46 | // the latest migration of the data format. Performance overhead is up to 15%. 47 | // Disabled by default. 48 | FetchLatestMetadata OptBool 49 | // YieldEvery describes number of tuples processed to yield after. 50 | // Should be positive. 51 | YieldEvery OptUint 52 | } 53 | 54 | // EncodeMsgpack provides custom msgpack encoder. 55 | func (opts SelectOpts) EncodeMsgpack(enc *msgpack.Encoder) error { 56 | const optsCnt = 14 57 | 58 | names := [optsCnt]string{timeoutOptName, vshardRouterOptName, 59 | fieldsOptName, bucketIdOptName, 60 | modeOptName, preferReplicaOptName, balanceOptName, 61 | firstOptName, afterOptName, batchSizeOptName, 62 | forceMapCallOptName, fullscanOptName, fetchLatestMetadataOptName, 63 | yieldEveryOptName} 64 | values := [optsCnt]interface{}{} 65 | exists := [optsCnt]bool{} 66 | values[0], exists[0] = opts.Timeout.Get() 67 | values[1], exists[1] = opts.VshardRouter.Get() 68 | values[2], exists[2] = opts.Fields.Get() 69 | values[3], exists[3] = opts.BucketId.Get() 70 | values[4], exists[4] = opts.Mode.Get() 71 | values[5], exists[5] = opts.PreferReplica.Get() 72 | values[6], exists[6] = opts.Balance.Get() 73 | values[7], exists[7] = opts.First.Get() 74 | values[8], exists[8] = opts.After.Get() 75 | values[9], exists[9] = opts.BatchSize.Get() 76 | values[10], exists[10] = opts.ForceMapCall.Get() 77 | values[11], exists[11] = opts.Fullscan.Get() 78 | values[12], exists[12] = opts.FetchLatestMetadata.Get() 79 | values[13], exists[13] = opts.YieldEvery.Get() 80 | 81 | return encodeOptions(enc, names[:], values[:], exists[:]) 82 | } 83 | 84 | // SelectRequest helps you to create request object to call `crud.select` 85 | // for execution by a Connection. 86 | type SelectRequest struct { 87 | spaceRequest 88 | conditions []Condition 89 | opts SelectOpts 90 | } 91 | 92 | type selectArgs struct { 93 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 94 | Space string 95 | Conditions []Condition 96 | Opts SelectOpts 97 | } 98 | 99 | // MakeSelectRequest returns a new empty SelectRequest. 100 | func MakeSelectRequest(space string) SelectRequest { 101 | req := SelectRequest{} 102 | req.impl = newCall("crud.select") 103 | req.space = space 104 | req.conditions = nil 105 | req.opts = SelectOpts{} 106 | return req 107 | } 108 | 109 | // Conditions sets the conditions for the SelectRequest request. 110 | // Note: default value is nil. 111 | func (req SelectRequest) Conditions(conditions []Condition) SelectRequest { 112 | req.conditions = conditions 113 | return req 114 | } 115 | 116 | // Opts sets the options for the SelectRequest request. 117 | // Note: default value is nil. 118 | func (req SelectRequest) Opts(opts SelectOpts) SelectRequest { 119 | req.opts = opts 120 | return req 121 | } 122 | 123 | // Body fills an encoder with the call request body. 124 | func (req SelectRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 125 | args := selectArgs{Space: req.space, Conditions: req.conditions, Opts: req.opts} 126 | req.impl = req.impl.Args(args) 127 | return req.impl.Body(res, enc) 128 | } 129 | 130 | // Context sets a passed context to CRUD request. 131 | func (req SelectRequest) Context(ctx context.Context) SelectRequest { 132 | req.impl = req.impl.Context(ctx) 133 | 134 | return req 135 | } 136 | -------------------------------------------------------------------------------- /crud/stats.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/vmihailenco/msgpack/v5" 7 | 8 | "github.com/tarantool/go-tarantool/v2" 9 | ) 10 | 11 | // StatsRequest helps you to create request object to call `crud.stats` 12 | // for execution by a Connection. 13 | type StatsRequest struct { 14 | baseRequest 15 | space OptString 16 | } 17 | 18 | // MakeStatsRequest returns a new empty StatsRequest. 19 | func MakeStatsRequest() StatsRequest { 20 | req := StatsRequest{} 21 | req.impl = newCall("crud.stats") 22 | return req 23 | } 24 | 25 | // Space sets the space name for the StatsRequest request. 26 | // Note: default value is nil. 27 | func (req StatsRequest) Space(space string) StatsRequest { 28 | req.space = MakeOptString(space) 29 | return req 30 | } 31 | 32 | // Body fills an encoder with the call request body. 33 | func (req StatsRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 34 | if value, ok := req.space.Get(); ok { 35 | req.impl = req.impl.Args([]interface{}{value}) 36 | } else { 37 | req.impl = req.impl.Args([]interface{}{}) 38 | } 39 | 40 | return req.impl.Body(res, enc) 41 | } 42 | 43 | // Context sets a passed context to CRUD request. 44 | func (req StatsRequest) Context(ctx context.Context) StatsRequest { 45 | req.impl = req.impl.Context(ctx) 46 | 47 | return req 48 | } 49 | -------------------------------------------------------------------------------- /crud/storage_info.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/vmihailenco/msgpack/v5" 7 | 8 | "github.com/tarantool/go-tarantool/v2" 9 | ) 10 | 11 | // StatusTable describes information for instance. 12 | type StatusTable struct { 13 | Status string 14 | IsMaster bool 15 | Message string 16 | } 17 | 18 | // DecodeMsgpack provides custom msgpack decoder. 19 | func (statusTable *StatusTable) DecodeMsgpack(d *msgpack.Decoder) error { 20 | l, err := d.DecodeMapLen() 21 | if err != nil { 22 | return err 23 | } 24 | for i := 0; i < l; i++ { 25 | key, err := d.DecodeString() 26 | if err != nil { 27 | return err 28 | } 29 | 30 | switch key { 31 | case "status": 32 | if statusTable.Status, err = d.DecodeString(); err != nil { 33 | return err 34 | } 35 | case "is_master": 36 | if statusTable.IsMaster, err = d.DecodeBool(); err != nil { 37 | return err 38 | } 39 | case "message": 40 | if statusTable.Message, err = d.DecodeString(); err != nil { 41 | return err 42 | } 43 | default: 44 | if err := d.Skip(); err != nil { 45 | return err 46 | } 47 | } 48 | } 49 | 50 | return nil 51 | } 52 | 53 | // StorageInfoResult describes result for `crud.storage_info` method. 54 | type StorageInfoResult struct { 55 | Info map[string]StatusTable 56 | } 57 | 58 | // DecodeMsgpack provides custom msgpack decoder. 59 | func (r *StorageInfoResult) DecodeMsgpack(d *msgpack.Decoder) error { 60 | _, err := d.DecodeArrayLen() 61 | if err != nil { 62 | return err 63 | } 64 | 65 | l, err := d.DecodeMapLen() 66 | if err != nil { 67 | return err 68 | } 69 | 70 | info := make(map[string]StatusTable) 71 | for i := 0; i < l; i++ { 72 | key, err := d.DecodeString() 73 | if err != nil { 74 | return err 75 | } 76 | 77 | statusTable := StatusTable{} 78 | if err := d.Decode(&statusTable); err != nil { 79 | return nil 80 | } 81 | 82 | info[key] = statusTable 83 | } 84 | 85 | r.Info = info 86 | 87 | return nil 88 | } 89 | 90 | // StorageInfoOpts describes options for `crud.storage_info` method. 91 | type StorageInfoOpts = BaseOpts 92 | 93 | // StorageInfoRequest helps you to create request object to call 94 | // `crud.storage_info` for execution by a Connection. 95 | type StorageInfoRequest struct { 96 | baseRequest 97 | opts StorageInfoOpts 98 | } 99 | 100 | type storageInfoArgs struct { 101 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 102 | Opts StorageInfoOpts 103 | } 104 | 105 | // MakeStorageInfoRequest returns a new empty StorageInfoRequest. 106 | func MakeStorageInfoRequest() StorageInfoRequest { 107 | req := StorageInfoRequest{} 108 | req.impl = newCall("crud.storage_info") 109 | req.opts = StorageInfoOpts{} 110 | return req 111 | } 112 | 113 | // Opts sets the options for the torageInfoRequest request. 114 | // Note: default value is nil. 115 | func (req StorageInfoRequest) Opts(opts StorageInfoOpts) StorageInfoRequest { 116 | req.opts = opts 117 | return req 118 | } 119 | 120 | // Body fills an encoder with the call request body. 121 | func (req StorageInfoRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 122 | args := storageInfoArgs{Opts: req.opts} 123 | req.impl = req.impl.Args(args) 124 | return req.impl.Body(res, enc) 125 | } 126 | 127 | // Context sets a passed context to CRUD request. 128 | func (req StorageInfoRequest) Context(ctx context.Context) StorageInfoRequest { 129 | req.impl = req.impl.Context(ctx) 130 | 131 | return req 132 | } 133 | -------------------------------------------------------------------------------- /crud/testdata/config.lua: -------------------------------------------------------------------------------- 1 | -- configure path so that you can run application 2 | -- from outside the root directory 3 | if package.setsearchroot ~= nil then 4 | package.setsearchroot() 5 | else 6 | -- Workaround for rocks loading in tarantool 1.10 7 | -- It can be removed in tarantool > 2.2 8 | -- By default, when you do require('mymodule'), tarantool looks into 9 | -- the current working directory and whatever is specified in 10 | -- package.path and package.cpath. If you run your app while in the 11 | -- root directory of that app, everything goes fine, but if you try to 12 | -- start your app with "tarantool myapp/init.lua", it will fail to load 13 | -- its modules, and modules from myapp/.rocks. 14 | local fio = require('fio') 15 | local app_dir = fio.abspath(fio.dirname(arg[0])) 16 | package.path = app_dir .. '/?.lua;' .. package.path 17 | package.path = app_dir .. '/?/init.lua;' .. package.path 18 | package.path = app_dir .. '/.rocks/share/tarantool/?.lua;' .. package.path 19 | package.path = app_dir .. '/.rocks/share/tarantool/?/init.lua;' .. package.path 20 | package.cpath = app_dir .. '/?.so;' .. package.cpath 21 | package.cpath = app_dir .. '/?.dylib;' .. package.cpath 22 | package.cpath = app_dir .. '/.rocks/lib/tarantool/?.so;' .. package.cpath 23 | package.cpath = app_dir .. '/.rocks/lib/tarantool/?.dylib;' .. package.cpath 24 | end 25 | 26 | local crud = require('crud') 27 | local vshard = require('vshard') 28 | 29 | -- Do not set listen for now so connector won't be 30 | -- able to send requests until everything is configured. 31 | box.cfg{ 32 | work_dir = os.getenv("TEST_TNT_WORK_DIR"), 33 | } 34 | 35 | box.schema.user.grant( 36 | 'guest', 37 | 'read,write,execute', 38 | 'universe' 39 | ) 40 | 41 | local s = box.schema.space.create('test', { 42 | id = 617, 43 | if_not_exists = true, 44 | format = { 45 | {name = 'id', type = 'unsigned'}, 46 | {name = 'bucket_id', type = 'unsigned', is_nullable = true}, 47 | {name = 'name', type = 'string'}, 48 | } 49 | }) 50 | s:create_index('primary_index', { 51 | parts = { 52 | {field = 1, type = 'unsigned'}, 53 | }, 54 | }) 55 | s:create_index('bucket_id', { 56 | parts = { 57 | {field = 2, type = 'unsigned'}, 58 | }, 59 | unique = false, 60 | }) 61 | 62 | local function is_ready_false() 63 | return false 64 | end 65 | 66 | local function is_ready_true() 67 | return true 68 | end 69 | 70 | rawset(_G, 'is_ready', is_ready_false) 71 | 72 | -- Setup vshard. 73 | _G.vshard = vshard 74 | box.once('guest', function() 75 | box.schema.user.grant('guest', 'super') 76 | end) 77 | local uri = 'guest@127.0.0.1:3013' 78 | local box_info = box.info() 79 | 80 | local replicaset_uuid 81 | if box_info.replicaset then 82 | -- Since Tarantool 3.0. 83 | replicaset_uuid = box_info.replicaset.uuid 84 | else 85 | replicaset_uuid = box_info.cluster.uuid 86 | end 87 | 88 | local cfg = { 89 | bucket_count = 300, 90 | sharding = { 91 | [replicaset_uuid] = { 92 | replicas = { 93 | [box_info.uuid] = { 94 | uri = uri, 95 | name = 'storage', 96 | master = true, 97 | }, 98 | }, 99 | }, 100 | }, 101 | } 102 | vshard.storage.cfg(cfg, box_info.uuid) 103 | vshard.router.cfg(cfg) 104 | vshard.router.bootstrap() 105 | 106 | -- Initialize crud. 107 | crud.init_storage() 108 | crud.init_router() 109 | crud.cfg{stats = true} 110 | 111 | box.schema.user.create('test', { password = 'test' , if_not_exists = true }) 112 | box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true }) 113 | box.schema.user.grant('test', 'create,read,write,drop,alter', 'space', nil, { if_not_exists = true }) 114 | box.schema.user.grant('test', 'create', 'sequence', nil, { if_not_exists = true }) 115 | 116 | -- Set is_ready = is_ready_true only when every other thing is configured. 117 | rawset(_G, 'is_ready', is_ready_true) 118 | -------------------------------------------------------------------------------- /crud/truncate.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/vmihailenco/msgpack/v5" 7 | 8 | "github.com/tarantool/go-tarantool/v2" 9 | ) 10 | 11 | // TruncateResult describes result for `crud.truncate` method. 12 | type TruncateResult = BoolResult 13 | 14 | // TruncateOpts describes options for `crud.truncate` method. 15 | type TruncateOpts = BaseOpts 16 | 17 | // TruncateRequest helps you to create request object to call `crud.truncate` 18 | // for execution by a Connection. 19 | type TruncateRequest struct { 20 | spaceRequest 21 | opts TruncateOpts 22 | } 23 | 24 | type truncateArgs struct { 25 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 26 | Space string 27 | Opts TruncateOpts 28 | } 29 | 30 | // MakeTruncateRequest returns a new empty TruncateRequest. 31 | func MakeTruncateRequest(space string) TruncateRequest { 32 | req := TruncateRequest{} 33 | req.impl = newCall("crud.truncate") 34 | req.space = space 35 | req.opts = TruncateOpts{} 36 | return req 37 | } 38 | 39 | // Opts sets the options for the TruncateRequest request. 40 | // Note: default value is nil. 41 | func (req TruncateRequest) Opts(opts TruncateOpts) TruncateRequest { 42 | req.opts = opts 43 | return req 44 | } 45 | 46 | // Body fills an encoder with the call request body. 47 | func (req TruncateRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 48 | args := truncateArgs{Space: req.space, Opts: req.opts} 49 | req.impl = req.impl.Args(args) 50 | return req.impl.Body(res, enc) 51 | } 52 | 53 | // Context sets a passed context to CRUD request. 54 | func (req TruncateRequest) Context(ctx context.Context) TruncateRequest { 55 | req.impl = req.impl.Context(ctx) 56 | 57 | return req 58 | } 59 | -------------------------------------------------------------------------------- /crud/tuple.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | // Tuple is a type to describe tuple for CRUD methods. It can be any type that 4 | // msgpask can encode as an array. 5 | type Tuple = interface{} 6 | 7 | // Tuples is a type to describe an array of tuples for CRUD methods. It can be 8 | // any type that msgpack can encode, but encoded data must be an array of 9 | // tuples. 10 | // 11 | // See the reason why not just []Tuple: 12 | // https://github.com/tarantool/go-tarantool/issues/365 13 | type Tuples = interface{} 14 | -------------------------------------------------------------------------------- /crud/unflatten_rows.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // UnflattenRows can be used to convert received tuples to objects. 8 | func UnflattenRows(tuples []interface{}, format []interface{}) ([]MapObject, error) { 9 | var ( 10 | ok bool 11 | fieldName string 12 | fieldInfo map[interface{}]interface{} 13 | ) 14 | 15 | objects := []MapObject{} 16 | 17 | for _, tuple := range tuples { 18 | object := make(map[string]interface{}) 19 | for fieldIdx, field := range tuple.([]interface{}) { 20 | if fieldInfo, ok = format[fieldIdx].(map[interface{}]interface{}); !ok { 21 | return nil, fmt.Errorf("unexpected space format: %q", format) 22 | } 23 | 24 | if fieldName, ok = fieldInfo["name"].(string); !ok { 25 | return nil, fmt.Errorf("unexpected space format: %q", format) 26 | } 27 | 28 | object[fieldName] = field 29 | } 30 | 31 | objects = append(objects, object) 32 | } 33 | 34 | return objects, nil 35 | } 36 | -------------------------------------------------------------------------------- /crud/update.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/vmihailenco/msgpack/v5" 7 | 8 | "github.com/tarantool/go-tarantool/v2" 9 | ) 10 | 11 | // UpdateOpts describes options for `crud.update` method. 12 | type UpdateOpts = SimpleOperationOpts 13 | 14 | // UpdateRequest helps you to create request object to call `crud.update` 15 | // for execution by a Connection. 16 | type UpdateRequest struct { 17 | spaceRequest 18 | key Tuple 19 | operations []Operation 20 | opts UpdateOpts 21 | } 22 | 23 | type updateArgs struct { 24 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 25 | Space string 26 | Key Tuple 27 | Operations []Operation 28 | Opts UpdateOpts 29 | } 30 | 31 | // MakeUpdateRequest returns a new empty UpdateRequest. 32 | func MakeUpdateRequest(space string) UpdateRequest { 33 | req := UpdateRequest{} 34 | req.impl = newCall("crud.update") 35 | req.space = space 36 | req.operations = []Operation{} 37 | req.opts = UpdateOpts{} 38 | return req 39 | } 40 | 41 | // Key sets the key for the UpdateRequest request. 42 | // Note: default value is nil. 43 | func (req UpdateRequest) Key(key Tuple) UpdateRequest { 44 | req.key = key 45 | return req 46 | } 47 | 48 | // Operations sets the operations for UpdateRequest request. 49 | // Note: default value is nil. 50 | func (req UpdateRequest) Operations(operations []Operation) UpdateRequest { 51 | req.operations = operations 52 | return req 53 | } 54 | 55 | // Opts sets the options for the UpdateRequest request. 56 | // Note: default value is nil. 57 | func (req UpdateRequest) Opts(opts UpdateOpts) UpdateRequest { 58 | req.opts = opts 59 | return req 60 | } 61 | 62 | // Body fills an encoder with the call request body. 63 | func (req UpdateRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 64 | if req.key == nil { 65 | req.key = []interface{}{} 66 | } 67 | args := updateArgs{Space: req.space, Key: req.key, 68 | Operations: req.operations, Opts: req.opts} 69 | req.impl = req.impl.Args(args) 70 | return req.impl.Body(res, enc) 71 | } 72 | 73 | // Context sets a passed context to CRUD request. 74 | func (req UpdateRequest) Context(ctx context.Context) UpdateRequest { 75 | req.impl = req.impl.Context(ctx) 76 | 77 | return req 78 | } 79 | -------------------------------------------------------------------------------- /crud/upsert.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/vmihailenco/msgpack/v5" 7 | 8 | "github.com/tarantool/go-tarantool/v2" 9 | ) 10 | 11 | // UpsertOpts describes options for `crud.upsert` method. 12 | type UpsertOpts = SimpleOperationOpts 13 | 14 | // UpsertRequest helps you to create request object to call `crud.upsert` 15 | // for execution by a Connection. 16 | type UpsertRequest struct { 17 | spaceRequest 18 | tuple Tuple 19 | operations []Operation 20 | opts UpsertOpts 21 | } 22 | 23 | type upsertArgs struct { 24 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 25 | Space string 26 | Tuple Tuple 27 | Operations []Operation 28 | Opts UpsertOpts 29 | } 30 | 31 | // MakeUpsertRequest returns a new empty UpsertRequest. 32 | func MakeUpsertRequest(space string) UpsertRequest { 33 | req := UpsertRequest{} 34 | req.impl = newCall("crud.upsert") 35 | req.space = space 36 | req.operations = []Operation{} 37 | req.opts = UpsertOpts{} 38 | return req 39 | } 40 | 41 | // Tuple sets the tuple for the UpsertRequest request. 42 | // Note: default value is nil. 43 | func (req UpsertRequest) Tuple(tuple Tuple) UpsertRequest { 44 | req.tuple = tuple 45 | return req 46 | } 47 | 48 | // Operations sets the operations for the UpsertRequest request. 49 | // Note: default value is nil. 50 | func (req UpsertRequest) Operations(operations []Operation) UpsertRequest { 51 | req.operations = operations 52 | return req 53 | } 54 | 55 | // Opts sets the options for the UpsertRequest request. 56 | // Note: default value is nil. 57 | func (req UpsertRequest) Opts(opts UpsertOpts) UpsertRequest { 58 | req.opts = opts 59 | return req 60 | } 61 | 62 | // Body fills an encoder with the call request body. 63 | func (req UpsertRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 64 | if req.tuple == nil { 65 | req.tuple = []interface{}{} 66 | } 67 | args := upsertArgs{Space: req.space, Tuple: req.tuple, 68 | Operations: req.operations, Opts: req.opts} 69 | req.impl = req.impl.Args(args) 70 | return req.impl.Body(res, enc) 71 | } 72 | 73 | // Context sets a passed context to CRUD request. 74 | func (req UpsertRequest) Context(ctx context.Context) UpsertRequest { 75 | req.impl = req.impl.Context(ctx) 76 | 77 | return req 78 | } 79 | 80 | // UpsertObjectOpts describes options for `crud.upsert_object` method. 81 | type UpsertObjectOpts = SimpleOperationOpts 82 | 83 | // UpsertObjectRequest helps you to create request object to call 84 | // `crud.upsert_object` for execution by a Connection. 85 | type UpsertObjectRequest struct { 86 | spaceRequest 87 | object Object 88 | operations []Operation 89 | opts UpsertObjectOpts 90 | } 91 | 92 | type upsertObjectArgs struct { 93 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 94 | Space string 95 | Object Object 96 | Operations []Operation 97 | Opts UpsertObjectOpts 98 | } 99 | 100 | // MakeUpsertObjectRequest returns a new empty UpsertObjectRequest. 101 | func MakeUpsertObjectRequest(space string) UpsertObjectRequest { 102 | req := UpsertObjectRequest{} 103 | req.impl = newCall("crud.upsert_object") 104 | req.space = space 105 | req.operations = []Operation{} 106 | req.opts = UpsertObjectOpts{} 107 | return req 108 | } 109 | 110 | // Object sets the tuple for the UpsertObjectRequest request. 111 | // Note: default value is nil. 112 | func (req UpsertObjectRequest) Object(object Object) UpsertObjectRequest { 113 | req.object = object 114 | return req 115 | } 116 | 117 | // Operations sets the operations for the UpsertObjectRequest request. 118 | // Note: default value is nil. 119 | func (req UpsertObjectRequest) Operations(operations []Operation) UpsertObjectRequest { 120 | req.operations = operations 121 | return req 122 | } 123 | 124 | // Opts sets the options for the UpsertObjectRequest request. 125 | // Note: default value is nil. 126 | func (req UpsertObjectRequest) Opts(opts UpsertObjectOpts) UpsertObjectRequest { 127 | req.opts = opts 128 | return req 129 | } 130 | 131 | // Body fills an encoder with the call request body. 132 | func (req UpsertObjectRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { 133 | if req.object == nil { 134 | req.object = MapObject{} 135 | } 136 | args := upsertObjectArgs{Space: req.space, Object: req.object, 137 | Operations: req.operations, Opts: req.opts} 138 | req.impl = req.impl.Args(args) 139 | return req.impl.Body(res, enc) 140 | } 141 | 142 | // Context sets a passed context to CRUD request. 143 | func (req UpsertObjectRequest) Context(ctx context.Context) UpsertObjectRequest { 144 | req.impl = req.impl.Context(ctx) 145 | 146 | return req 147 | } 148 | -------------------------------------------------------------------------------- /datetime/adjust.go: -------------------------------------------------------------------------------- 1 | package datetime 2 | 3 | // An Adjust is used as a parameter for date adjustions, see: 4 | // https://github.com/tarantool/tarantool/wiki/Datetime-Internals#date-adjustions-and-leap-years 5 | type Adjust int 6 | 7 | const ( 8 | NoneAdjust Adjust = 0 // adjust = "none" in Tarantool 9 | ExcessAdjust Adjust = 1 // adjust = "excess" in Tarantool 10 | LastAdjust Adjust = 2 // adjust = "last" in Tarantool 11 | ) 12 | 13 | // We need the mappings to make NoneAdjust as a default value instead of 14 | // dtExcess. 15 | const ( 16 | dtExcess = 0 // DT_EXCESS from dt-c/dt_arithmetic.h 17 | dtLimit = 1 // DT_LIMIT 18 | dtSnap = 2 // DT_SNAP 19 | ) 20 | 21 | var adjustToDt = map[Adjust]int64{ 22 | NoneAdjust: dtLimit, 23 | ExcessAdjust: dtExcess, 24 | LastAdjust: dtSnap, 25 | } 26 | 27 | var dtToAdjust = map[int64]Adjust{ 28 | dtExcess: ExcessAdjust, 29 | dtLimit: NoneAdjust, 30 | dtSnap: LastAdjust, 31 | } 32 | -------------------------------------------------------------------------------- /datetime/config.lua: -------------------------------------------------------------------------------- 1 | local has_datetime, datetime = pcall(require, 'datetime') 2 | 3 | if not has_datetime then 4 | error('Datetime unsupported, use Tarantool 2.10 or newer') 5 | end 6 | 7 | -- Do not set listen for now so connector won't be 8 | -- able to send requests until everything is configured. 9 | box.cfg{ 10 | work_dir = os.getenv("TEST_TNT_WORK_DIR"), 11 | } 12 | 13 | box.schema.user.create('test', { password = 'test' , if_not_exists = true }) 14 | box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true }) 15 | 16 | box.once("init", function() 17 | local s_1 = box.schema.space.create('testDatetime_1', { 18 | id = 524, 19 | if_not_exists = true, 20 | }) 21 | s_1:create_index('primary', { 22 | type = 'TREE', 23 | parts = { 24 | { field = 1, type = 'datetime' }, 25 | }, 26 | if_not_exists = true 27 | }) 28 | s_1:truncate() 29 | 30 | local s_3 = box.schema.space.create('testDatetime_2', { 31 | id = 526, 32 | if_not_exists = true, 33 | }) 34 | s_3:create_index('primary', { 35 | type = 'tree', 36 | parts = { 37 | {1, 'uint'}, 38 | }, 39 | if_not_exists = true 40 | }) 41 | s_3:truncate() 42 | 43 | box.schema.func.create('call_datetime_testdata') 44 | box.schema.user.grant('test', 'read,write', 'space', 'testDatetime_1', { if_not_exists = true }) 45 | box.schema.user.grant('test', 'read,write', 'space', 'testDatetime_2', { if_not_exists = true }) 46 | end) 47 | 48 | local function call_datetime_testdata() 49 | local dt1 = datetime.new({ year = 1934 }) 50 | local dt2 = datetime.new({ year = 1961 }) 51 | local dt3 = datetime.new({ year = 1968 }) 52 | return { 53 | { 54 | 5, "Go!", { 55 | {"Klushino", dt1}, 56 | {"Baikonur", dt2}, 57 | {"Novoselovo", dt3}, 58 | }, 59 | } 60 | } 61 | end 62 | rawset(_G, 'call_datetime_testdata', call_datetime_testdata) 63 | 64 | local function call_interval_testdata(interval) 65 | return interval 66 | end 67 | rawset(_G, 'call_interval_testdata', call_interval_testdata) 68 | 69 | local function call_datetime_interval(dtleft, dtright) 70 | return dtright - dtleft 71 | end 72 | rawset(_G, 'call_datetime_interval', call_datetime_interval) 73 | 74 | -- Set listen only when every other thing is configured. 75 | box.cfg{ 76 | listen = os.getenv("TEST_TNT_LISTEN"), 77 | } 78 | 79 | require('console').start() 80 | -------------------------------------------------------------------------------- /datetime/export_test.go: -------------------------------------------------------------------------------- 1 | package datetime 2 | 3 | /* It's kind of an integration test data from an external data source. */ 4 | var IndexToTimezone = indexToTimezone 5 | var TimezoneToIndex = timezoneToIndex 6 | -------------------------------------------------------------------------------- /datetime/gen-timezones.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xeuo pipefail 3 | 4 | SRC_COMMIT="9ee45289e01232b8df1413efea11db170ae3b3b4" 5 | SRC_FILE=timezones.h 6 | DST_FILE=timezones.go 7 | 8 | [ -e ${SRC_FILE} ] && rm ${SRC_FILE} 9 | wget -O ${SRC_FILE} \ 10 | https://raw.githubusercontent.com/tarantool/tarantool/${SRC_COMMIT}/src/lib/tzcode/timezones.h 11 | 12 | # We don't need aliases in indexToTimezone because Tarantool always replace it: 13 | # 14 | # tarantool> T = date.parse '2022-01-01T00:00 Pacific/Enderbury' 15 | # --- 16 | # ... 17 | # tarantool> T 18 | # --- 19 | # - 2022-01-01T00:00:00 Pacific/Kanton 20 | # ... 21 | # 22 | # So we can do the same and don't worry, be happy. 23 | 24 | cat < ${DST_FILE} 25 | package datetime 26 | 27 | /* Automatically generated by gen-timezones.sh */ 28 | 29 | var indexToTimezone = map[int]string{ 30 | EOF 31 | 32 | grep ZONE_ABBREV ${SRC_FILE} | sed "s/ZONE_ABBREV( *//g" | sed "s/[),]//g" \ 33 | | awk '{printf("\t%s : %s,\n", $1, $3)}' >> ${DST_FILE} 34 | grep ZONE_UNIQUE ${SRC_FILE} | sed "s/ZONE_UNIQUE( *//g" | sed "s/[),]//g" \ 35 | | awk '{printf("\t%s : %s,\n", $1, $2)}' >> ${DST_FILE} 36 | 37 | cat <> ${DST_FILE} 38 | } 39 | 40 | var timezoneToIndex = map[string]int{ 41 | EOF 42 | 43 | grep ZONE_ABBREV ${SRC_FILE} | sed "s/ZONE_ABBREV( *//g" | sed "s/[),]//g" \ 44 | | awk '{printf("\t%s : %s,\n", $3, $1)}' >> ${DST_FILE} 45 | grep ZONE_UNIQUE ${SRC_FILE} | sed "s/ZONE_UNIQUE( *//g" | sed "s/[),]//g" \ 46 | | awk '{printf("\t%s : %s,\n", $2, $1)}' >> ${DST_FILE} 47 | grep ZONE_ALIAS ${SRC_FILE} | sed "s/ZONE_ALIAS( *//g" | sed "s/[),]//g" \ 48 | | awk '{printf("\t%s : %s,\n", $2, $1)}' >> ${DST_FILE} 49 | 50 | echo "}" >> ${DST_FILE} 51 | 52 | rm timezones.h 53 | 54 | gofmt -s -w ${DST_FILE} 55 | -------------------------------------------------------------------------------- /datetime/interval_test.go: -------------------------------------------------------------------------------- 1 | package datetime_test 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/tarantool/go-tarantool/v2" 9 | . "github.com/tarantool/go-tarantool/v2/datetime" 10 | "github.com/tarantool/go-tarantool/v2/test_helpers" 11 | ) 12 | 13 | func TestIntervalAdd(t *testing.T) { 14 | orig := Interval{ 15 | Year: 1, 16 | Month: 2, 17 | Week: 3, 18 | Day: 4, 19 | Hour: -5, 20 | Min: 6, 21 | Sec: -7, 22 | Nsec: 8, 23 | Adjust: LastAdjust, 24 | } 25 | cpyOrig := orig 26 | add := Interval{ 27 | Year: 2, 28 | Month: 3, 29 | Week: -4, 30 | Day: 5, 31 | Hour: -6, 32 | Min: 7, 33 | Sec: -8, 34 | Nsec: 0, 35 | Adjust: ExcessAdjust, 36 | } 37 | expected := Interval{ 38 | Year: orig.Year + add.Year, 39 | Month: orig.Month + add.Month, 40 | Week: orig.Week + add.Week, 41 | Day: orig.Day + add.Day, 42 | Hour: orig.Hour + add.Hour, 43 | Min: orig.Min + add.Min, 44 | Sec: orig.Sec + add.Sec, 45 | Nsec: orig.Nsec + add.Nsec, 46 | Adjust: orig.Adjust, 47 | } 48 | 49 | ival := orig.Add(add) 50 | 51 | if !reflect.DeepEqual(ival, expected) { 52 | t.Fatalf("Unexpected %v, expected %v", ival, expected) 53 | } 54 | if !reflect.DeepEqual(cpyOrig, orig) { 55 | t.Fatalf("Original value changed %v, expected %v", orig, cpyOrig) 56 | } 57 | } 58 | 59 | func TestIntervalSub(t *testing.T) { 60 | orig := Interval{ 61 | Year: 1, 62 | Month: 2, 63 | Week: 3, 64 | Day: 4, 65 | Hour: -5, 66 | Min: 6, 67 | Sec: -7, 68 | Nsec: 8, 69 | Adjust: LastAdjust, 70 | } 71 | cpyOrig := orig 72 | sub := Interval{ 73 | Year: 2, 74 | Month: 3, 75 | Week: -4, 76 | Day: 5, 77 | Hour: -6, 78 | Min: 7, 79 | Sec: -8, 80 | Nsec: 0, 81 | Adjust: ExcessAdjust, 82 | } 83 | expected := Interval{ 84 | Year: orig.Year - sub.Year, 85 | Month: orig.Month - sub.Month, 86 | Week: orig.Week - sub.Week, 87 | Day: orig.Day - sub.Day, 88 | Hour: orig.Hour - sub.Hour, 89 | Min: orig.Min - sub.Min, 90 | Sec: orig.Sec - sub.Sec, 91 | Nsec: orig.Nsec - sub.Nsec, 92 | Adjust: orig.Adjust, 93 | } 94 | 95 | ival := orig.Sub(sub) 96 | 97 | if !reflect.DeepEqual(ival, expected) { 98 | t.Fatalf("Unexpected %v, expected %v", ival, expected) 99 | } 100 | if !reflect.DeepEqual(cpyOrig, orig) { 101 | t.Fatalf("Original value changed %v, expected %v", orig, cpyOrig) 102 | } 103 | } 104 | 105 | func TestIntervalTarantoolEncoding(t *testing.T) { 106 | skipIfDatetimeUnsupported(t) 107 | 108 | conn := test_helpers.ConnectWithValidation(t, dialer, opts) 109 | defer conn.Close() 110 | 111 | cases := []Interval{ 112 | {}, 113 | {1, 2, 3, 4, -5, 6, -7, 8, LastAdjust}, 114 | {1, 2, 3, 4, -5, 6, -7, 8, ExcessAdjust}, 115 | {1, 2, 3, 4, -5, 6, -7, 8, LastAdjust}, 116 | {0, 2, 3, 4, -5, 0, -7, 8, LastAdjust}, 117 | {0, 0, 3, 0, -5, 6, -7, 8, ExcessAdjust}, 118 | {0, 0, 0, 4, 0, 0, 0, 8, LastAdjust}, 119 | } 120 | for _, tc := range cases { 121 | t.Run(fmt.Sprintf("%v", tc), func(t *testing.T) { 122 | req := tarantool.NewCallRequest("call_interval_testdata"). 123 | Args([]interface{}{tc}) 124 | data, err := conn.Do(req).Get() 125 | if err != nil { 126 | t.Fatalf("Unexpected error: %s", err.Error()) 127 | } 128 | 129 | ret := data[0].(Interval) 130 | if !reflect.DeepEqual(ret, tc) { 131 | t.Fatalf("Unexpected response: %v, expected %v", ret, tc) 132 | } 133 | }) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /deadline_io.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | type deadlineIO struct { 9 | to time.Duration 10 | c net.Conn 11 | } 12 | 13 | func (d *deadlineIO) Write(b []byte) (n int, err error) { 14 | if d.to > 0 { 15 | d.c.SetWriteDeadline(time.Now().Add(d.to)) 16 | } 17 | n, err = d.c.Write(b) 18 | return 19 | } 20 | 21 | func (d *deadlineIO) Read(b []byte) (n int, err error) { 22 | if d.to > 0 { 23 | d.c.SetReadDeadline(time.Now().Add(d.to)) 24 | } 25 | n, err = d.c.Read(b) 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /decimal/config.lua: -------------------------------------------------------------------------------- 1 | local decimal = require('decimal') 2 | local msgpack = require('msgpack') 3 | 4 | -- Do not set listen for now so connector won't be 5 | -- able to send requests until everything is configured. 6 | box.cfg{ 7 | work_dir = os.getenv("TEST_TNT_WORK_DIR"), 8 | } 9 | 10 | box.schema.user.create('test', { password = 'test' , if_not_exists = true }) 11 | box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true }) 12 | 13 | local decimal_msgpack_supported = pcall(msgpack.encode, decimal.new(1)) 14 | if not decimal_msgpack_supported then 15 | error('Decimal unsupported, use Tarantool 2.2 or newer') 16 | end 17 | 18 | local s = box.schema.space.create('testDecimal', { 19 | id = 524, 20 | if_not_exists = true, 21 | }) 22 | s:create_index('primary', { 23 | type = 'TREE', 24 | parts = { 25 | { 26 | field = 1, 27 | type = 'decimal', 28 | }, 29 | }, 30 | if_not_exists = true 31 | }) 32 | s:truncate() 33 | 34 | box.schema.user.grant('test', 'read,write', 'space', 'testDecimal', { if_not_exists = true }) 35 | 36 | -- Set listen only when every other thing is configured. 37 | box.cfg{ 38 | listen = os.getenv("TEST_TNT_LISTEN"), 39 | } 40 | 41 | require('console').start() 42 | -------------------------------------------------------------------------------- /decimal/decimal.go: -------------------------------------------------------------------------------- 1 | // Package decimal with support of Tarantool's decimal data type. 2 | // 3 | // Decimal data type supported in Tarantool since 2.2. 4 | // 5 | // Since: 1.7.0 6 | // 7 | // See also: 8 | // 9 | // - Tarantool MessagePack extensions: 10 | // https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-decimal-type 11 | // 12 | // - Tarantool data model: 13 | // https://www.tarantool.io/en/doc/latest/book/box/data_model/ 14 | // 15 | // - Tarantool issue for support decimal type: 16 | // https://github.com/tarantool/tarantool/issues/692 17 | // 18 | // - Tarantool module decimal: 19 | // https://www.tarantool.io/en/doc/latest/reference/reference_lua/decimal/ 20 | package decimal 21 | 22 | import ( 23 | "fmt" 24 | "reflect" 25 | 26 | "github.com/shopspring/decimal" 27 | "github.com/vmihailenco/msgpack/v5" 28 | ) 29 | 30 | // Decimal numbers have 38 digits of precision, that is, the total 31 | // number of digits before and after the decimal point can be 38. 32 | // A decimal operation will fail if overflow happens (when a number is 33 | // greater than 10^38 - 1 or less than -10^38 - 1). 34 | // 35 | // See also: 36 | // 37 | // - Tarantool module decimal: 38 | // https://www.tarantool.io/en/doc/latest/reference/reference_lua/decimal/ 39 | 40 | const ( 41 | // Decimal external type. 42 | decimalExtID = 1 43 | decimalPrecision = 38 44 | ) 45 | 46 | var ( 47 | one decimal.Decimal = decimal.NewFromInt(1) 48 | // -10^decimalPrecision - 1 49 | minSupportedDecimal decimal.Decimal = maxSupportedDecimal.Neg().Sub(one) 50 | // 10^decimalPrecision - 1 51 | maxSupportedDecimal decimal.Decimal = decimal.New(1, decimalPrecision).Sub(one) 52 | ) 53 | 54 | type Decimal struct { 55 | decimal.Decimal 56 | } 57 | 58 | // MakeDecimal creates a new Decimal from a decimal.Decimal. 59 | func MakeDecimal(decimal decimal.Decimal) Decimal { 60 | return Decimal{Decimal: decimal} 61 | } 62 | 63 | // MakeDecimalFromString creates a new Decimal from a string. 64 | func MakeDecimalFromString(src string) (Decimal, error) { 65 | result := Decimal{} 66 | dec, err := decimal.NewFromString(src) 67 | if err != nil { 68 | return result, err 69 | } 70 | result = MakeDecimal(dec) 71 | return result, nil 72 | } 73 | 74 | func decimalEncoder(e *msgpack.Encoder, v reflect.Value) ([]byte, error) { 75 | dec := v.Interface().(Decimal) 76 | if dec.GreaterThan(maxSupportedDecimal) { 77 | return nil, 78 | fmt.Errorf( 79 | "msgpack: decimal number is bigger than maximum supported number (10^%d - 1)", 80 | decimalPrecision) 81 | } 82 | if dec.LessThan(minSupportedDecimal) { 83 | return nil, 84 | fmt.Errorf( 85 | "msgpack: decimal number is lesser than minimum supported number (-10^%d - 1)", 86 | decimalPrecision) 87 | } 88 | 89 | strBuf := dec.String() 90 | bcdBuf, err := encodeStringToBCD(strBuf) 91 | if err != nil { 92 | return nil, fmt.Errorf("msgpack: can't encode string (%s) to a BCD buffer: %w", strBuf, err) 93 | } 94 | return bcdBuf, nil 95 | } 96 | 97 | func decimalDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { 98 | b := make([]byte, extLen) 99 | n, err := d.Buffered().Read(b) 100 | if err != nil { 101 | return err 102 | } 103 | if n < extLen { 104 | return fmt.Errorf("msgpack: unexpected end of stream after %d decimal bytes", n) 105 | } 106 | 107 | // Decimal values can be encoded to fixext MessagePack, where buffer 108 | // has a fixed length encoded by first byte, and ext MessagePack, where 109 | // buffer length is not fixed and encoded by a number in a separate 110 | // field: 111 | // 112 | // +--------+-------------------+------------+===============+ 113 | // | MP_EXT | length (optional) | MP_DECIMAL | PackedDecimal | 114 | // +--------+-------------------+------------+===============+ 115 | digits, exp, err := decodeStringFromBCD(b) 116 | if err != nil { 117 | return fmt.Errorf("msgpack: can't decode string from BCD buffer (%x): %w", b, err) 118 | } 119 | 120 | dec, err := decimal.NewFromString(digits) 121 | if err != nil { 122 | return fmt.Errorf("msgpack: can't encode string (%s) to a decimal number: %w", digits, err) 123 | } 124 | 125 | if exp != 0 { 126 | dec = dec.Shift(int32(exp)) 127 | } 128 | ptr := v.Addr().Interface().(*Decimal) 129 | *ptr = MakeDecimal(dec) 130 | return nil 131 | } 132 | 133 | func init() { 134 | msgpack.RegisterExtDecoder(decimalExtID, Decimal{}, decimalDecoder) 135 | msgpack.RegisterExtEncoder(decimalExtID, Decimal{}, decimalEncoder) 136 | } 137 | -------------------------------------------------------------------------------- /decimal/example_test.go: -------------------------------------------------------------------------------- 1 | // Run Tarantool instance before example execution: 2 | // 3 | // Terminal 1: 4 | // $ cd decimal 5 | // $ TEST_TNT_LISTEN=3013 TEST_TNT_WORK_DIR=$(mktemp -d -t 'tarantool.XXX') tarantool config.lua 6 | // 7 | // Terminal 2: 8 | // $ go test -v example_test.go 9 | package decimal_test 10 | 11 | import ( 12 | "context" 13 | "log" 14 | "time" 15 | 16 | "github.com/tarantool/go-tarantool/v2" 17 | . "github.com/tarantool/go-tarantool/v2/decimal" 18 | ) 19 | 20 | // To enable support of decimal in msgpack with 21 | // https://github.com/shopspring/decimal, 22 | // import tarantool/decimal submodule. 23 | func Example() { 24 | server := "127.0.0.1:3013" 25 | dialer := tarantool.NetDialer{ 26 | Address: server, 27 | User: "test", 28 | Password: "test", 29 | } 30 | opts := tarantool.Opts{ 31 | Timeout: 5 * time.Second, 32 | } 33 | ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) 34 | client, err := tarantool.Connect(ctx, dialer, opts) 35 | cancel() 36 | if err != nil { 37 | log.Fatalf("Failed to connect: %s", err.Error()) 38 | } 39 | 40 | spaceNo := uint32(524) 41 | 42 | number, err := MakeDecimalFromString("-22.804") 43 | if err != nil { 44 | log.Fatalf("Failed to prepare test decimal: %s", err) 45 | } 46 | 47 | data, err := client.Do(tarantool.NewReplaceRequest(spaceNo). 48 | Tuple([]interface{}{number}), 49 | ).Get() 50 | if err != nil { 51 | log.Fatalf("Decimal replace failed: %s", err) 52 | } 53 | 54 | log.Println("Decimal tuple replace") 55 | log.Println("Error", err) 56 | log.Println("Data", data) 57 | } 58 | -------------------------------------------------------------------------------- /decimal/export_test.go: -------------------------------------------------------------------------------- 1 | package decimal 2 | 3 | func EncodeStringToBCD(buf string) ([]byte, error) { 4 | return encodeStringToBCD(buf) 5 | } 6 | 7 | func DecodeStringFromBCD(bcdBuf []byte) (string, int, error) { 8 | return decodeStringFromBCD(bcdBuf) 9 | } 10 | 11 | func GetNumberLength(buf string) int { 12 | return getNumberLength(buf) 13 | } 14 | 15 | const ( 16 | DecimalPrecision = decimalPrecision 17 | ) 18 | -------------------------------------------------------------------------------- /decimal/fuzzing_test.go: -------------------------------------------------------------------------------- 1 | //go:build go_tarantool_decimal_fuzzing 2 | // +build go_tarantool_decimal_fuzzing 3 | 4 | package decimal_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/shopspring/decimal" 10 | . "github.com/tarantool/go-tarantool/v2/decimal" 11 | ) 12 | 13 | func strToDecimal(t *testing.T, buf string, exp int) decimal.Decimal { 14 | decNum, err := decimal.NewFromString(buf) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | if exp != 0 { 19 | decNum = decNum.Shift(int32(exp)) 20 | } 21 | return decNum 22 | } 23 | 24 | func FuzzEncodeDecodeBCD(f *testing.F) { 25 | samples := append(correctnessSamples, benchmarkSamples...) 26 | for _, testcase := range samples { 27 | if len(testcase.numString) > 0 { 28 | f.Add(testcase.numString) // Use f.Add to provide a seed corpus. 29 | } 30 | } 31 | f.Fuzz(func(t *testing.T, orig string) { 32 | if l := GetNumberLength(orig); l > DecimalPrecision { 33 | t.Skip("max number length is exceeded") 34 | } 35 | bcdBuf, err := EncodeStringToBCD(orig) 36 | if err != nil { 37 | t.Skip("Only correct requests are interesting: %w", err) 38 | } 39 | 40 | dec, exp, err := DecodeStringFromBCD(bcdBuf) 41 | if err != nil { 42 | t.Fatalf("Failed to decode encoded value ('%s')", orig) 43 | } 44 | 45 | if !strToDecimal(t, dec, exp).Equal(strToDecimal(t, orig, 0)) { 46 | t.Fatal("Decimal numbers are not equal") 47 | } 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /decoder.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/vmihailenco/msgpack/v5" 7 | ) 8 | 9 | func untypedMapDecoder(dec *msgpack.Decoder) (interface{}, error) { 10 | return dec.DecodeUntypedMap() 11 | } 12 | 13 | func getDecoder(r io.Reader) *msgpack.Decoder { 14 | d := msgpack.GetDecoder() 15 | 16 | d.Reset(r) 17 | d.SetMapDecoder(untypedMapDecoder) 18 | 19 | return d 20 | } 21 | 22 | func putDecoder(dec *msgpack.Decoder) { 23 | msgpack.PutDecoder(dec) 24 | } 25 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/tarantool/go-iproto" 7 | ) 8 | 9 | // Error is wrapper around error returned by Tarantool. 10 | type Error struct { 11 | Code iproto.Error 12 | Msg string 13 | ExtendedInfo *BoxError 14 | } 15 | 16 | // Error converts an Error to a string. 17 | func (tnterr Error) Error() string { 18 | if tnterr.ExtendedInfo != nil { 19 | return tnterr.ExtendedInfo.Error() 20 | } 21 | 22 | return fmt.Sprintf("%s (0x%x)", tnterr.Msg, tnterr.Code) 23 | } 24 | 25 | // ClientError is connection error produced by this client, 26 | // i.e. connection failures or timeouts. 27 | type ClientError struct { 28 | Code uint32 29 | Msg string 30 | } 31 | 32 | // Error converts a ClientError to a string. 33 | func (clierr ClientError) Error() string { 34 | return fmt.Sprintf("%s (0x%x)", clierr.Msg, clierr.Code) 35 | } 36 | 37 | // Temporary returns true if next attempt to perform request may succeeded. 38 | // 39 | // Currently it returns true when: 40 | // 41 | // - Connection is not connected at the moment 42 | // 43 | // - request is timeouted 44 | // 45 | // - request is aborted due to rate limit 46 | func (clierr ClientError) Temporary() bool { 47 | switch clierr.Code { 48 | case ErrConnectionNotReady, ErrTimeouted, ErrRateLimited, ErrIoError: 49 | return true 50 | default: 51 | return false 52 | } 53 | } 54 | 55 | // Tarantool client error codes. 56 | const ( 57 | ErrConnectionNotReady = 0x4000 + iota 58 | ErrConnectionClosed = 0x4000 + iota 59 | ErrProtocolError = 0x4000 + iota 60 | ErrTimeouted = 0x4000 + iota 61 | ErrRateLimited = 0x4000 + iota 62 | ErrConnectionShutdown = 0x4000 + iota 63 | ErrIoError = 0x4000 + iota 64 | ) 65 | -------------------------------------------------------------------------------- /example_custom_unpacking_test.go: -------------------------------------------------------------------------------- 1 | package tarantool_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/vmihailenco/msgpack/v5" 10 | 11 | "github.com/tarantool/go-tarantool/v2" 12 | ) 13 | 14 | type Tuple2 struct { 15 | Cid uint 16 | Orig string 17 | Members []Member 18 | } 19 | 20 | // Same effect in a "magic" way, but slower. 21 | type Tuple3 struct { 22 | _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused 23 | 24 | Cid uint 25 | Orig string 26 | Members []Member 27 | } 28 | 29 | func (c *Tuple2) EncodeMsgpack(e *msgpack.Encoder) error { 30 | if err := e.EncodeArrayLen(3); err != nil { 31 | return err 32 | } 33 | if err := e.EncodeUint(uint64(c.Cid)); err != nil { 34 | return err 35 | } 36 | if err := e.EncodeString(c.Orig); err != nil { 37 | return err 38 | } 39 | e.Encode(c.Members) 40 | return nil 41 | } 42 | 43 | func (c *Tuple2) DecodeMsgpack(d *msgpack.Decoder) error { 44 | var err error 45 | var l int 46 | if l, err = d.DecodeArrayLen(); err != nil { 47 | return err 48 | } 49 | if l != 3 { 50 | return fmt.Errorf("array len doesn't match: %d", l) 51 | } 52 | if c.Cid, err = d.DecodeUint(); err != nil { 53 | return err 54 | } 55 | if c.Orig, err = d.DecodeString(); err != nil { 56 | return err 57 | } 58 | if l, err = d.DecodeArrayLen(); err != nil { 59 | return err 60 | } 61 | c.Members = make([]Member, l) 62 | for i := 0; i < l; i++ { 63 | d.Decode(&c.Members[i]) 64 | } 65 | return nil 66 | } 67 | 68 | // Example demonstrates how to use custom (un)packing with typed selects and 69 | // function calls. 70 | // 71 | // You can specify user-defined packing/unpacking functions for your types. 72 | // This allows you to store complex structures within a tuple and may speed up 73 | // your requests. 74 | // 75 | // Alternatively, you can just instruct the msgpack library to encode your 76 | // structure as an array. This is safe "magic". It is easier to implement than 77 | // a custom packer/unpacker, but it will work slower. 78 | func Example_customUnpacking() { 79 | // Establish a connection. 80 | 81 | dialer := tarantool.NetDialer{ 82 | Address: "127.0.0.1:3013", 83 | User: "test", 84 | Password: "test", 85 | } 86 | opts := tarantool.Opts{} 87 | ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) 88 | conn, err := tarantool.Connect(ctx, dialer, opts) 89 | cancel() 90 | if err != nil { 91 | log.Fatalf("Failed to connect: %s", err.Error()) 92 | } 93 | 94 | spaceNo := uint32(617) 95 | indexNo := uint32(0) 96 | 97 | tuple := Tuple2{Cid: 777, Orig: "orig", Members: []Member{{"lol", "", 1}, {"wut", "", 3}}} 98 | // Insert a structure itself. 99 | initReq := tarantool.NewReplaceRequest(spaceNo).Tuple(&tuple) 100 | data, err := conn.Do(initReq).Get() 101 | if err != nil { 102 | log.Fatalf("Failed to insert: %s", err.Error()) 103 | return 104 | } 105 | fmt.Println("Data", data) 106 | 107 | var tuples1 []Tuple2 108 | selectReq := tarantool.NewSelectRequest(spaceNo). 109 | Index(indexNo). 110 | Limit(1). 111 | Iterator(tarantool.IterEq). 112 | Key([]interface{}{777}) 113 | err = conn.Do(selectReq).GetTyped(&tuples1) 114 | if err != nil { 115 | log.Fatalf("Failed to SelectTyped: %s", err.Error()) 116 | return 117 | } 118 | fmt.Println("Tuples (tuples1)", tuples1) 119 | 120 | // Same result in a "magic" way. 121 | var tuples2 []Tuple3 122 | err = conn.Do(selectReq).GetTyped(&tuples2) 123 | if err != nil { 124 | log.Fatalf("Failed to SelectTyped: %s", err.Error()) 125 | return 126 | } 127 | fmt.Println("Tuples (tuples2):", tuples2) 128 | 129 | // Call a function "func_name" returning a table of custom tuples. 130 | var tuples3 [][]Tuple3 131 | callReq := tarantool.NewCallRequest("func_name") 132 | err = conn.Do(callReq).GetTyped(&tuples3) 133 | if err != nil { 134 | log.Fatalf("Failed to CallTyped: %s", err.Error()) 135 | return 136 | } 137 | fmt.Println("Tuples (tuples3):", tuples3) 138 | 139 | // Output: 140 | // Data [[777 orig [[lol 1] [wut 3]]]] 141 | // Tuples (tuples1) [{777 orig [{lol 1} {wut 3}]}] 142 | // Tuples (tuples2): [{{} 777 orig [{lol 1} {wut 3}]}] 143 | // Tuples (tuples3): [[{{} 221 [{Moscow 34} {Minsk 23} {Kiev 31}]}]] 144 | 145 | } 146 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tarantool/go-tarantool/v2 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/google/uuid v1.3.0 7 | github.com/shopspring/decimal v1.3.1 8 | github.com/stretchr/testify v1.9.0 9 | github.com/tarantool/go-iproto v1.1.0 10 | github.com/vmihailenco/msgpack/v5 v5.4.1 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/pmezard/go-difflib v1.0.0 // indirect 16 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 17 | gopkg.in/yaml.v3 v3.0.1 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 4 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= 8 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 9 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 10 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 11 | github.com/tarantool/go-iproto v1.1.0 h1:HULVOIHsiehI+FnHfM7wMDntuzUddO09DKqu2WnFQ5A= 12 | github.com/tarantool/go-iproto v1.1.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= 13 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 14 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 15 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 16 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 19 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 20 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 21 | -------------------------------------------------------------------------------- /header.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import "github.com/tarantool/go-iproto" 4 | 5 | // Header is a response header. 6 | type Header struct { 7 | // RequestId is an id of a corresponding request. 8 | RequestId uint32 9 | // Error is a response error. It could be used 10 | // to check that response has or hasn't an error without decoding. 11 | // Error == ErrorNo (iproto.ER_UNKNOWN) if there is no error. 12 | // Otherwise, it contains an error code from iproto.Error enumeration. 13 | Error iproto.Error 14 | } 15 | -------------------------------------------------------------------------------- /iterator.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "github.com/tarantool/go-iproto" 5 | ) 6 | 7 | // Iter is an enumeration type of a select iterator. 8 | type Iter uint32 9 | 10 | const ( 11 | // Key == x ASC order. 12 | IterEq Iter = Iter(iproto.ITER_EQ) 13 | // Key == x DESC order. 14 | IterReq Iter = Iter(iproto.ITER_REQ) 15 | // All tuples. 16 | IterAll Iter = Iter(iproto.ITER_ALL) 17 | // Key < x. 18 | IterLt Iter = Iter(iproto.ITER_LT) 19 | // Key <= x. 20 | IterLe Iter = Iter(iproto.ITER_LE) 21 | // Key >= x. 22 | IterGe Iter = Iter(iproto.ITER_GE) 23 | // Key > x. 24 | IterGt Iter = Iter(iproto.ITER_GT) 25 | // All bits from x are set in key. 26 | IterBitsAllSet Iter = Iter(iproto.ITER_BITS_ALL_SET) 27 | // All bits are not set. 28 | IterBitsAnySet Iter = Iter(iproto.ITER_BITS_ANY_SET) 29 | // All bits are not set. 30 | IterBitsAllNotSet Iter = Iter(iproto.ITER_BITS_ALL_NOT_SET) 31 | // Key overlaps x. 32 | IterOverlaps Iter = Iter(iproto.ITER_OVERLAPS) 33 | // Tuples in distance ascending order from specified point. 34 | IterNeighbor Iter = Iter(iproto.ITER_NEIGHBOR) 35 | ) 36 | -------------------------------------------------------------------------------- /pool/config.lua: -------------------------------------------------------------------------------- 1 | -- Do not set listen for now so connector won't be 2 | -- able to send requests until everything is configured. 3 | box.cfg{ 4 | work_dir = os.getenv("TEST_TNT_WORK_DIR"), 5 | memtx_use_mvcc_engine = os.getenv("TEST_TNT_MEMTX_USE_MVCC_ENGINE") == 'true' or nil, 6 | } 7 | 8 | box.once("init", function() 9 | box.schema.user.create('test', { password = 'test' }) 10 | box.schema.user.grant('test', 'read,write,execute', 'universe') 11 | 12 | box.schema.user.create('test_noexec', { password = 'test' }) 13 | box.schema.user.grant('test_noexec', 'read,write', 'universe') 14 | 15 | local s = box.schema.space.create('testPool', { 16 | id = 520, 17 | if_not_exists = true, 18 | format = { 19 | {name = "key", type = "string"}, 20 | {name = "value", type = "string"}, 21 | }, 22 | }) 23 | s:create_index('pk', { 24 | type = 'tree', 25 | parts = {{ field = 1, type = 'string' }}, 26 | if_not_exists = true 27 | }) 28 | 29 | local sp = box.schema.space.create('SQL_TEST', { 30 | id = 521, 31 | if_not_exists = true, 32 | format = { 33 | {name = "NAME0", type = "unsigned"}, 34 | {name = "NAME1", type = "string"}, 35 | {name = "NAME2", type = "string"}, 36 | } 37 | }) 38 | sp:create_index('primary', {type = 'tree', parts = {1, 'uint'}, if_not_exists = true}) 39 | sp:insert{1, "test", "test"} 40 | -- grants for sql tests 41 | box.schema.user.grant('test', 'create,read,write,drop,alter', 'space') 42 | box.schema.user.grant('test', 'create', 'sequence') 43 | end) 44 | 45 | local function simple_incr(a) 46 | return a + 1 47 | end 48 | 49 | rawset(_G, 'simple_incr', simple_incr) 50 | 51 | -- Set listen only when every other thing is configured. 52 | box.cfg{ 53 | listen = os.getenv("TEST_TNT_LISTEN"), 54 | } 55 | -------------------------------------------------------------------------------- /pool/const.go: -------------------------------------------------------------------------------- 1 | //go:generate stringer -type Role -linecomment 2 | package pool 3 | 4 | /* 5 | Default mode for each request table: 6 | 7 | Request Default mode 8 | ---------- -------------- 9 | | call | no default | 10 | | eval | no default | 11 | | execute | no default | 12 | | ping | no default | 13 | | insert | RW | 14 | | delete | RW | 15 | | replace | RW | 16 | | update | RW | 17 | | upsert | RW | 18 | | select | ANY | 19 | | get | ANY | 20 | */ 21 | type Mode uint32 22 | 23 | const ( 24 | ANY Mode = iota // The request can be executed on any instance (master or replica). 25 | RW // The request can only be executed on master. 26 | RO // The request can only be executed on replica. 27 | PreferRW // If there is one, otherwise fallback to a writeable one (master). 28 | PreferRO // If there is one, otherwise fallback to a read only one (replica). 29 | ) 30 | 31 | // Role describes a role of an instance by its mode. 32 | type Role uint32 33 | 34 | const ( 35 | // UnknownRole - the connection pool was unable to detect the instance mode. 36 | UnknownRole Role = iota // unknown 37 | // MasterRole - the instance is in read-write mode. 38 | MasterRole // master 39 | // ReplicaRole - the instance is in read-only mode. 40 | ReplicaRole // replica 41 | ) 42 | -------------------------------------------------------------------------------- /pool/const_test.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestRole_String(t *testing.T) { 10 | require.Equal(t, "unknown", UnknownRole.String()) 11 | require.Equal(t, "master", MasterRole.String()) 12 | require.Equal(t, "replica", ReplicaRole.String()) 13 | } 14 | -------------------------------------------------------------------------------- /pool/role_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type Role -linecomment"; DO NOT EDIT. 2 | 3 | package pool 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[UnknownRole-0] 12 | _ = x[MasterRole-1] 13 | _ = x[ReplicaRole-2] 14 | } 15 | 16 | const _Role_name = "unknownmasterreplica" 17 | 18 | var _Role_index = [...]uint8{0, 7, 13, 20} 19 | 20 | func (i Role) String() string { 21 | if i >= Role(len(_Role_index)-1) { 22 | return "Role(" + strconv.FormatInt(int64(i), 10) + ")" 23 | } 24 | return _Role_name[_Role_index[i]:_Role_index[i+1]] 25 | } 26 | -------------------------------------------------------------------------------- /pool/round_robin.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | 7 | "github.com/tarantool/go-tarantool/v2" 8 | ) 9 | 10 | type roundRobinStrategy struct { 11 | conns []*tarantool.Connection 12 | indexById map[string]uint 13 | mutex sync.RWMutex 14 | size uint64 15 | current uint64 16 | } 17 | 18 | func newRoundRobinStrategy(size int) *roundRobinStrategy { 19 | return &roundRobinStrategy{ 20 | conns: make([]*tarantool.Connection, 0, size), 21 | indexById: make(map[string]uint, size), 22 | size: 0, 23 | current: 0, 24 | } 25 | } 26 | 27 | func (r *roundRobinStrategy) GetConnection(id string) *tarantool.Connection { 28 | r.mutex.RLock() 29 | defer r.mutex.RUnlock() 30 | 31 | index, found := r.indexById[id] 32 | if !found { 33 | return nil 34 | } 35 | 36 | return r.conns[index] 37 | } 38 | 39 | func (r *roundRobinStrategy) DeleteConnection(id string) *tarantool.Connection { 40 | r.mutex.Lock() 41 | defer r.mutex.Unlock() 42 | 43 | if r.size == 0 { 44 | return nil 45 | } 46 | 47 | index, found := r.indexById[id] 48 | if !found { 49 | return nil 50 | } 51 | 52 | delete(r.indexById, id) 53 | 54 | conn := r.conns[index] 55 | r.conns = append(r.conns[:index], r.conns[index+1:]...) 56 | r.size -= 1 57 | 58 | for k, v := range r.indexById { 59 | if v > index { 60 | r.indexById[k] = v - 1 61 | } 62 | } 63 | 64 | return conn 65 | } 66 | 67 | func (r *roundRobinStrategy) IsEmpty() bool { 68 | r.mutex.RLock() 69 | defer r.mutex.RUnlock() 70 | 71 | return r.size == 0 72 | } 73 | 74 | func (r *roundRobinStrategy) GetNextConnection() *tarantool.Connection { 75 | r.mutex.RLock() 76 | defer r.mutex.RUnlock() 77 | 78 | if r.size == 0 { 79 | return nil 80 | } 81 | return r.conns[r.nextIndex()] 82 | } 83 | 84 | func (r *roundRobinStrategy) GetConnections() map[string]*tarantool.Connection { 85 | r.mutex.RLock() 86 | defer r.mutex.RUnlock() 87 | 88 | conns := map[string]*tarantool.Connection{} 89 | for id, index := range r.indexById { 90 | conns[id] = r.conns[index] 91 | } 92 | 93 | return conns 94 | } 95 | 96 | func (r *roundRobinStrategy) AddConnection(id string, conn *tarantool.Connection) { 97 | r.mutex.Lock() 98 | defer r.mutex.Unlock() 99 | 100 | if idx, ok := r.indexById[id]; ok { 101 | r.conns[idx] = conn 102 | } else { 103 | r.conns = append(r.conns, conn) 104 | r.indexById[id] = uint(r.size) 105 | r.size += 1 106 | } 107 | } 108 | 109 | func (r *roundRobinStrategy) nextIndex() uint64 { 110 | next := atomic.AddUint64(&r.current, 1) 111 | return (next - 1) % r.size 112 | } 113 | -------------------------------------------------------------------------------- /pool/round_robin_test.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tarantool/go-tarantool/v2" 7 | ) 8 | 9 | const ( 10 | validAddr1 = "x" 11 | validAddr2 = "y" 12 | ) 13 | 14 | func TestRoundRobinAddDelete(t *testing.T) { 15 | rr := newRoundRobinStrategy(10) 16 | 17 | addrs := []string{validAddr1, validAddr2} 18 | conns := []*tarantool.Connection{&tarantool.Connection{}, &tarantool.Connection{}} 19 | 20 | for i, addr := range addrs { 21 | rr.AddConnection(addr, conns[i]) 22 | } 23 | 24 | for i, addr := range addrs { 25 | if conn := rr.DeleteConnection(addr); conn != conns[i] { 26 | t.Errorf("Unexpected connection on address %s", addr) 27 | } 28 | } 29 | if !rr.IsEmpty() { 30 | t.Errorf("RoundRobin does not empty") 31 | } 32 | } 33 | 34 | func TestRoundRobinAddDuplicateDelete(t *testing.T) { 35 | rr := newRoundRobinStrategy(10) 36 | 37 | conn1 := &tarantool.Connection{} 38 | conn2 := &tarantool.Connection{} 39 | 40 | rr.AddConnection(validAddr1, conn1) 41 | rr.AddConnection(validAddr1, conn2) 42 | 43 | if rr.DeleteConnection(validAddr1) != conn2 { 44 | t.Errorf("Unexpected deleted connection") 45 | } 46 | if !rr.IsEmpty() { 47 | t.Errorf("RoundRobin does not empty") 48 | } 49 | if rr.DeleteConnection(validAddr1) != nil { 50 | t.Errorf("Unexpected value after second deletion") 51 | } 52 | } 53 | 54 | func TestRoundRobinGetNextConnection(t *testing.T) { 55 | rr := newRoundRobinStrategy(10) 56 | 57 | addrs := []string{validAddr1, validAddr2} 58 | conns := []*tarantool.Connection{&tarantool.Connection{}, &tarantool.Connection{}} 59 | 60 | for i, addr := range addrs { 61 | rr.AddConnection(addr, conns[i]) 62 | } 63 | 64 | expectedConns := []*tarantool.Connection{conns[0], conns[1], conns[0], conns[1]} 65 | for i, expected := range expectedConns { 66 | if rr.GetNextConnection() != expected { 67 | t.Errorf("Unexpected connection on %d call", i) 68 | } 69 | } 70 | } 71 | 72 | func TestRoundRobinStrategy_GetConnections(t *testing.T) { 73 | rr := newRoundRobinStrategy(10) 74 | 75 | addrs := []string{validAddr1, validAddr2} 76 | conns := []*tarantool.Connection{&tarantool.Connection{}, &tarantool.Connection{}} 77 | 78 | for i, addr := range addrs { 79 | rr.AddConnection(addr, conns[i]) 80 | } 81 | 82 | rr.GetConnections()[validAddr2] = conns[0] // GetConnections() returns a copy. 83 | rrConns := rr.GetConnections() 84 | 85 | for i, addr := range addrs { 86 | if conns[i] != rrConns[addr] { 87 | t.Errorf("Unexpected connection on %s addr", addr) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /pool/state.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "sync/atomic" 5 | ) 6 | 7 | // pool state 8 | type state uint32 9 | 10 | const ( 11 | unknownState state = iota 12 | connectedState 13 | shutdownState 14 | closedState 15 | ) 16 | 17 | func (s *state) set(news state) { 18 | atomic.StoreUint32((*uint32)(s), uint32(news)) 19 | } 20 | 21 | func (s *state) cas(olds, news state) bool { 22 | return atomic.CompareAndSwapUint32((*uint32)(s), uint32(olds), uint32(news)) 23 | } 24 | 25 | func (s *state) get() state { 26 | return state(atomic.LoadUint32((*uint32)(s))) 27 | } 28 | -------------------------------------------------------------------------------- /pool/watcher.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/tarantool/go-tarantool/v2" 7 | ) 8 | 9 | // watcherContainer is a very simple implementation of a thread-safe container 10 | // for watchers. It is not expected that there will be too many watchers and 11 | // they will registered/unregistered too frequently. 12 | // 13 | // Otherwise, the implementation will need to be optimized. 14 | type watcherContainer struct { 15 | head *poolWatcher 16 | mutex sync.RWMutex 17 | } 18 | 19 | // add adds a watcher to the container. 20 | func (c *watcherContainer) add(watcher *poolWatcher) { 21 | c.mutex.Lock() 22 | defer c.mutex.Unlock() 23 | 24 | watcher.next = c.head 25 | c.head = watcher 26 | } 27 | 28 | // remove removes a watcher from the container. 29 | func (c *watcherContainer) remove(watcher *poolWatcher) bool { 30 | c.mutex.Lock() 31 | defer c.mutex.Unlock() 32 | 33 | if watcher == c.head { 34 | c.head = watcher.next 35 | return true 36 | } else if c.head != nil { 37 | cur := c.head 38 | for cur.next != nil { 39 | if cur.next == watcher { 40 | cur.next = watcher.next 41 | return true 42 | } 43 | cur = cur.next 44 | } 45 | } 46 | return false 47 | } 48 | 49 | // foreach iterates over the container to the end or until the call returns 50 | // false. 51 | func (c *watcherContainer) foreach(call func(watcher *poolWatcher) error) error { 52 | cur := c.head 53 | for cur != nil { 54 | if err := call(cur); err != nil { 55 | return err 56 | } 57 | cur = cur.next 58 | } 59 | return nil 60 | } 61 | 62 | // poolWatcher is an internal implementation of the tarantool.Watcher interface. 63 | type poolWatcher struct { 64 | // The watcher container data. We can split the structure into two parts 65 | // in the future: a watcher data and a watcher container data, but it looks 66 | // simple at now. 67 | 68 | // next item in the watcher container. 69 | next *poolWatcher 70 | // container is the container for all active poolWatcher objects. 71 | container *watcherContainer 72 | 73 | // The watcher data. 74 | // mode of the watcher. 75 | mode Mode 76 | key string 77 | callback tarantool.WatchCallback 78 | // watchers is a map connection -> connection watcher. 79 | watchers map[*tarantool.Connection]tarantool.Watcher 80 | // unregistered is true if the watcher already unregistered. 81 | unregistered bool 82 | // mutex for the pool watcher. 83 | mutex sync.Mutex 84 | } 85 | 86 | // Unregister unregisters the pool watcher. 87 | func (w *poolWatcher) Unregister() { 88 | w.mutex.Lock() 89 | unregistered := w.unregistered 90 | w.mutex.Unlock() 91 | 92 | if !unregistered && w.container.remove(w) { 93 | w.mutex.Lock() 94 | w.unregistered = true 95 | for _, watcher := range w.watchers { 96 | watcher.Unregister() 97 | } 98 | w.mutex.Unlock() 99 | } 100 | } 101 | 102 | // watch adds a watcher for the connection. 103 | func (w *poolWatcher) watch(conn *tarantool.Connection) error { 104 | w.mutex.Lock() 105 | defer w.mutex.Unlock() 106 | 107 | if !w.unregistered { 108 | if _, ok := w.watchers[conn]; ok { 109 | return nil 110 | } 111 | 112 | if watcher, err := conn.NewWatcher(w.key, w.callback); err == nil { 113 | w.watchers[conn] = watcher 114 | return nil 115 | } else { 116 | return err 117 | } 118 | } 119 | return nil 120 | } 121 | 122 | // unwatch removes a watcher for the connection. 123 | func (w *poolWatcher) unwatch(conn *tarantool.Connection) { 124 | w.mutex.Lock() 125 | defer w.mutex.Unlock() 126 | 127 | if !w.unregistered { 128 | if watcher, ok := w.watchers[conn]; ok { 129 | watcher.Unregister() 130 | delete(w.watchers, conn) 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /protocol.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/tarantool/go-iproto" 7 | "github.com/vmihailenco/msgpack/v5" 8 | ) 9 | 10 | // ProtocolVersion type stores Tarantool protocol version. 11 | type ProtocolVersion uint64 12 | 13 | // ProtocolInfo type aggregates Tarantool protocol version and features info. 14 | type ProtocolInfo struct { 15 | // Auth is an authentication method. 16 | Auth Auth 17 | // Version is the supported protocol version. 18 | Version ProtocolVersion 19 | // Features are supported protocol features. 20 | Features []iproto.Feature 21 | } 22 | 23 | // Clone returns an exact copy of the ProtocolInfo object. 24 | // Any changes in copy will not affect the original values. 25 | func (info ProtocolInfo) Clone() ProtocolInfo { 26 | infoCopy := info 27 | 28 | if info.Features != nil { 29 | infoCopy.Features = make([]iproto.Feature, len(info.Features)) 30 | copy(infoCopy.Features, info.Features) 31 | } 32 | 33 | return infoCopy 34 | } 35 | 36 | var clientProtocolInfo ProtocolInfo = ProtocolInfo{ 37 | // Protocol version supported by connector. Version 3 38 | // was introduced in Tarantool 2.10.0, version 4 was 39 | // introduced in master 948e5cd (possible 2.10.5 or 2.11.0). 40 | // Support of protocol version on connector side was introduced in 41 | // 1.10.0. 42 | Version: ProtocolVersion(6), 43 | // Streams and transactions were introduced in protocol version 1 44 | // (Tarantool 2.10.0), in connector since 1.7.0. 45 | // Error extension type was introduced in protocol 46 | // version 2 (Tarantool 2.10.0), in connector since 1.10.0. 47 | // Watchers were introduced in protocol version 3 (Tarantool 2.10.0), in 48 | // connector since 1.10.0. 49 | // Pagination were introduced in protocol version 4 (Tarantool 2.11.0), in 50 | // connector since 1.11.0. 51 | // WatchOnce request type was introduces in protocol version 6 52 | // (Tarantool 3.0.0), in connector since 2.0.0. 53 | Features: []iproto.Feature{ 54 | iproto.IPROTO_FEATURE_STREAMS, 55 | iproto.IPROTO_FEATURE_TRANSACTIONS, 56 | iproto.IPROTO_FEATURE_ERROR_EXTENSION, 57 | iproto.IPROTO_FEATURE_WATCHERS, 58 | iproto.IPROTO_FEATURE_PAGINATION, 59 | iproto.IPROTO_FEATURE_SPACE_AND_INDEX_NAMES, 60 | iproto.IPROTO_FEATURE_WATCH_ONCE, 61 | }, 62 | } 63 | 64 | // IdRequest informs the server about supported protocol 65 | // version and protocol features. 66 | type IdRequest struct { 67 | baseRequest 68 | protocolInfo ProtocolInfo 69 | } 70 | 71 | func fillId(enc *msgpack.Encoder, protocolInfo ProtocolInfo) error { 72 | enc.EncodeMapLen(2) 73 | 74 | enc.EncodeUint(uint64(iproto.IPROTO_VERSION)) 75 | if err := enc.Encode(protocolInfo.Version); err != nil { 76 | return err 77 | } 78 | 79 | enc.EncodeUint(uint64(iproto.IPROTO_FEATURES)) 80 | 81 | t := len(protocolInfo.Features) 82 | if err := enc.EncodeArrayLen(t); err != nil { 83 | return err 84 | } 85 | 86 | for _, feature := range protocolInfo.Features { 87 | if err := enc.Encode(feature); err != nil { 88 | return err 89 | } 90 | } 91 | 92 | return nil 93 | } 94 | 95 | // NewIdRequest returns a new IdRequest. 96 | func NewIdRequest(protocolInfo ProtocolInfo) *IdRequest { 97 | req := new(IdRequest) 98 | req.rtype = iproto.IPROTO_ID 99 | req.protocolInfo = protocolInfo.Clone() 100 | return req 101 | } 102 | 103 | // Body fills an msgpack.Encoder with the id request body. 104 | func (req *IdRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { 105 | return fillId(enc, req.protocolInfo) 106 | } 107 | 108 | // Context sets a passed context to the request. 109 | // 110 | // Pay attention that when using context with request objects, 111 | // the timeout option for Connection does not affect the lifetime 112 | // of the request. For those purposes use context.WithTimeout() as 113 | // the root context. 114 | func (req *IdRequest) Context(ctx context.Context) *IdRequest { 115 | req.ctx = ctx 116 | return req 117 | } 118 | -------------------------------------------------------------------------------- /protocol_test.go: -------------------------------------------------------------------------------- 1 | package tarantool_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/tarantool/go-iproto" 8 | 9 | . "github.com/tarantool/go-tarantool/v2" 10 | ) 11 | 12 | func TestProtocolInfoClonePreservesFeatures(t *testing.T) { 13 | original := ProtocolInfo{ 14 | Version: ProtocolVersion(100), 15 | Features: []iproto.Feature{iproto.Feature(99), iproto.Feature(100)}, 16 | } 17 | 18 | origCopy := original.Clone() 19 | 20 | original.Features[1] = iproto.Feature(98) 21 | 22 | require.Equal(t, 23 | origCopy, 24 | ProtocolInfo{ 25 | Version: ProtocolVersion(100), 26 | Features: []iproto.Feature{iproto.Feature(99), iproto.Feature(100)}, 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /queue/const.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | const ( 4 | READY = "r" 5 | TAKEN = "t" 6 | DONE = "-" 7 | BURIED = "!" 8 | DELAYED = "~" 9 | ) 10 | 11 | type queueType string 12 | 13 | const ( 14 | FIFO queueType = "fifo" 15 | FIFO_TTL queueType = "fifottl" 16 | UTUBE queueType = "utube" 17 | UTUBE_TTL queueType = "utubettl" 18 | ) 19 | 20 | type State int 21 | 22 | const ( 23 | UnknownState State = iota 24 | InitState 25 | StartupState 26 | RunningState 27 | EndingState 28 | WaitingState 29 | ) 30 | 31 | var strToState = map[string]State{ 32 | "INIT": InitState, 33 | "STARTUP": StartupState, 34 | "RUNNING": RunningState, 35 | "ENDING": EndingState, 36 | "WAITING": WaitingState, 37 | } 38 | -------------------------------------------------------------------------------- /queue/example_msgpack_test.go: -------------------------------------------------------------------------------- 1 | // Setup queue module and start Tarantool instance before execution: 2 | // Terminal 1: 3 | // $ make deps 4 | // $ TEST_TNT_LISTEN=3013 tarantool queue/config.lua 5 | // 6 | // Terminal 2: 7 | // $ cd queue 8 | // $ go test -v example_msgpack_test.go 9 | package queue_test 10 | 11 | import ( 12 | "context" 13 | "fmt" 14 | "log" 15 | "time" 16 | 17 | "github.com/vmihailenco/msgpack/v5" 18 | 19 | "github.com/tarantool/go-tarantool/v2" 20 | "github.com/tarantool/go-tarantool/v2/queue" 21 | ) 22 | 23 | type dummyData struct { 24 | Dummy bool 25 | } 26 | 27 | func (c *dummyData) DecodeMsgpack(d *msgpack.Decoder) error { 28 | var err error 29 | if c.Dummy, err = d.DecodeBool(); err != nil { 30 | return err 31 | } 32 | return nil 33 | } 34 | 35 | func (c *dummyData) EncodeMsgpack(e *msgpack.Encoder) error { 36 | return e.EncodeBool(c.Dummy) 37 | } 38 | 39 | // Example demonstrates an operations like Put and Take with queue and custom 40 | // MsgPack structure. 41 | // 42 | // Features of the implementation: 43 | // 44 | // - If you use the connection timeout and call TakeWithTimeout with a 45 | // parameter greater than the connection timeout, the parameter is reduced to 46 | // it. 47 | // 48 | // - If you use the connection timeout and call Take, we return an error if we 49 | // cannot take the task out of the queue within the time corresponding to the 50 | // connection timeout. 51 | func Example_simpleQueueCustomMsgPack() { 52 | dialer := tarantool.NetDialer{ 53 | Address: "127.0.0.1:3013", 54 | User: "test", 55 | Password: "test", 56 | } 57 | opts := tarantool.Opts{ 58 | Reconnect: time.Second, 59 | Timeout: 5 * time.Second, 60 | MaxReconnects: 5, 61 | } 62 | ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) 63 | conn, err := tarantool.Connect(ctx, dialer, opts) 64 | cancel() 65 | if err != nil { 66 | log.Fatalf("connection: %s", err) 67 | return 68 | } 69 | defer conn.Close() 70 | 71 | cfg := queue.Cfg{ 72 | Temporary: true, 73 | IfNotExists: true, 74 | Kind: queue.FIFO, 75 | Opts: queue.Opts{ 76 | Ttl: 20 * time.Second, 77 | Ttr: 10 * time.Second, 78 | Delay: 6 * time.Second, 79 | Pri: 1, 80 | }, 81 | } 82 | 83 | que := queue.New(conn, "test_queue_msgpack") 84 | if err = que.Create(cfg); err != nil { 85 | fmt.Printf("queue create: %s", err) 86 | return 87 | } 88 | 89 | // Put data. 90 | task, err := que.Put("test_data") 91 | if err != nil { 92 | fmt.Printf("put task: %s", err) 93 | return 94 | } 95 | fmt.Println("Task id is", task.Id()) 96 | 97 | // Take data. 98 | task, err = que.Take() // Blocking operation. 99 | if err != nil { 100 | fmt.Printf("take task: %s", err) 101 | return 102 | } 103 | fmt.Println("Data is", task.Data()) 104 | task.Ack() 105 | 106 | // Take typed example. 107 | putData := dummyData{} 108 | // Put data. 109 | task, err = que.Put(&putData) 110 | if err != nil { 111 | fmt.Printf("put typed task: %s", err) 112 | return 113 | } 114 | fmt.Println("Task id is ", task.Id()) 115 | 116 | takeData := dummyData{} 117 | // Take data. 118 | task, err = que.TakeTyped(&takeData) // Blocking operation. 119 | if err != nil { 120 | fmt.Printf("take take typed: %s", err) 121 | return 122 | } 123 | fmt.Println("Data is ", takeData) 124 | // Same data. 125 | fmt.Println("Data is ", task.Data()) 126 | 127 | task, err = que.Put([]int{1, 2, 3}) 128 | if err != nil { 129 | fmt.Printf("Put failed: %s", err) 130 | return 131 | } 132 | task.Bury() 133 | 134 | task, err = que.TakeTimeout(2 * time.Second) 135 | if err != nil { 136 | fmt.Printf("Take with timeout failed: %s", err) 137 | return 138 | } 139 | if task == nil { 140 | fmt.Println("Task is nil") 141 | } 142 | 143 | que.Drop() 144 | 145 | // Unordered output: 146 | // Task id is 0 147 | // Data is test_data 148 | // Task id is 0 149 | // Data is {false} 150 | // Data is &{false} 151 | // Task is nil 152 | } 153 | -------------------------------------------------------------------------------- /queue/example_test.go: -------------------------------------------------------------------------------- 1 | // Setup queue module and start Tarantool instance before execution: 2 | // Terminal 1: 3 | // $ make deps 4 | // $ TEST_TNT_LISTEN=3013 tarantool queue/config.lua 5 | // 6 | // Terminal 2: 7 | // $ cd queue 8 | // $ go test -v example_test.go 9 | package queue_test 10 | 11 | import ( 12 | "context" 13 | "fmt" 14 | "time" 15 | 16 | "github.com/tarantool/go-tarantool/v2" 17 | "github.com/tarantool/go-tarantool/v2/queue" 18 | ) 19 | 20 | // Example demonstrates an operations like Put and Take with queue. 21 | func Example_simpleQueue() { 22 | cfg := queue.Cfg{ 23 | Temporary: false, 24 | Kind: queue.FIFO, 25 | Opts: queue.Opts{ 26 | Ttl: 10 * time.Second, 27 | }, 28 | } 29 | opts := tarantool.Opts{ 30 | Timeout: 2500 * time.Millisecond, 31 | } 32 | dialer := tarantool.NetDialer{ 33 | Address: "127.0.0.1:3013", 34 | User: "test", 35 | Password: "test", 36 | } 37 | 38 | ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) 39 | defer cancel() 40 | conn, err := tarantool.Connect(ctx, dialer, opts) 41 | if err != nil { 42 | fmt.Printf("error in prepare is %v", err) 43 | return 44 | } 45 | defer conn.Close() 46 | 47 | q := queue.New(conn, "test_queue") 48 | if err := q.Create(cfg); err != nil { 49 | fmt.Printf("error in queue is %v", err) 50 | return 51 | } 52 | 53 | defer q.Drop() 54 | 55 | testData_1 := "test_data_1" 56 | if _, err = q.Put(testData_1); err != nil { 57 | fmt.Printf("error in put is %v", err) 58 | return 59 | } 60 | 61 | testData_2 := "test_data_2" 62 | task_2, err := q.PutWithOpts(testData_2, queue.Opts{Ttl: 2 * time.Second}) 63 | if err != nil { 64 | fmt.Printf("error in put with config is %v", err) 65 | return 66 | } 67 | 68 | task, err := q.Take() 69 | if err != nil { 70 | fmt.Printf("error in take with is %v", err) 71 | return 72 | } 73 | task.Ack() 74 | fmt.Println("data_1: ", task.Data()) 75 | 76 | err = task_2.Bury() 77 | if err != nil { 78 | fmt.Printf("error in bury with is %v", err) 79 | return 80 | } 81 | 82 | task, err = q.TakeTimeout(2 * time.Second) 83 | if err != nil { 84 | fmt.Printf("error in take with timeout") 85 | } 86 | if task != nil { 87 | fmt.Printf("Task should be nil, but %d", task.Id()) 88 | return 89 | } 90 | 91 | // Output: data_1: test_data_1 92 | } 93 | -------------------------------------------------------------------------------- /queue/task.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/vmihailenco/msgpack/v5" 8 | ) 9 | 10 | // Task represents a task from Tarantool queue's tube. 11 | type Task struct { 12 | id uint64 13 | status string 14 | data interface{} 15 | q *queue 16 | } 17 | 18 | func (t *Task) DecodeMsgpack(d *msgpack.Decoder) error { 19 | var err error 20 | var l int 21 | if l, err = d.DecodeArrayLen(); err != nil { 22 | return err 23 | } 24 | if l < 3 { 25 | return fmt.Errorf("array len doesn't match: %d", l) 26 | } 27 | if t.id, err = d.DecodeUint64(); err != nil { 28 | return err 29 | } 30 | if t.status, err = d.DecodeString(); err != nil { 31 | return err 32 | } 33 | if t.data != nil { 34 | d.Decode(t.data) 35 | } else if t.data, err = d.DecodeInterface(); err != nil { 36 | return err 37 | } 38 | return nil 39 | } 40 | 41 | // Id is a getter for task id. 42 | func (t *Task) Id() uint64 { 43 | return t.id 44 | } 45 | 46 | // Data is a getter for task data. 47 | func (t *Task) Data() interface{} { 48 | return t.data 49 | } 50 | 51 | // Status is a getter for task status. 52 | func (t *Task) Status() string { 53 | return t.status 54 | } 55 | 56 | // Touch increases ttr of running task. 57 | func (t *Task) Touch(increment time.Duration) error { 58 | return t.accept(t.q._touch(t.id, increment)) 59 | } 60 | 61 | // Ack signals about task completion. 62 | func (t *Task) Ack() error { 63 | return t.accept(t.q._ack(t.id)) 64 | } 65 | 66 | // Delete task from queue. 67 | func (t *Task) Delete() error { 68 | return t.accept(t.q._delete(t.id)) 69 | } 70 | 71 | // Bury signals that task task cannot be executed in the current circumstances, 72 | // task becomes "buried" - ie neither completed, nor ready, so it could not be 73 | // deleted or taken by other worker. 74 | // To revert "burying" call queue.Kick(numberOfBurried). 75 | func (t *Task) Bury() error { 76 | return t.accept(t.q._bury(t.id)) 77 | } 78 | 79 | // Release returns task back in the queue without making it complete. 80 | // In other words, this worker failed to complete the task, and 81 | // it, so other worker could try to do that again. 82 | func (t *Task) Release() error { 83 | return t.accept(t.q._release(t.id, Opts{})) 84 | } 85 | 86 | // ReleaseCfg returns task to a queue and changes its configuration. 87 | func (t *Task) ReleaseCfg(cfg Opts) error { 88 | return t.accept(t.q._release(t.id, cfg)) 89 | } 90 | 91 | func (t *Task) accept(newStatus string, err error) error { 92 | if err == nil { 93 | t.status = newStatus 94 | } 95 | return err 96 | } 97 | 98 | // IsReady returns if task is ready. 99 | func (t *Task) IsReady() bool { 100 | return t.status == READY 101 | } 102 | 103 | // IsTaken returns if task is taken. 104 | func (t *Task) IsTaken() bool { 105 | return t.status == TAKEN 106 | } 107 | 108 | // IsDone returns if task is done. 109 | func (t *Task) IsDone() bool { 110 | return t.status == DONE 111 | } 112 | 113 | // IsBurred returns if task is buried. 114 | func (t *Task) IsBuried() bool { 115 | return t.status == BURIED 116 | } 117 | 118 | // IsDelayed returns if task is delayed. 119 | func (t *Task) IsDelayed() bool { 120 | return t.status == DELAYED 121 | } 122 | -------------------------------------------------------------------------------- /queue/testdata/config.lua: -------------------------------------------------------------------------------- 1 | -- configure path so that you can run application 2 | -- from outside the root directory 3 | if package.setsearchroot ~= nil then 4 | package.setsearchroot() 5 | else 6 | -- Workaround for rocks loading in tarantool 1.10 7 | -- It can be removed in tarantool > 2.2 8 | -- By default, when you do require('mymodule'), tarantool looks into 9 | -- the current working directory and whatever is specified in 10 | -- package.path and package.cpath. If you run your app while in the 11 | -- root directory of that app, everything goes fine, but if you try to 12 | -- start your app with "tarantool myapp/init.lua", it will fail to load 13 | -- its modules, and modules from myapp/.rocks. 14 | local fio = require('fio') 15 | local app_dir = fio.abspath(fio.dirname(arg[0])) 16 | package.path = app_dir .. '/?.lua;' .. package.path 17 | package.path = app_dir .. '/?/init.lua;' .. package.path 18 | package.path = app_dir .. '/.rocks/share/tarantool/?.lua;' .. package.path 19 | package.path = app_dir .. '/.rocks/share/tarantool/?/init.lua;' .. package.path 20 | package.cpath = app_dir .. '/?.so;' .. package.cpath 21 | package.cpath = app_dir .. '/?.dylib;' .. package.cpath 22 | package.cpath = app_dir .. '/.rocks/lib/tarantool/?.so;' .. package.cpath 23 | package.cpath = app_dir .. '/.rocks/lib/tarantool/?.dylib;' .. package.cpath 24 | end 25 | 26 | local queue = require('queue') 27 | rawset(_G, 'queue', queue) 28 | 29 | -- Do not set listen for now so connector won't be 30 | -- able to send requests until everything is configured. 31 | box.cfg{ 32 | work_dir = os.getenv("TEST_TNT_WORK_DIR"), 33 | } 34 | 35 | box.once("init", function() 36 | box.schema.user.create('test', {password = 'test'}) 37 | box.schema.func.create('queue.tube.test_queue:touch') 38 | box.schema.func.create('queue.tube.test_queue:ack') 39 | box.schema.func.create('queue.tube.test_queue:put') 40 | box.schema.func.create('queue.tube.test_queue:drop') 41 | box.schema.func.create('queue.tube.test_queue:peek') 42 | box.schema.func.create('queue.tube.test_queue:kick') 43 | box.schema.func.create('queue.tube.test_queue:take') 44 | box.schema.func.create('queue.tube.test_queue:delete') 45 | box.schema.func.create('queue.tube.test_queue:release') 46 | box.schema.func.create('queue.tube.test_queue:release_all') 47 | box.schema.func.create('queue.tube.test_queue:bury') 48 | box.schema.func.create('queue.identify') 49 | box.schema.func.create('queue.state') 50 | box.schema.func.create('queue.statistics') 51 | box.schema.user.grant('test', 'create,read,write,drop', 'space') 52 | box.schema.user.grant('test', 'read, write', 'space', '_queue_session_ids') 53 | box.schema.user.grant('test', 'execute', 'universe') 54 | box.schema.user.grant('test', 'read,write', 'space', '_queue') 55 | box.schema.user.grant('test', 'read,write', 'space', '_schema') 56 | box.schema.user.grant('test', 'read,write', 'space', '_space_sequence') 57 | box.schema.user.grant('test', 'read,write', 'space', '_space') 58 | box.schema.user.grant('test', 'read,write', 'space', '_index') 59 | box.schema.user.grant('test', 'read,write', 'space', '_priv') 60 | if box.space._trigger ~= nil then 61 | box.schema.user.grant('test', 'read', 'space', '_trigger') 62 | end 63 | if box.space._fk_constraint ~= nil then 64 | box.schema.user.grant('test', 'read', 'space', '_fk_constraint') 65 | end 66 | if box.space._ck_constraint ~= nil then 67 | box.schema.user.grant('test', 'read', 'space', '_ck_constraint') 68 | end 69 | if box.space._func_index ~= nil then 70 | box.schema.user.grant('test', 'read', 'space', '_func_index') 71 | end 72 | end) 73 | 74 | -- Set listen only when every other thing is configured. 75 | box.cfg{ 76 | listen = os.getenv("TEST_TNT_LISTEN"), 77 | } 78 | 79 | require('console').start() 80 | -------------------------------------------------------------------------------- /queue/testdata/pool.lua: -------------------------------------------------------------------------------- 1 | -- configure path so that you can run application 2 | -- from outside the root directory 3 | if package.setsearchroot ~= nil then 4 | package.setsearchroot() 5 | else 6 | -- Workaround for rocks loading in tarantool 1.10 7 | -- It can be removed in tarantool > 2.2 8 | -- By default, when you do require('mymodule'), tarantool looks into 9 | -- the current working directory and whatever is specified in 10 | -- package.path and package.cpath. If you run your app while in the 11 | -- root directory of that app, everything goes fine, but if you try to 12 | -- start your app with "tarantool myapp/init.lua", it will fail to load 13 | -- its modules, and modules from myapp/.rocks. 14 | local fio = require('fio') 15 | local app_dir = fio.abspath(fio.dirname(arg[0])) 16 | package.path = app_dir .. '/?.lua;' .. package.path 17 | package.path = app_dir .. '/?/init.lua;' .. package.path 18 | package.path = app_dir .. '/.rocks/share/tarantool/?.lua;' .. package.path 19 | package.path = app_dir .. '/.rocks/share/tarantool/?/init.lua;' .. package.path 20 | package.cpath = app_dir .. '/?.so;' .. package.cpath 21 | package.cpath = app_dir .. '/?.dylib;' .. package.cpath 22 | package.cpath = app_dir .. '/.rocks/lib/tarantool/?.so;' .. package.cpath 23 | package.cpath = app_dir .. '/.rocks/lib/tarantool/?.dylib;' .. package.cpath 24 | end 25 | 26 | local queue = require('queue') 27 | rawset(_G, 'queue', queue) 28 | -- queue.cfg({in_replicaset = true}) should be called before box.cfg({}) 29 | -- https://github.com/tarantool/queue/issues/206 30 | queue.cfg({in_replicaset = true, ttr = 60}) 31 | 32 | local listen = os.getenv("TEST_TNT_LISTEN") 33 | box.cfg{ 34 | work_dir = os.getenv("TEST_TNT_WORK_DIR"), 35 | listen = listen, 36 | replication = { 37 | "test:test@127.0.0.1:3014", 38 | "test:test@127.0.0.1:3015", 39 | }, 40 | read_only = listen == "127.0.0.1:3015" 41 | } 42 | 43 | box.once("schema", function() 44 | box.schema.user.create('test', {password = 'test'}) 45 | box.schema.user.grant('test', 'replication') 46 | 47 | box.schema.func.create('queue.tube.test_queue:touch') 48 | box.schema.func.create('queue.tube.test_queue:ack') 49 | box.schema.func.create('queue.tube.test_queue:put') 50 | box.schema.func.create('queue.tube.test_queue:drop') 51 | box.schema.func.create('queue.tube.test_queue:peek') 52 | box.schema.func.create('queue.tube.test_queue:kick') 53 | box.schema.func.create('queue.tube.test_queue:take') 54 | box.schema.func.create('queue.tube.test_queue:delete') 55 | box.schema.func.create('queue.tube.test_queue:release') 56 | box.schema.func.create('queue.tube.test_queue:release_all') 57 | box.schema.func.create('queue.tube.test_queue:bury') 58 | box.schema.func.create('queue.identify') 59 | box.schema.func.create('queue.state') 60 | box.schema.func.create('queue.statistics') 61 | box.schema.user.grant('test', 'create,read,write,drop', 'space') 62 | box.schema.user.grant('test', 'read, write', 'space', '_queue_session_ids') 63 | box.schema.user.grant('test', 'execute', 'universe') 64 | box.schema.user.grant('test', 'read,write', 'space', '_queue') 65 | box.schema.user.grant('test', 'read,write', 'space', '_schema') 66 | box.schema.user.grant('test', 'read,write', 'space', '_space_sequence') 67 | box.schema.user.grant('test', 'read,write', 'space', '_space') 68 | box.schema.user.grant('test', 'read,write', 'space', '_index') 69 | box.schema.user.grant('test', 'read,write', 'space', '_priv') 70 | if box.space._trigger ~= nil then 71 | box.schema.user.grant('test', 'read', 'space', '_trigger') 72 | end 73 | if box.space._fk_constraint ~= nil then 74 | box.schema.user.grant('test', 'read', 'space', '_fk_constraint') 75 | end 76 | if box.space._ck_constraint ~= nil then 77 | box.schema.user.grant('test', 'read', 'space', '_ck_constraint') 78 | end 79 | if box.space._func_index ~= nil then 80 | box.schema.user.grant('test', 'read', 'space', '_func_index') 81 | end 82 | end) 83 | 84 | require('console').start() 85 | -------------------------------------------------------------------------------- /response_it.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // ResponseIterator is an interface for iteration over a set of responses. 8 | // 9 | // Deprecated: the method will be removed in the next major version, 10 | // use Connector.NewWatcher() instead of box.session.push(). 11 | type ResponseIterator interface { 12 | // Next tries to switch to a next Response and returns true if it exists. 13 | Next() bool 14 | // Value returns a current Response if it exists, nil otherwise. 15 | Value() Response 16 | // IsPush returns true if the current response is a push response. 17 | IsPush() bool 18 | // Err returns error if it happens. 19 | Err() error 20 | } 21 | 22 | // TimeoutResponseIterator is an interface that extends ResponseIterator 23 | // and adds the ability to change a timeout for the Next() call. 24 | // 25 | // Deprecated: the method will be removed in the next major version, 26 | // use Connector.NewWatcher() instead of box.session.push(). 27 | type TimeoutResponseIterator interface { 28 | ResponseIterator 29 | // WithTimeout allows to set up a timeout for the Next() call. 30 | // Note: in the current implementation, there is a timeout for each 31 | // response (the timeout for the request is reset by each push message): 32 | // Connection's Opts.Timeout. You need to increase the value if necessary. 33 | WithTimeout(timeout time.Duration) TimeoutResponseIterator 34 | } 35 | -------------------------------------------------------------------------------- /response_test.go: -------------------------------------------------------------------------------- 1 | package tarantool_test 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | "github.com/tarantool/go-iproto" 10 | "github.com/tarantool/go-tarantool/v2" 11 | "github.com/vmihailenco/msgpack/v5" 12 | ) 13 | 14 | func encodeResponseData(t *testing.T, data interface{}) io.Reader { 15 | t.Helper() 16 | 17 | buf := bytes.NewBuffer([]byte{}) 18 | enc := msgpack.NewEncoder(buf) 19 | 20 | enc.EncodeMapLen(1) 21 | enc.EncodeUint8(uint8(iproto.IPROTO_DATA)) 22 | enc.Encode([]interface{}{data}) 23 | return buf 24 | 25 | } 26 | 27 | func TestDecodeBaseResponse(t *testing.T) { 28 | tests := []struct { 29 | name string 30 | header tarantool.Header 31 | body interface{} 32 | }{ 33 | { 34 | "test1", 35 | tarantool.Header{}, 36 | nil, 37 | }, 38 | { 39 | "test2", 40 | tarantool.Header{RequestId: 123}, 41 | []byte{'v', '2'}, 42 | }, 43 | } 44 | for _, tt := range tests { 45 | t.Run(tt.name, func(t *testing.T) { 46 | res, err := tarantool.DecodeBaseResponse(tt.header, encodeResponseData(t, tt.body)) 47 | require.NoError(t, err) 48 | require.Equal(t, tt.header, res.Header()) 49 | 50 | got, err := res.Decode() 51 | require.NoError(t, err) 52 | require.Equal(t, []interface{}{tt.body}, got) 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /schema_test.go: -------------------------------------------------------------------------------- 1 | package tarantool_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | "github.com/vmihailenco/msgpack/v5" 10 | 11 | "github.com/tarantool/go-tarantool/v2" 12 | "github.com/tarantool/go-tarantool/v2/test_helpers" 13 | ) 14 | 15 | func TestGetSchema_ok(t *testing.T) { 16 | space1 := tarantool.Space{ 17 | Id: 1, 18 | Name: "name1", 19 | Indexes: make(map[string]tarantool.Index), 20 | IndexesById: make(map[uint32]tarantool.Index), 21 | Fields: make(map[string]tarantool.Field), 22 | FieldsById: make(map[uint32]tarantool.Field), 23 | } 24 | index := tarantool.Index{ 25 | Id: 1, 26 | SpaceId: 2, 27 | Name: "index_name", 28 | Type: "index_type", 29 | Unique: true, 30 | Fields: make([]tarantool.IndexField, 0), 31 | } 32 | space2 := tarantool.Space{ 33 | Id: 2, 34 | Name: "name2", 35 | Indexes: map[string]tarantool.Index{ 36 | "index_name": index, 37 | }, 38 | IndexesById: map[uint32]tarantool.Index{ 39 | 1: index, 40 | }, 41 | Fields: make(map[string]tarantool.Field), 42 | FieldsById: make(map[uint32]tarantool.Field), 43 | } 44 | 45 | mockDoer := test_helpers.NewMockDoer(t, 46 | test_helpers.NewMockResponse(t, [][]interface{}{ 47 | { 48 | uint32(1), 49 | "skip", 50 | "name1", 51 | "", 52 | 0, 53 | }, 54 | { 55 | uint32(2), 56 | "skip", 57 | "name2", 58 | "", 59 | 0, 60 | }, 61 | }), 62 | test_helpers.NewMockResponse(t, [][]interface{}{ 63 | { 64 | uint32(2), 65 | uint32(1), 66 | "index_name", 67 | "index_type", 68 | uint8(1), 69 | uint8(0), 70 | }, 71 | }), 72 | ) 73 | 74 | expectedSchema := tarantool.Schema{ 75 | SpacesById: map[uint32]tarantool.Space{ 76 | 1: space1, 77 | 2: space2, 78 | }, 79 | Spaces: map[string]tarantool.Space{ 80 | "name1": space1, 81 | "name2": space2, 82 | }, 83 | } 84 | 85 | schema, err := tarantool.GetSchema(&mockDoer) 86 | require.NoError(t, err) 87 | require.Equal(t, expectedSchema, schema) 88 | } 89 | 90 | func TestGetSchema_spaces_select_error(t *testing.T) { 91 | mockDoer := test_helpers.NewMockDoer(t, fmt.Errorf("some error")) 92 | 93 | schema, err := tarantool.GetSchema(&mockDoer) 94 | require.EqualError(t, err, "some error") 95 | require.Equal(t, tarantool.Schema{}, schema) 96 | } 97 | 98 | func TestGetSchema_index_select_error(t *testing.T) { 99 | mockDoer := test_helpers.NewMockDoer(t, 100 | test_helpers.NewMockResponse(t, [][]interface{}{ 101 | { 102 | uint32(1), 103 | "skip", 104 | "name1", 105 | "", 106 | 0, 107 | }, 108 | }), 109 | fmt.Errorf("some error")) 110 | 111 | schema, err := tarantool.GetSchema(&mockDoer) 112 | require.EqualError(t, err, "some error") 113 | require.Equal(t, tarantool.Schema{}, schema) 114 | } 115 | 116 | func TestResolverCalledWithoutNameSupport(t *testing.T) { 117 | resolver := ValidSchemeResolver{nameUseSupported: false} 118 | 119 | req := tarantool.NewSelectRequest("valid") 120 | req.Index("valid") 121 | 122 | var reqBuf bytes.Buffer 123 | reqEnc := msgpack.NewEncoder(&reqBuf) 124 | 125 | err := req.Body(&resolver, reqEnc) 126 | if err != nil { 127 | t.Errorf("An unexpected Response.Body() error: %q", err.Error()) 128 | } 129 | 130 | if resolver.spaceResolverCalls != 1 { 131 | t.Errorf("ResolveSpace was called %d times instead of 1.", 132 | resolver.spaceResolverCalls) 133 | } 134 | if resolver.indexResolverCalls != 1 { 135 | t.Errorf("ResolveIndex was called %d times instead of 1.", 136 | resolver.indexResolverCalls) 137 | } 138 | } 139 | 140 | func TestResolverNotCalledWithNameSupport(t *testing.T) { 141 | resolver := ValidSchemeResolver{nameUseSupported: true} 142 | 143 | req := tarantool.NewSelectRequest("valid") 144 | req.Index("valid") 145 | 146 | var reqBuf bytes.Buffer 147 | reqEnc := msgpack.NewEncoder(&reqBuf) 148 | 149 | err := req.Body(&resolver, reqEnc) 150 | if err != nil { 151 | t.Errorf("An unexpected Response.Body() error: %q", err.Error()) 152 | } 153 | 154 | if resolver.spaceResolverCalls != 0 { 155 | t.Errorf("ResolveSpace was called %d times instead of 0.", 156 | resolver.spaceResolverCalls) 157 | } 158 | if resolver.indexResolverCalls != 0 { 159 | t.Errorf("ResolveIndex was called %d times instead of 0.", 160 | resolver.indexResolverCalls) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /settings/const.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | const sessionSettingsSpace string = "_session_settings" 4 | 5 | // In Go and IPROTO_UPDATE count starts with 0. 6 | const sessionSettingValueField int = 1 7 | 8 | const ( 9 | errorMarshalingEnabled string = "error_marshaling_enabled" 10 | sqlDefaultEngine string = "sql_default_engine" 11 | sqlDeferForeignKeys string = "sql_defer_foreign_keys" 12 | sqlFullColumnNames string = "sql_full_column_names" 13 | sqlFullMetadata string = "sql_full_metadata" 14 | sqlParserDebug string = "sql_parser_debug" 15 | sqlRecursiveTriggers string = "sql_recursive_triggers" 16 | sqlReverseUnorderedSelects string = "sql_reverse_unordered_selects" 17 | sqlSelectDebug string = "sql_select_debug" 18 | sqlVDBEDebug string = "sql_vdbe_debug" 19 | ) 20 | 21 | const selectAllLimit uint32 = 1000 22 | -------------------------------------------------------------------------------- /settings/example_test.go: -------------------------------------------------------------------------------- 1 | package settings_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/tarantool/go-tarantool/v2" 9 | "github.com/tarantool/go-tarantool/v2/settings" 10 | "github.com/tarantool/go-tarantool/v2/test_helpers" 11 | ) 12 | 13 | var exampleDialer = tarantool.NetDialer{ 14 | Address: "127.0.0.1", 15 | User: "test", 16 | Password: "test", 17 | } 18 | 19 | var exampleOpts = tarantool.Opts{ 20 | Timeout: 5 * time.Second, 21 | } 22 | 23 | func example_connect(dialer tarantool.Dialer, opts tarantool.Opts) *tarantool.Connection { 24 | ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) 25 | defer cancel() 26 | conn, err := tarantool.Connect(ctx, dialer, opts) 27 | if err != nil { 28 | panic("Connection is not established: " + err.Error()) 29 | } 30 | return conn 31 | } 32 | 33 | func Example_sqlFullColumnNames() { 34 | var resp tarantool.Response 35 | var err error 36 | var isLess bool 37 | 38 | conn := example_connect(exampleDialer, exampleOpts) 39 | defer conn.Close() 40 | 41 | // Tarantool supports session settings since version 2.3.1 42 | isLess, err = test_helpers.IsTarantoolVersionLess(2, 3, 1) 43 | if err != nil || isLess { 44 | return 45 | } 46 | 47 | // Create a space. 48 | req := tarantool.NewExecuteRequest("CREATE TABLE example(id INT PRIMARY KEY, x INT);") 49 | _, err = conn.Do(req).Get() 50 | if err != nil { 51 | fmt.Printf("error in create table: %v\n", err) 52 | return 53 | } 54 | 55 | // Insert some tuple into space. 56 | req = tarantool.NewExecuteRequest("INSERT INTO example VALUES (1, 1);") 57 | _, err = conn.Do(req).Get() 58 | if err != nil { 59 | fmt.Printf("error on insert: %v\n", err) 60 | return 61 | } 62 | 63 | // Enable showing full column names in SQL responses. 64 | _, err = conn.Do(settings.NewSQLFullColumnNamesSetRequest(true)).Get() 65 | if err != nil { 66 | fmt.Printf("error on setting setup: %v\n", err) 67 | return 68 | } 69 | 70 | // Get some data with SQL query. 71 | req = tarantool.NewExecuteRequest("SELECT x FROM example WHERE id = 1;") 72 | resp, err = conn.Do(req).GetResponse() 73 | if err != nil { 74 | fmt.Printf("error on select: %v\n", err) 75 | return 76 | } 77 | 78 | exResp, ok := resp.(*tarantool.ExecuteResponse) 79 | if !ok { 80 | fmt.Printf("wrong response type") 81 | return 82 | } 83 | 84 | metaData, err := exResp.MetaData() 85 | if err != nil { 86 | fmt.Printf("error on getting MetaData: %v\n", err) 87 | return 88 | } 89 | // Show response metadata. 90 | fmt.Printf("full column name: %v\n", metaData[0].FieldName) 91 | 92 | // Disable showing full column names in SQL responses. 93 | _, err = conn.Do(settings.NewSQLFullColumnNamesSetRequest(false)).Get() 94 | if err != nil { 95 | fmt.Printf("error on setting setup: %v\n", err) 96 | return 97 | } 98 | 99 | // Get some data with SQL query. 100 | resp, err = conn.Do(req).GetResponse() 101 | if err != nil { 102 | fmt.Printf("error on select: %v\n", err) 103 | return 104 | } 105 | exResp, ok = resp.(*tarantool.ExecuteResponse) 106 | if !ok { 107 | fmt.Printf("wrong response type") 108 | return 109 | } 110 | metaData, err = exResp.MetaData() 111 | if err != nil { 112 | fmt.Printf("error on getting MetaData: %v\n", err) 113 | return 114 | } 115 | // Show response metadata. 116 | fmt.Printf("short column name: %v\n", metaData[0].FieldName) 117 | } 118 | -------------------------------------------------------------------------------- /settings/request_test.go: -------------------------------------------------------------------------------- 1 | package settings_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | "github.com/tarantool/go-iproto" 10 | "github.com/vmihailenco/msgpack/v5" 11 | 12 | "github.com/tarantool/go-tarantool/v2" 13 | . "github.com/tarantool/go-tarantool/v2/settings" 14 | ) 15 | 16 | type ValidSchemeResolver struct { 17 | } 18 | 19 | func (*ValidSchemeResolver) ResolveSpace(s interface{}) (uint32, error) { 20 | return 0, nil 21 | } 22 | 23 | func (*ValidSchemeResolver) ResolveIndex(i interface{}, spaceNo uint32) (uint32, error) { 24 | return 0, nil 25 | } 26 | 27 | func (r *ValidSchemeResolver) NamesUseSupported() bool { 28 | return false 29 | } 30 | 31 | var resolver ValidSchemeResolver 32 | 33 | func TestRequestsAPI(t *testing.T) { 34 | tests := []struct { 35 | req tarantool.Request 36 | async bool 37 | rtype iproto.Type 38 | }{ 39 | {req: NewErrorMarshalingEnabledSetRequest(false), async: false, 40 | rtype: iproto.IPROTO_UPDATE}, 41 | {req: NewErrorMarshalingEnabledGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, 42 | {req: NewSQLDefaultEngineSetRequest("memtx"), async: false, rtype: iproto.IPROTO_UPDATE}, 43 | {req: NewSQLDefaultEngineGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, 44 | {req: NewSQLDeferForeignKeysSetRequest(false), async: false, rtype: iproto.IPROTO_UPDATE}, 45 | {req: NewSQLDeferForeignKeysGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, 46 | {req: NewSQLFullColumnNamesSetRequest(false), async: false, rtype: iproto.IPROTO_UPDATE}, 47 | {req: NewSQLFullColumnNamesGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, 48 | {req: NewSQLFullMetadataSetRequest(false), async: false, rtype: iproto.IPROTO_UPDATE}, 49 | {req: NewSQLFullMetadataGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, 50 | {req: NewSQLParserDebugSetRequest(false), async: false, rtype: iproto.IPROTO_UPDATE}, 51 | {req: NewSQLParserDebugGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, 52 | {req: NewSQLRecursiveTriggersSetRequest(false), async: false, rtype: iproto.IPROTO_UPDATE}, 53 | {req: NewSQLRecursiveTriggersGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, 54 | {req: NewSQLReverseUnorderedSelectsSetRequest(false), async: false, 55 | rtype: iproto.IPROTO_UPDATE}, 56 | {req: NewSQLReverseUnorderedSelectsGetRequest(), async: false, 57 | rtype: iproto.IPROTO_SELECT}, 58 | {req: NewSQLSelectDebugSetRequest(false), async: false, rtype: iproto.IPROTO_UPDATE}, 59 | {req: NewSQLSelectDebugGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, 60 | {req: NewSQLVDBEDebugSetRequest(false), async: false, rtype: iproto.IPROTO_UPDATE}, 61 | {req: NewSQLVDBEDebugGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, 62 | {req: NewSessionSettingsGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, 63 | } 64 | 65 | for _, test := range tests { 66 | require.Equal(t, test.async, test.req.Async()) 67 | require.Equal(t, test.rtype, test.req.Type()) 68 | 69 | var reqBuf bytes.Buffer 70 | enc := msgpack.NewEncoder(&reqBuf) 71 | require.Nilf(t, test.req.Body(&resolver, enc), "No errors on fill") 72 | } 73 | } 74 | 75 | func TestRequestsCtx(t *testing.T) { 76 | // tarantool.Request interface doesn't have Context() 77 | getTests := []struct { 78 | req *GetRequest 79 | }{ 80 | {req: NewErrorMarshalingEnabledGetRequest()}, 81 | {req: NewSQLDefaultEngineGetRequest()}, 82 | {req: NewSQLDeferForeignKeysGetRequest()}, 83 | {req: NewSQLFullColumnNamesGetRequest()}, 84 | {req: NewSQLFullMetadataGetRequest()}, 85 | {req: NewSQLParserDebugGetRequest()}, 86 | {req: NewSQLRecursiveTriggersGetRequest()}, 87 | {req: NewSQLReverseUnorderedSelectsGetRequest()}, 88 | {req: NewSQLSelectDebugGetRequest()}, 89 | {req: NewSQLVDBEDebugGetRequest()}, 90 | {req: NewSessionSettingsGetRequest()}, 91 | } 92 | 93 | for _, test := range getTests { 94 | var ctx context.Context 95 | require.Equal(t, ctx, test.req.Context(ctx).Ctx()) 96 | } 97 | 98 | setTests := []struct { 99 | req *SetRequest 100 | }{ 101 | {req: NewErrorMarshalingEnabledSetRequest(false)}, 102 | {req: NewSQLDefaultEngineSetRequest("memtx")}, 103 | {req: NewSQLDeferForeignKeysSetRequest(false)}, 104 | {req: NewSQLFullColumnNamesSetRequest(false)}, 105 | {req: NewSQLFullMetadataSetRequest(false)}, 106 | {req: NewSQLParserDebugSetRequest(false)}, 107 | {req: NewSQLRecursiveTriggersSetRequest(false)}, 108 | {req: NewSQLReverseUnorderedSelectsSetRequest(false)}, 109 | {req: NewSQLSelectDebugSetRequest(false)}, 110 | {req: NewSQLVDBEDebugSetRequest(false)}, 111 | } 112 | 113 | for _, test := range setTests { 114 | var ctx context.Context 115 | require.Equal(t, ctx, test.req.Context(ctx).Ctx()) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /settings/testdata/config.lua: -------------------------------------------------------------------------------- 1 | -- Do not set listen for now so connector won't be 2 | -- able to send requests until everything is configured. 3 | box.cfg{ 4 | work_dir = os.getenv("TEST_TNT_WORK_DIR"), 5 | } 6 | 7 | box.schema.user.create('test', { password = 'test' , if_not_exists = true }) 8 | box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true }) 9 | box.schema.user.grant('test', 'create,read,write,drop,alter', 'space', nil, { if_not_exists = true }) 10 | box.schema.user.grant('test', 'create', 'sequence', nil, { if_not_exists = true }) 11 | 12 | -- Set listen only when every other thing is configured. 13 | box.cfg{ 14 | listen = os.getenv("TEST_TNT_LISTEN"), 15 | } 16 | -------------------------------------------------------------------------------- /smallbuf.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | ) 7 | 8 | type smallBuf struct { 9 | b []byte 10 | p int 11 | } 12 | 13 | func (s *smallBuf) Read(d []byte) (l int, err error) { 14 | l = len(s.b) - s.p 15 | if l == 0 && len(d) > 0 { 16 | return 0, io.EOF 17 | } 18 | if l > len(d) { 19 | l = len(d) 20 | } 21 | copy(d, s.b[s.p:]) 22 | s.p += l 23 | return l, nil 24 | } 25 | 26 | func (s *smallBuf) ReadByte() (b byte, err error) { 27 | if s.p == len(s.b) { 28 | return 0, io.EOF 29 | } 30 | b = s.b[s.p] 31 | s.p++ 32 | return b, nil 33 | } 34 | 35 | func (s *smallBuf) UnreadByte() error { 36 | if s.p == 0 { 37 | return errors.New("could not unread") 38 | } 39 | s.p-- 40 | return nil 41 | } 42 | 43 | func (s *smallBuf) Len() int { 44 | return len(s.b) - s.p 45 | } 46 | 47 | func (s *smallBuf) Bytes() []byte { 48 | if len(s.b) > s.p { 49 | return s.b[s.p:] 50 | } 51 | return nil 52 | } 53 | 54 | func (s *smallBuf) Offset() int { 55 | return s.p 56 | } 57 | 58 | func (s *smallBuf) Seek(offset int) error { 59 | if offset < 0 { 60 | return errors.New("too small offset") 61 | } 62 | if offset > len(s.b) { 63 | return errors.New("too big offset") 64 | } 65 | s.p = offset 66 | return nil 67 | } 68 | 69 | type smallWBuf struct { 70 | b []byte 71 | sum uint 72 | n uint 73 | } 74 | 75 | func (s *smallWBuf) Write(b []byte) (int, error) { 76 | s.b = append(s.b, b...) 77 | return len(s.b), nil 78 | } 79 | 80 | func (s *smallWBuf) WriteByte(b byte) error { 81 | s.b = append(s.b, b) 82 | return nil 83 | } 84 | 85 | func (s *smallWBuf) WriteString(ss string) (int, error) { 86 | s.b = append(s.b, ss...) 87 | return len(ss), nil 88 | } 89 | 90 | func (s smallWBuf) Len() int { 91 | return len(s.b) 92 | } 93 | 94 | func (s smallWBuf) Cap() int { 95 | return cap(s.b) 96 | } 97 | 98 | func (s *smallWBuf) Trunc(n int) { 99 | s.b = s.b[:n] 100 | } 101 | 102 | func (s *smallWBuf) Reset() { 103 | s.sum = uint(uint64(s.sum)*15/16) + uint(len(s.b)) 104 | if s.n < 16 { 105 | s.n++ 106 | } 107 | if cap(s.b) > 1024 && s.sum/s.n < uint(cap(s.b))/4 { 108 | s.b = make([]byte, 0, s.sum/s.n) 109 | } else { 110 | s.b = s.b[:0] 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /test_helpers/doer.go: -------------------------------------------------------------------------------- 1 | package test_helpers 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/tarantool/go-tarantool/v2" 8 | ) 9 | 10 | type doerResponse struct { 11 | resp *MockResponse 12 | err error 13 | } 14 | 15 | // MockDoer is an implementation of the Doer interface 16 | // used for testing purposes. 17 | type MockDoer struct { 18 | // Requests is a slice of received requests. 19 | // It could be used to compare incoming requests with expected. 20 | Requests []tarantool.Request 21 | responses []doerResponse 22 | t *testing.T 23 | } 24 | 25 | // NewMockDoer creates a MockDoer by given responses. 26 | // Each response could be one of two types: MockResponse or error. 27 | func NewMockDoer(t *testing.T, responses ...interface{}) MockDoer { 28 | t.Helper() 29 | 30 | mockDoer := MockDoer{t: t} 31 | for _, response := range responses { 32 | doerResp := doerResponse{} 33 | 34 | switch resp := response.(type) { 35 | case *MockResponse: 36 | doerResp.resp = resp 37 | case error: 38 | doerResp.err = resp 39 | default: 40 | t.Fatalf("unsupported type: %T", response) 41 | } 42 | 43 | mockDoer.responses = append(mockDoer.responses, doerResp) 44 | } 45 | return mockDoer 46 | } 47 | 48 | // Do returns a future with the current response or an error. 49 | // It saves the current request into MockDoer.Requests. 50 | func (doer *MockDoer) Do(req tarantool.Request) *tarantool.Future { 51 | doer.Requests = append(doer.Requests, req) 52 | 53 | mockReq := NewMockRequest() 54 | fut := tarantool.NewFuture(mockReq) 55 | 56 | if len(doer.responses) == 0 { 57 | doer.t.Fatalf("list of responses is empty") 58 | } 59 | response := doer.responses[0] 60 | 61 | if response.err != nil { 62 | fut.SetError(response.err) 63 | } else { 64 | fut.SetResponse(response.resp.header, bytes.NewBuffer(response.resp.data)) 65 | } 66 | doer.responses = doer.responses[1:] 67 | 68 | return fut 69 | } 70 | -------------------------------------------------------------------------------- /test_helpers/example_test.go: -------------------------------------------------------------------------------- 1 | package test_helpers_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/tarantool/go-tarantool/v2" 9 | "github.com/tarantool/go-tarantool/v2/test_helpers" 10 | ) 11 | 12 | func TestExampleMockDoer(t *testing.T) { 13 | mockDoer := test_helpers.NewMockDoer(t, 14 | test_helpers.NewMockResponse(t, []interface{}{"some data"}), 15 | fmt.Errorf("some error"), 16 | test_helpers.NewMockResponse(t, "some typed data"), 17 | fmt.Errorf("some error"), 18 | ) 19 | 20 | data, err := mockDoer.Do(tarantool.NewPingRequest()).Get() 21 | assert.NoError(t, err) 22 | assert.Equal(t, []interface{}{"some data"}, data) 23 | 24 | data, err = mockDoer.Do(tarantool.NewSelectRequest("foo")).Get() 25 | assert.EqualError(t, err, "some error") 26 | assert.Nil(t, data) 27 | 28 | var stringData string 29 | err = mockDoer.Do(tarantool.NewInsertRequest("space")).GetTyped(&stringData) 30 | assert.NoError(t, err) 31 | assert.Equal(t, "some typed data", stringData) 32 | 33 | err = mockDoer.Do(tarantool.NewPrepareRequest("expr")).GetTyped(&stringData) 34 | assert.EqualError(t, err, "some error") 35 | assert.Nil(t, data) 36 | } 37 | -------------------------------------------------------------------------------- /test_helpers/request.go: -------------------------------------------------------------------------------- 1 | package test_helpers 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/tarantool/go-iproto" 8 | "github.com/vmihailenco/msgpack/v5" 9 | 10 | "github.com/tarantool/go-tarantool/v2" 11 | ) 12 | 13 | // MockRequest is an empty mock request used for testing purposes. 14 | type MockRequest struct { 15 | } 16 | 17 | // NewMockRequest creates an empty MockRequest. 18 | func NewMockRequest() *MockRequest { 19 | return &MockRequest{} 20 | } 21 | 22 | // Type returns an iproto type for MockRequest. 23 | func (req *MockRequest) Type() iproto.Type { 24 | return iproto.Type(0) 25 | } 26 | 27 | // Async returns if MockRequest expects a response. 28 | func (req *MockRequest) Async() bool { 29 | return false 30 | } 31 | 32 | // Body fills an msgpack.Encoder with the watch request body. 33 | func (req *MockRequest) Body(resolver tarantool.SchemaResolver, enc *msgpack.Encoder) error { 34 | return nil 35 | } 36 | 37 | // Conn returns the Connection object the request belongs to. 38 | func (req *MockRequest) Conn() *tarantool.Connection { 39 | return &tarantool.Connection{} 40 | } 41 | 42 | // Ctx returns a context of the MockRequest. 43 | func (req *MockRequest) Ctx() context.Context { 44 | return nil 45 | } 46 | 47 | // Response creates a response for the MockRequest. 48 | func (req *MockRequest) Response(header tarantool.Header, 49 | body io.Reader) (tarantool.Response, error) { 50 | resp, err := CreateMockResponse(header, body) 51 | return resp, err 52 | } 53 | -------------------------------------------------------------------------------- /test_helpers/response.go: -------------------------------------------------------------------------------- 1 | package test_helpers 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/vmihailenco/msgpack/v5" 9 | 10 | "github.com/tarantool/go-tarantool/v2" 11 | ) 12 | 13 | // MockResponse is a mock response used for testing purposes. 14 | type MockResponse struct { 15 | // header contains response header 16 | header tarantool.Header 17 | // data contains data inside a response. 18 | data []byte 19 | } 20 | 21 | // NewMockResponse creates a new MockResponse with an empty header and the given data. 22 | // body should be passed as a structure to be encoded. 23 | // The encoded body is served as response data and will be decoded once the 24 | // response is decoded. 25 | func NewMockResponse(t *testing.T, body interface{}) *MockResponse { 26 | t.Helper() 27 | 28 | buf := bytes.NewBuffer([]byte{}) 29 | enc := msgpack.NewEncoder(buf) 30 | 31 | err := enc.Encode(body) 32 | if err != nil { 33 | t.Errorf("unexpected error while encoding: %s", err) 34 | } 35 | 36 | return &MockResponse{data: buf.Bytes()} 37 | } 38 | 39 | // CreateMockResponse creates a MockResponse from the header and a data, 40 | // packed inside an io.Reader. 41 | func CreateMockResponse(header tarantool.Header, body io.Reader) (*MockResponse, error) { 42 | if body == nil { 43 | return &MockResponse{header: header, data: nil}, nil 44 | } 45 | data, err := io.ReadAll(body) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return &MockResponse{header: header, data: data}, nil 50 | } 51 | 52 | // Header returns a header for the MockResponse. 53 | func (resp *MockResponse) Header() tarantool.Header { 54 | return resp.header 55 | } 56 | 57 | // Decode returns the result of decoding the response data as slice. 58 | func (resp *MockResponse) Decode() ([]interface{}, error) { 59 | if resp.data == nil { 60 | return nil, nil 61 | } 62 | dec := msgpack.NewDecoder(bytes.NewBuffer(resp.data)) 63 | return dec.DecodeSlice() 64 | } 65 | 66 | // DecodeTyped returns the result of decoding the response data. 67 | func (resp *MockResponse) DecodeTyped(res interface{}) error { 68 | if resp.data == nil { 69 | return nil 70 | } 71 | dec := msgpack.NewDecoder(bytes.NewBuffer(resp.data)) 72 | return dec.Decode(res) 73 | } 74 | -------------------------------------------------------------------------------- /test_helpers/tcs/prepare.go: -------------------------------------------------------------------------------- 1 | package tcs 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "text/template" 9 | "time" 10 | 11 | "github.com/tarantool/go-tarantool/v2" 12 | "github.com/tarantool/go-tarantool/v2/test_helpers" 13 | ) 14 | 15 | const ( 16 | waitTimeout = 500 * time.Millisecond 17 | connectRetry = 3 18 | tcsUser = "client" 19 | tcsPassword = "secret" 20 | ) 21 | 22 | //go:embed testdata/config.yaml 23 | var tcsConfig []byte 24 | 25 | func writeConfig(name string, port int) error { 26 | cfg, err := os.Create(name) 27 | if err != nil { 28 | return err 29 | } 30 | defer cfg.Close() 31 | 32 | cfg.Chmod(0644) 33 | 34 | t := template.Must(template.New("config").Parse(string(tcsConfig))) 35 | return t.Execute(cfg, map[string]interface{}{ 36 | "host": "localhost", 37 | "port": port, 38 | }) 39 | } 40 | 41 | func makeOpts(port int) (test_helpers.StartOpts, error) { 42 | opts := test_helpers.StartOpts{} 43 | var err error 44 | opts.WorkDir, err = os.MkdirTemp("", "tcs_dir") 45 | if err != nil { 46 | return opts, err 47 | } 48 | 49 | opts.ConfigFile = filepath.Join(opts.WorkDir, "config.yaml") 50 | err = writeConfig(opts.ConfigFile, port) 51 | if err != nil { 52 | return opts, fmt.Errorf("can't save file %q: %w", opts.ConfigFile, err) 53 | } 54 | 55 | opts.Listen = fmt.Sprintf("localhost:%d", port) 56 | opts.WaitStart = waitTimeout 57 | opts.ConnectRetry = connectRetry 58 | opts.RetryTimeout = waitTimeout 59 | opts.InstanceName = "master" 60 | opts.Dialer = tarantool.NetDialer{ 61 | Address: opts.Listen, 62 | User: tcsUser, 63 | Password: tcsPassword, 64 | } 65 | return opts, nil 66 | } 67 | -------------------------------------------------------------------------------- /test_helpers/tcs/testdata/config.yaml: -------------------------------------------------------------------------------- 1 | credentials: 2 | users: 3 | replicator: 4 | password: "topsecret" 5 | roles: [replication] 6 | client: 7 | password: "secret" 8 | privileges: 9 | - permissions: [execute] 10 | universe: true 11 | - permissions: [read, write] 12 | spaces: [config_storage, config_storage_meta] 13 | 14 | iproto: 15 | advertise: 16 | peer: 17 | login: replicator 18 | 19 | replication: 20 | failover: election 21 | 22 | database: 23 | use_mvcc_engine: true 24 | 25 | groups: 26 | group-001: 27 | replicasets: 28 | replicaset-001: 29 | roles: [config.storage] 30 | roles_cfg: 31 | config_storage: 32 | status_check_interval: 3 33 | instances: 34 | master: 35 | iproto: 36 | listen: 37 | - uri: "{{.host}}:{{.port}}" 38 | params: 39 | transport: plain 40 | -------------------------------------------------------------------------------- /testdata/sidecar/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "strconv" 7 | 8 | "github.com/tarantool/go-tarantool/v2" 9 | ) 10 | 11 | func main() { 12 | fd, err := strconv.Atoi(os.Getenv("SOCKET_FD")) 13 | if err != nil { 14 | panic(err) 15 | } 16 | dialer := tarantool.FdDialer{ 17 | Fd: uintptr(fd), 18 | } 19 | conn, err := tarantool.Connect(context.Background(), dialer, tarantool.Opts{}) 20 | if err != nil { 21 | panic(err) 22 | } 23 | if _, err := conn.Do(tarantool.NewPingRequest()).Get(); err != nil { 24 | panic(err) 25 | } 26 | // Insert new tuple. 27 | if _, err := conn.Do(tarantool.NewInsertRequest("test"). 28 | Tuple([]interface{}{239})).Get(); err != nil { 29 | panic(err) 30 | } 31 | // Delete inserted tuple. 32 | if _, err := conn.Do(tarantool.NewDeleteRequest("test"). 33 | Index("primary"). 34 | Key([]interface{}{239})).Get(); err != nil { 35 | panic(err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /uuid/config.lua: -------------------------------------------------------------------------------- 1 | local uuid = require('uuid') 2 | local msgpack = require('msgpack') 3 | 4 | -- Do not set listen for now so connector won't be 5 | -- able to send requests until everything is configured. 6 | box.cfg{ 7 | work_dir = os.getenv("TEST_TNT_WORK_DIR"), 8 | } 9 | 10 | box.schema.user.create('test', { password = 'test' , if_not_exists = true }) 11 | box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true }) 12 | 13 | local uuid_msgpack_supported = pcall(msgpack.encode, uuid.new()) 14 | if not uuid_msgpack_supported then 15 | error('UUID unsupported, use Tarantool 2.4.1 or newer') 16 | end 17 | 18 | local s = box.schema.space.create('testUUID', { 19 | id = 524, 20 | if_not_exists = true, 21 | }) 22 | s:create_index('primary', { 23 | type = 'tree', 24 | parts = {{ field = 1, type = 'uuid' }}, 25 | if_not_exists = true 26 | }) 27 | s:truncate() 28 | 29 | box.schema.user.grant('test', 'read,write', 'space', 'testUUID', { if_not_exists = true }) 30 | 31 | s:insert({ uuid.fromstr("c8f0fa1f-da29-438c-a040-393f1126ad39") }) 32 | 33 | -- Set listen only when every other thing is configured. 34 | box.cfg{ 35 | listen = os.getenv("TEST_TNT_LISTEN"), 36 | } 37 | -------------------------------------------------------------------------------- /uuid/example_test.go: -------------------------------------------------------------------------------- 1 | // Run Tarantool instance before example execution: 2 | // Terminal 1: 3 | // $ cd uuid 4 | // $ TEST_TNT_LISTEN=3013 TEST_TNT_WORK_DIR=$(mktemp -d -t 'tarantool.XXX') tarantool config.lua 5 | // 6 | // Terminal 2: 7 | // $ cd uuid 8 | // $ go test -v example_test.go 9 | package uuid_test 10 | 11 | import ( 12 | "context" 13 | "fmt" 14 | "log" 15 | "time" 16 | 17 | "github.com/google/uuid" 18 | "github.com/tarantool/go-tarantool/v2" 19 | _ "github.com/tarantool/go-tarantool/v2/uuid" 20 | ) 21 | 22 | var exampleOpts = tarantool.Opts{ 23 | Timeout: 5 * time.Second, 24 | } 25 | 26 | // Example demonstrates how to use tuples with UUID. To enable UUID support 27 | // in msgpack with google/uuid (https://github.com/google/uuid), import 28 | // tarantool/uuid submodule. 29 | func Example() { 30 | dialer := tarantool.NetDialer{ 31 | Address: "127.0.0.1:3013", 32 | User: "test", 33 | Password: "test", 34 | } 35 | ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) 36 | client, err := tarantool.Connect(ctx, dialer, exampleOpts) 37 | cancel() 38 | if err != nil { 39 | log.Fatalf("Failed to connect: %s", err.Error()) 40 | } 41 | 42 | spaceNo := uint32(524) 43 | 44 | id, uuidErr := uuid.Parse("c8f0fa1f-da29-438c-a040-393f1126ad39") 45 | if uuidErr != nil { 46 | log.Fatalf("Failed to prepare uuid: %s", uuidErr) 47 | } 48 | 49 | data, err := client.Do(tarantool.NewReplaceRequest(spaceNo). 50 | Tuple([]interface{}{id}), 51 | ).Get() 52 | 53 | fmt.Println("UUID tuple replace") 54 | fmt.Println("Error", err) 55 | fmt.Println("Data", data) 56 | } 57 | -------------------------------------------------------------------------------- /uuid/uuid.go: -------------------------------------------------------------------------------- 1 | // Package with support of Tarantool's UUID data type. 2 | // 3 | // UUID data type supported in Tarantool since 2.4.1. 4 | // 5 | // Since: 1.6.0. 6 | // 7 | // # See also 8 | // 9 | // - Tarantool commit with UUID support: 10 | // https://github.com/tarantool/tarantool/commit/d68fc29246714eee505bc9bbcd84a02de17972c5 11 | // 12 | // - Tarantool data model: 13 | // https://www.tarantool.io/en/doc/latest/book/box/data_model/ 14 | // 15 | // - Module UUID: 16 | // https://www.tarantool.io/en/doc/latest/reference/reference_lua/uuid/ 17 | package uuid 18 | 19 | import ( 20 | "fmt" 21 | "reflect" 22 | 23 | "github.com/google/uuid" 24 | "github.com/vmihailenco/msgpack/v5" 25 | ) 26 | 27 | // UUID external type. 28 | const uuid_extID = 2 29 | 30 | func encodeUUID(e *msgpack.Encoder, v reflect.Value) error { 31 | id := v.Interface().(uuid.UUID) 32 | 33 | bytes, err := id.MarshalBinary() 34 | if err != nil { 35 | return fmt.Errorf("msgpack: can't marshal binary uuid: %w", err) 36 | } 37 | 38 | _, err = e.Writer().Write(bytes) 39 | if err != nil { 40 | return fmt.Errorf("msgpack: can't write bytes to msgpack.Encoder writer: %w", err) 41 | } 42 | 43 | return nil 44 | } 45 | 46 | func decodeUUID(d *msgpack.Decoder, v reflect.Value) error { 47 | var bytesCount = 16 48 | bytes := make([]byte, bytesCount) 49 | 50 | n, err := d.Buffered().Read(bytes) 51 | if err != nil { 52 | return fmt.Errorf("msgpack: can't read bytes on uuid decode: %w", err) 53 | } 54 | if n < bytesCount { 55 | return fmt.Errorf("msgpack: unexpected end of stream after %d uuid bytes", n) 56 | } 57 | 58 | id, err := uuid.FromBytes(bytes) 59 | if err != nil { 60 | return fmt.Errorf("msgpack: can't create uuid from bytes: %w", err) 61 | } 62 | 63 | v.Set(reflect.ValueOf(id)) 64 | return nil 65 | } 66 | 67 | func init() { 68 | msgpack.Register(reflect.TypeOf((*uuid.UUID)(nil)).Elem(), encodeUUID, decodeUUID) 69 | msgpack.RegisterExtEncoder(uuid_extID, uuid.UUID{}, 70 | func(e *msgpack.Encoder, v reflect.Value) ([]byte, error) { 71 | uuid := v.Interface().(uuid.UUID) 72 | return uuid.MarshalBinary() 73 | }) 74 | msgpack.RegisterExtDecoder(uuid_extID, uuid.UUID{}, 75 | func(d *msgpack.Decoder, v reflect.Value, extLen int) error { 76 | return decodeUUID(d, v) 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /watch.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/tarantool/go-iproto" 8 | "github.com/vmihailenco/msgpack/v5" 9 | ) 10 | 11 | // BroadcastRequest helps to send broadcast messages. See: 12 | // https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_events/broadcast/ 13 | type BroadcastRequest struct { 14 | call *CallRequest 15 | key string 16 | } 17 | 18 | // NewBroadcastRequest returns a new broadcast request for a specified key. 19 | func NewBroadcastRequest(key string) *BroadcastRequest { 20 | req := new(BroadcastRequest) 21 | req.key = key 22 | req.call = NewCallRequest("box.broadcast").Args([]interface{}{key}) 23 | return req 24 | } 25 | 26 | // Value sets the value for the broadcast request. 27 | // Note: default value is nil. 28 | func (req *BroadcastRequest) Value(value interface{}) *BroadcastRequest { 29 | req.call = req.call.Args([]interface{}{req.key, value}) 30 | return req 31 | } 32 | 33 | // Context sets a passed context to the broadcast request. 34 | func (req *BroadcastRequest) Context(ctx context.Context) *BroadcastRequest { 35 | req.call = req.call.Context(ctx) 36 | return req 37 | } 38 | 39 | // Code returns IPROTO code for the broadcast request. 40 | func (req *BroadcastRequest) Type() iproto.Type { 41 | return req.call.Type() 42 | } 43 | 44 | // Body fills an msgpack.Encoder with the broadcast request body. 45 | func (req *BroadcastRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { 46 | return req.call.Body(res, enc) 47 | } 48 | 49 | // Ctx returns a context of the broadcast request. 50 | func (req *BroadcastRequest) Ctx() context.Context { 51 | return req.call.Ctx() 52 | } 53 | 54 | // Async returns is the broadcast request expects a response. 55 | func (req *BroadcastRequest) Async() bool { 56 | return req.call.Async() 57 | } 58 | 59 | // Response creates a response for a BroadcastRequest. 60 | func (req *BroadcastRequest) Response(header Header, body io.Reader) (Response, error) { 61 | return DecodeBaseResponse(header, body) 62 | } 63 | 64 | // watchRequest subscribes to the updates of a specified key defined on the 65 | // server. After receiving the notification, you should send a new 66 | // watchRequest to acknowledge the notification. 67 | type watchRequest struct { 68 | baseRequest 69 | key string 70 | ctx context.Context 71 | } 72 | 73 | // newWatchRequest returns a new watchRequest. 74 | func newWatchRequest(key string) *watchRequest { 75 | req := new(watchRequest) 76 | req.rtype = iproto.IPROTO_WATCH 77 | req.async = true 78 | req.key = key 79 | return req 80 | } 81 | 82 | // Body fills an msgpack.Encoder with the watch request body. 83 | func (req *watchRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { 84 | if err := enc.EncodeMapLen(1); err != nil { 85 | return err 86 | } 87 | if err := enc.EncodeUint(uint64(iproto.IPROTO_EVENT_KEY)); err != nil { 88 | return err 89 | } 90 | return enc.EncodeString(req.key) 91 | } 92 | 93 | // Context sets a passed context to the request. 94 | func (req *watchRequest) Context(ctx context.Context) *watchRequest { 95 | req.ctx = ctx 96 | return req 97 | } 98 | 99 | // unwatchRequest unregisters a watcher subscribed to the given notification 100 | // key. 101 | type unwatchRequest struct { 102 | baseRequest 103 | key string 104 | ctx context.Context 105 | } 106 | 107 | // newUnwatchRequest returns a new unwatchRequest. 108 | func newUnwatchRequest(key string) *unwatchRequest { 109 | req := new(unwatchRequest) 110 | req.rtype = iproto.IPROTO_UNWATCH 111 | req.async = true 112 | req.key = key 113 | return req 114 | } 115 | 116 | // Body fills an msgpack.Encoder with the unwatch request body. 117 | func (req *unwatchRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { 118 | if err := enc.EncodeMapLen(1); err != nil { 119 | return err 120 | } 121 | if err := enc.EncodeUint(uint64(iproto.IPROTO_EVENT_KEY)); err != nil { 122 | return err 123 | } 124 | return enc.EncodeString(req.key) 125 | } 126 | 127 | // Context sets a passed context to the request. 128 | func (req *unwatchRequest) Context(ctx context.Context) *unwatchRequest { 129 | req.ctx = ctx 130 | return req 131 | } 132 | 133 | // WatchEvent is a watch notification event received from a server. 134 | type WatchEvent struct { 135 | Conn *Connection // A source connection. 136 | Key string // A key. 137 | Value interface{} // A value. 138 | } 139 | 140 | // Watcher is a subscription to broadcast events. 141 | type Watcher interface { 142 | // Unregister unregisters the watcher. 143 | Unregister() 144 | } 145 | 146 | // WatchCallback is a callback to invoke when the key value is updated. 147 | type WatchCallback func(event WatchEvent) 148 | --------------------------------------------------------------------------------