From d7925800514ec04b4b0818e4eb0da5d5c3a26227 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 12 Jan 2023 17:30:44 +0300 Subject: [PATCH] api: add pagination support A user could fetch a position of a last tuple using a new method of the SelectRequest type: selectRequest = selectRequest.FetchPos(true) The position will be stored in a new field of the Response type: Response.Pos A user could specify a tuple from which selection must continue or its position with a new method of the SelectRequest type: selectRequest = selectRequest.After([]interface{}{23}) or selectRequest = selectRequest.After(resp.Pos) In action it looks like: req := NewSelectRequest(space).Key(key).Limit(10).FetchPos(true) for condition { resp, err := conn.Do(req).Get() // ... req = req.After(resp.Pos) } 1. https://github.com/tarantool/tarantool/issues/7639 Part of #246 --- CHANGELOG.md | 2 + const.go | 4 + example_test.go | 2 +- export_test.go | 5 +- protocol.go | 5 +- request.go | 141 ++++++++++++++++++++---- request_test.go | 46 ++++++-- response.go | 17 ++- tarantool_test.go | 249 ++++++++++++++++++++++++++++++++++++------ test_helpers/utils.go | 8 ++ 10 files changed, 406 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bc276b18..acfe2a914 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +- Support pagination (#246) + ### Changed ### Fixed diff --git a/const.go b/const.go index ead151878..29bcc101b 100644 --- a/const.go +++ b/const.go @@ -30,16 +30,20 @@ const ( KeyLimit = 0x12 KeyOffset = 0x13 KeyIterator = 0x14 + KeyFetchPos = 0x1f KeyKey = 0x20 KeyTuple = 0x21 KeyFunctionName = 0x22 KeyUserName = 0x23 KeyExpression = 0x27 + KeyAfterPos = 0x2e + KeyAfterTuple = 0x2f KeyDefTuple = 0x28 KeyData = 0x30 KeyError24 = 0x31 /* Error in pre-2.4 format. */ KeyMetaData = 0x32 KeyBindCount = 0x34 + KeyPos = 0x35 KeySQLText = 0x40 KeySQLBind = 0x41 KeySQLInfo = 0x42 diff --git a/example_test.go b/example_test.go index 27a962da0..7b0ee9a6a 100644 --- a/example_test.go +++ b/example_test.go @@ -329,7 +329,7 @@ func ExampleProtocolVersion() { fmt.Println("Connector client protocol features:", clientProtocolInfo.Features) // Output: // Connector client protocol version: 4 - // Connector client protocol features: [StreamsFeature TransactionsFeature ErrorExtensionFeature WatchersFeature] + // Connector client protocol features: [StreamsFeature TransactionsFeature ErrorExtensionFeature WatchersFeature PaginationFeature] } func getTestTxnOpts() tarantool.Opts { diff --git a/export_test.go b/export_test.go index 464a85844..71886bb20 100644 --- a/export_test.go +++ b/export_test.go @@ -23,8 +23,9 @@ func RefImplPingBody(enc *encoder) error { // RefImplSelectBody is reference implementation for filling of a select // request's body. -func RefImplSelectBody(enc *encoder, space, index, offset, limit, iterator uint32, key interface{}) error { - return fillSelect(enc, space, index, offset, limit, iterator, key) +func RefImplSelectBody(enc *encoder, space, index, offset, limit, iterator uint32, + key, after interface{}, fetchPos bool) error { + return fillSelect(enc, space, index, offset, limit, iterator, key, after, fetchPos) } // RefImplInsertBody is reference implementation for filling of an insert diff --git a/protocol.go b/protocol.go index 8252f538a..ae8dce306 100644 --- a/protocol.go +++ b/protocol.go @@ -47,7 +47,7 @@ const ( // (supported by connector). WatchersFeature ProtocolFeature = 3 // PaginationFeature represents support of pagination - // (unsupported by connector). + // (supported by connector). PaginationFeature ProtocolFeature = 4 ) @@ -83,11 +83,14 @@ var clientProtocolInfo ProtocolInfo = ProtocolInfo{ // version 2 (Tarantool 2.10.0), in connector since 1.10.0. // Watchers were introduced in protocol version 3 (Tarantool 2.10.0), in // connector since 1.10.0. + // Pagination were introduced in protocol version 4 (Tarantool 2.11.0), in + // connector since 1.11.0. Features: []ProtocolFeature{ StreamsFeature, TransactionsFeature, ErrorExtensionFeature, WatchersFeature, + PaginationFeature, }, } diff --git a/request.go b/request.go index 1b135eaa6..55e36292d 100644 --- a/request.go +++ b/request.go @@ -10,35 +10,103 @@ import ( ) func fillSearch(enc *encoder, spaceNo, indexNo uint32, key interface{}) error { - encodeUint(enc, KeySpaceNo) - encodeUint(enc, uint64(spaceNo)) - encodeUint(enc, KeyIndexNo) - encodeUint(enc, uint64(indexNo)) - encodeUint(enc, KeyKey) + if err := encodeUint(enc, KeySpaceNo); err != nil { + return err + } + if err := encodeUint(enc, uint64(spaceNo)); err != nil { + return err + } + if err := encodeUint(enc, KeyIndexNo); err != nil { + return err + } + if err := encodeUint(enc, uint64(indexNo)); err != nil { + return err + } + if err := encodeUint(enc, KeyKey); err != nil { + return err + } return enc.Encode(key) } -func fillIterator(enc *encoder, offset, limit, iterator uint32) { - encodeUint(enc, KeyIterator) - encodeUint(enc, uint64(iterator)) - encodeUint(enc, KeyOffset) - encodeUint(enc, uint64(offset)) - encodeUint(enc, KeyLimit) - encodeUint(enc, uint64(limit)) +func fillIterator(enc *encoder, offset, limit, iterator uint32) error { + if err := encodeUint(enc, KeyIterator); err != nil { + return err + } + if err := encodeUint(enc, uint64(iterator)); err != nil { + return err + } + if err := encodeUint(enc, KeyOffset); err != nil { + return err + } + if err := encodeUint(enc, uint64(offset)); err != nil { + return err + } + if err := encodeUint(enc, KeyLimit); err != nil { + return err + } + return encodeUint(enc, uint64(limit)) } func fillInsert(enc *encoder, spaceNo uint32, tuple interface{}) error { - enc.EncodeMapLen(2) - encodeUint(enc, KeySpaceNo) - encodeUint(enc, uint64(spaceNo)) - encodeUint(enc, KeyTuple) + if err := enc.EncodeMapLen(2); err != nil { + return err + } + if err := encodeUint(enc, KeySpaceNo); err != nil { + return err + } + if err := encodeUint(enc, uint64(spaceNo)); err != nil { + return err + } + if err := encodeUint(enc, KeyTuple); err != nil { + return err + } return enc.Encode(tuple) } -func fillSelect(enc *encoder, spaceNo, indexNo, offset, limit, iterator uint32, key interface{}) error { - enc.EncodeMapLen(6) - fillIterator(enc, offset, limit, iterator) - return fillSearch(enc, spaceNo, indexNo, key) +func fillSelect(enc *encoder, spaceNo, indexNo, offset, limit, iterator uint32, + key, after interface{}, fetchPos bool) error { + mapLen := 6 + if fetchPos { + mapLen += 1 + } + if after != nil { + mapLen += 1 + } + if err := enc.EncodeMapLen(mapLen); err != nil { + return err + } + if err := fillIterator(enc, offset, limit, iterator); err != nil { + return err + } + if err := fillSearch(enc, spaceNo, indexNo, key); err != nil { + return err + } + if fetchPos { + if err := encodeUint(enc, KeyFetchPos); err != nil { + return err + } + if err := enc.EncodeBool(fetchPos); err != nil { + return err + } + } + if after != nil { + if pos, ok := after.([]byte); ok { + if err := encodeUint(enc, KeyAfterPos); err != nil { + return err + } + if err := enc.EncodeString(string(pos)); err != nil { + return err + } + } else { + if err := encodeUint(enc, KeyAfterTuple); err != nil { + return err + } + if err := enc.Encode(after); err != nil { + return err + } + } + } + return nil } func fillUpdate(enc *encoder, spaceNo, indexNo uint32, key, ops interface{}) error { @@ -660,9 +728,9 @@ func (req *PingRequest) Context(ctx context.Context) *PingRequest { // by a Connection. type SelectRequest struct { spaceIndexRequest - isIteratorSet bool + isIteratorSet, fetchPos bool offset, limit, iterator uint32 - key interface{} + key, after interface{} } // NewSelectRequest returns a new empty SelectRequest. @@ -671,8 +739,10 @@ func NewSelectRequest(space interface{}) *SelectRequest { req.requestCode = SelectRequestCode req.setSpace(space) req.isIteratorSet = false + req.fetchPos = false req.iterator = IterAll req.key = []interface{}{} + req.after = nil req.limit = 0xFFFFFFFF return req } @@ -716,6 +786,30 @@ func (req *SelectRequest) Key(key interface{}) *SelectRequest { return req } +// FetchPos determines whether to fetch positions of the last tuple. A position +// descriptor will be saved in Response.Pos value. +// +// Note: default value is false. +// +// Requires Tarantool >= 2.11. +// Since 1.11.0 +func (req *SelectRequest) FetchPos(fetch bool) *SelectRequest { + req.fetchPos = fetch + return req +} + +// After must contain a tuple from which selection must continue or its +// position (a value from Response.Pos). +// +// Note: default value in nil. +// +// Requires Tarantool >= 2.11. +// Since 1.11.0 +func (req *SelectRequest) After(after interface{}) *SelectRequest { + req.after = after + return req +} + // Body fills an encoder with the select request body. func (req *SelectRequest) Body(res SchemaResolver, enc *encoder) error { spaceNo, indexNo, err := res.ResolveSpaceIndex(req.space, req.index) @@ -723,7 +817,8 @@ func (req *SelectRequest) Body(res SchemaResolver, enc *encoder) error { return err } - return fillSelect(enc, spaceNo, indexNo, req.offset, req.limit, req.iterator, req.key) + return fillSelect(enc, spaceNo, indexNo, req.offset, req.limit, req.iterator, + req.key, req.after, req.fetchPos) } // Context sets a passed context to the request. diff --git a/request_test.go b/request_test.go index 70aa29f45..32bdd87e1 100644 --- a/request_test.go +++ b/request_test.go @@ -319,7 +319,8 @@ func TestSelectRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer refEnc := NewEncoder(&refBuf) - err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, IterAll, []interface{}{}) + err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, + IterAll, []interface{}{}, nil, false) if err != nil { t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) return @@ -334,7 +335,8 @@ func TestSelectRequestDefaultIteratorEqIfKey(t *testing.T) { key := []interface{}{uint(18)} refEnc := NewEncoder(&refBuf) - err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, IterEq, key) + err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, + IterEq, key, nil, false) if err != nil { t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) return @@ -351,7 +353,8 @@ func TestSelectRequestIteratorNotChangedIfKey(t *testing.T) { const iter = IterGe refEnc := NewEncoder(&refBuf) - err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, iter, key) + err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, + iter, key, nil, false) if err != nil { t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) return @@ -368,22 +371,45 @@ func TestSelectRequestSetters(t *testing.T) { const limit = 5 const iter = IterLt key := []interface{}{uint(36)} - var refBuf bytes.Buffer + afterBytes := []byte{0x1, 0x2, 0x3} + afterKey := []interface{}{uint(13)} + var refBufAfterBytes, refBufAfterKey bytes.Buffer - refEnc := NewEncoder(&refBuf) - err := RefImplSelectBody(refEnc, validSpace, validIndex, offset, limit, iter, key) + refEncAfterBytes := NewEncoder(&refBufAfterBytes) + err := RefImplSelectBody(refEncAfterBytes, validSpace, validIndex, offset, + limit, iter, key, afterBytes, true) if err != nil { - t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) + t.Errorf("An unexpected RefImplSelectBody() error %s", err) return } - req := NewSelectRequest(validSpace). + refEncAfterKey := NewEncoder(&refBufAfterKey) + err = RefImplSelectBody(refEncAfterKey, validSpace, validIndex, offset, + limit, iter, key, afterKey, true) + if err != nil { + t.Errorf("An unexpected RefImplSelectBody() error %s", err) + return + } + + reqAfterBytes := NewSelectRequest(validSpace). Index(validIndex). Offset(offset). Limit(limit). Iterator(iter). - Key(key) - assertBodyEqual(t, refBuf.Bytes(), req) + Key(key). + After(afterBytes). + FetchPos(true) + reqAfterKey := NewSelectRequest(validSpace). + Index(validIndex). + Offset(offset). + Limit(limit). + Iterator(iter). + Key(key). + After(afterKey). + FetchPos(true) + + assertBodyEqual(t, refBufAfterBytes.Bytes(), reqAfterBytes) + assertBodyEqual(t, refBufAfterKey.Bytes(), reqAfterKey) } func TestInsertRequestDefaultValues(t *testing.T) { diff --git a/response.go b/response.go index dc747c852..e5486b0a4 100644 --- a/response.go +++ b/response.go @@ -7,9 +7,12 @@ import ( type Response struct { RequestId uint32 Code uint32 - Error string // error message - // Data contains deserialized data for untyped requests - Data []interface{} + // Error contains an error message. + Error string + // Data contains deserialized data for untyped requests. + Data []interface{} + // Pos contains a position descriptor of last selected tuple. + Pos []byte MetaData []ColumnMetaData SQLInfo SQLInfo buf smallBuf @@ -228,6 +231,10 @@ func (resp *Response) decodeBody() (err error) { if !found { return fmt.Errorf("unknown auth type %s", auth) } + case KeyPos: + if resp.Pos, err = d.DecodeBytes(); err != nil { + return fmt.Errorf("unable to decode a position: %w", err) + } default: if err = d.Skip(); err != nil { return err @@ -300,6 +307,10 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { if err = d.Decode(&resp.MetaData); err != nil { return err } + case KeyPos: + if resp.Pos, err = d.DecodeBytes(); err != nil { + return fmt.Errorf("unable to decode a position: %w", err) + } default: if err = d.Skip(); err != nil { return err diff --git a/tarantool_test.go b/tarantool_test.go index dc02d2c17..3c11aa4ea 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -850,6 +850,9 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Ping") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } // Insert resp, err = conn.Insert(spaceNo, []interface{}{uint(1), "hello", "world"}) @@ -859,6 +862,9 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Insert") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) != 1 { t.Errorf("Response Body len != 1") } @@ -892,6 +898,9 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Delete") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) != 1 { t.Errorf("Response Body len != 1") } @@ -915,6 +924,9 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Delete") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) != 0 { t.Errorf("Response Data len != 0") } @@ -925,7 +937,10 @@ func TestClient(t *testing.T) { t.Fatalf("Failed to Replace: %s", err.Error()) } if resp == nil { - t.Errorf("Response is nil after Replace") + t.Fatalf("Response is nil after Replace") + } + if resp.Pos != nil { + t.Errorf("Response should not have a position") } resp, err = conn.Replace(spaceNo, []interface{}{uint(2), "hi", "planet"}) if err != nil { @@ -959,6 +974,9 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Update") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) != 1 { t.Errorf("Response Data len != 1") } @@ -982,7 +1000,10 @@ func TestClient(t *testing.T) { t.Fatalf("Failed to Upsert (insert): %s", err.Error()) } if resp == nil { - t.Errorf("Response is nil after Upsert (insert)") + t.Fatalf("Response is nil after Upsert (insert)") + } + if resp.Pos != nil { + t.Errorf("Response should not have a position") } resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, []interface{}{[]interface{}{"+", 1, 1}}) if err != nil { @@ -998,6 +1019,9 @@ func TestClient(t *testing.T) { if err != nil { t.Fatalf("Failed to Replace: %s", err.Error()) } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if resp.Code != 0 { t.Errorf("Failed to replace") } @@ -1009,6 +1033,9 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Select") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) != 1 { t.Errorf("Response Data len != 1") } @@ -1031,6 +1058,9 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Select") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) != 0 { t.Errorf("Response Data len != 0") } @@ -1101,6 +1131,9 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Call16") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after Eval") } @@ -1110,6 +1143,9 @@ func TestClient(t *testing.T) { if err != nil { t.Errorf("Failed to use Call16") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if val, ok := resp.Data[0].([]interface{})[0].(string); !ok || val != "11" { t.Errorf("result is not {{1}} : %v", resp.Data) } @@ -1118,6 +1154,9 @@ func TestClient(t *testing.T) { if err != nil { t.Errorf("Failed to use Call") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if val, ok := resp.Data[0].(string); !ok || val != "11" { t.Errorf("result is not {{1}} : %v", resp.Data) } @@ -1130,6 +1169,9 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Eval") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after Eval") } @@ -1201,6 +1243,9 @@ func TestClientSessionPush(t *testing.T) { t.Errorf("Response is empty after it.Next() == true") break } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after CallAsync") break @@ -2090,6 +2135,9 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Insert") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if resp.Data == nil { t.Fatalf("Response data is nil after Insert") } @@ -2125,6 +2173,9 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Replace") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if resp.Data == nil { t.Fatalf("Response data is nil after Replace") } @@ -2159,6 +2210,9 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Delete") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if resp.Data == nil { t.Fatalf("Response data is nil after Delete") } @@ -2193,6 +2247,9 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Update") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if resp.Data == nil { t.Fatalf("Response data is nil after Update") } @@ -2225,6 +2282,9 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Update") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if resp.Data == nil { t.Fatalf("Response data is nil after Update") } @@ -2255,6 +2315,9 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Upsert (update)") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if resp.Data == nil { t.Fatalf("Response data is nil after Upsert") } @@ -2273,6 +2336,9 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Upsert (update)") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if resp.Data == nil { t.Fatalf("Response data is nil after Upsert") } @@ -2280,43 +2346,15 @@ func TestClientRequestObjects(t *testing.T) { t.Fatalf("Response Data len != 0") } - // Select. - req = NewSelectRequest(spaceNo). - Index(indexNo). - Limit(20). - Iterator(IterGe). - Key([]interface{}{uint(1010)}) - resp, err = conn.Do(req).Get() - if err != nil { - t.Errorf("Failed to Select: %s", err.Error()) - } - if resp == nil { - t.Errorf("Response is nil after Select") - return - } - if len(resp.Data) != 9 { - t.Fatalf("Response Data len %d != 9", len(resp.Data)) - } - if tpl, ok := resp.Data[0].([]interface{}); !ok { - t.Errorf("Unexpected body of Select") - } else { - if id, err := convertUint64(tpl[0]); err != nil || id != 1010 { - t.Errorf("Unexpected body of Select (0) %v, expected %d", tpl[0], 1010) - } - if h, ok := tpl[1].(string); !ok || h != "bye" { - t.Errorf("Unexpected body of Select (1) %q, expected %q", tpl[1].(string), "bye") - } - if h, ok := tpl[2].(string); !ok || h != "bye" { - t.Errorf("Unexpected body of Select (2) %q, expected %q", tpl[2].(string), "bye") - } - } - // Call16 vs Call17 req = NewCall16Request("simple_concat").Args([]interface{}{"1"}) resp, err = conn.Do(req).Get() if err != nil { t.Errorf("Failed to use Call") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if val, ok := resp.Data[0].([]interface{})[0].(string); !ok || val != "11" { t.Errorf("result is not {{1}} : %v", resp.Data) } @@ -2327,6 +2365,9 @@ func TestClientRequestObjects(t *testing.T) { if err != nil { t.Errorf("Failed to use Call17") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if val, ok := resp.Data[0].(string); !ok || val != "11" { t.Errorf("result is not {{1}} : %v", resp.Data) } @@ -2340,6 +2381,9 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Eval") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after Eval") } @@ -2364,6 +2408,9 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatal("Response is nil after Execute") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) != 0 { t.Fatalf("Response Body len != 0") } @@ -2382,6 +2429,9 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatal("Response is nil after Execute") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) != 0 { t.Fatalf("Response Body len != 0") } @@ -2393,6 +2443,137 @@ func TestClientRequestObjects(t *testing.T) { } } +func testConnectionDoSelectRequestPrepare(t *testing.T, conn Connector) { + t.Helper() + + for i := 1010; i < 1020; i++ { + req := NewReplaceRequest(spaceName).Tuple( + []interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"}) + if _, err := conn.Do(req).Get(); err != nil { + t.Fatalf("Unable to prepare tuples: %s", err) + } + } +} + +func testConnectionDoSelectRequestCheck(t *testing.T, + resp *Response, err error, pos bool, dataLen int, firstKey uint64) { + t.Helper() + + if err != nil { + t.Fatalf("Failed to Select: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + if !pos && resp.Pos != nil { + t.Errorf("Response should not have a position descriptor") + } + if pos && resp.Pos == nil { + t.Fatalf("A response must have a position descriptor") + } + if len(resp.Data) != dataLen { + t.Fatalf("Response Data len %d != %d", len(resp.Data), dataLen) + } + for i := 0; i < dataLen; i++ { + key := firstKey + uint64(i) + if tpl, ok := resp.Data[i].([]interface{}); !ok { + t.Errorf("Unexpected body of Select") + } else { + if id, err := convertUint64(tpl[0]); err != nil || id != key { + t.Errorf("Unexpected body of Select (0) %v, expected %d", + tpl[0], key) + } + expectedSecond := fmt.Sprintf("val %d", key) + if h, ok := tpl[1].(string); !ok || h != expectedSecond { + t.Errorf("Unexpected body of Select (1) %q, expected %q", + tpl[1].(string), expectedSecond) + } + if h, ok := tpl[2].(string); !ok || h != "bla" { + t.Errorf("Unexpected body of Select (2) %q, expected %q", + tpl[2].(string), "bla") + } + } + } +} + +func TestConnectionDoSelectRequest(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + testConnectionDoSelectRequestPrepare(t, conn) + + req := NewSelectRequest(spaceNo). + Index(indexNo). + Limit(20). + Iterator(IterGe). + Key([]interface{}{uint(1010)}) + resp, err := conn.Do(req).Get() + + testConnectionDoSelectRequestCheck(t, resp, err, false, 10, 1010) +} + +func TestConnectionDoSelectRequest_fetch_pos(t *testing.T) { + test_helpers.SkipIfPaginationUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + testConnectionDoSelectRequestPrepare(t, conn) + + req := NewSelectRequest(spaceNo). + Index(indexNo). + Limit(2). + Iterator(IterGe). + FetchPos(true). + Key([]interface{}{uint(1010)}) + resp, err := conn.Do(req).Get() + + testConnectionDoSelectRequestCheck(t, resp, err, true, 2, 1010) +} + +func TestConnectDoSelectRequest_after_tuple(t *testing.T) { + test_helpers.SkipIfPaginationUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + testConnectionDoSelectRequestPrepare(t, conn) + + req := NewSelectRequest(spaceNo). + Index(indexNo). + Limit(2). + Iterator(IterGe). + FetchPos(true). + Key([]interface{}{uint(1010)}). + After([]interface{}{uint(1012)}) + resp, err := conn.Do(req).Get() + + testConnectionDoSelectRequestCheck(t, resp, err, true, 2, 1013) +} + +func TestConnectionDoSelectRequest_pagination_pos(t *testing.T) { + test_helpers.SkipIfPaginationUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + testConnectionDoSelectRequestPrepare(t, conn) + + req := NewSelectRequest(spaceNo). + Index(indexNo). + Limit(2). + Iterator(IterGe). + FetchPos(true). + Key([]interface{}{uint(1010)}) + resp, err := conn.Do(req).Get() + + testConnectionDoSelectRequestCheck(t, resp, err, true, 2, 1010) + + resp, err = conn.Do(req.After(resp.Pos)).Get() + + testConnectionDoSelectRequestCheck(t, resp, err, true, 2, 1012) +} + func TestClientRequestObjectsWithNilContext(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() @@ -2943,6 +3124,7 @@ func TestConnectionProtocolInfoSupported(t *testing.T) { TransactionsFeature, ErrorExtensionFeature, WatchersFeature, + PaginationFeature, }, }) @@ -3059,6 +3241,7 @@ func TestConnectionProtocolInfoUnsupported(t *testing.T) { TransactionsFeature, ErrorExtensionFeature, WatchersFeature, + PaginationFeature, }, }) diff --git a/test_helpers/utils.go b/test_helpers/utils.go index f1002e954..12c65009e 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -182,6 +182,14 @@ func SkipIfErrorMessagePackTypeUnsupported(t *testing.T) { SkipIfFeatureUnsupported(t, "error type in MessagePack", 2, 10, 0) } +// SkipIfPaginationUnsupported skips test run if Tarantool without +// pagination is used. +func SkipIfPaginationUnsupported(t *testing.T) { + t.Helper() + + SkipIfFeatureUnsupported(t, "pagination", 2, 11, 0) +} + // CheckEqualBoxErrors checks equivalence of tarantool.BoxError objects. // // Tarantool errors are not comparable by nature: