diff --git a/.github/workflows/ort.yml b/.github/workflows/ort.yml index 403560286d..d88f2287fd 100644 --- a/.github/workflows/ort.yml +++ b/.github/workflows/ort.yml @@ -158,6 +158,17 @@ jobs: distribution: "temurin" java-version: 11 + - name: Install protoc (protobuf) + uses: arduino/setup-protoc@v3 + with: + version: "29.1" + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build java artifact + working-directory: ./java + run: | + ./gradlew publishToMavenLocal -x buildRust -x javadoc + - name: Run ORT tools for Java uses: ./.github/workflows/run-ort-tools with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 98a99a17aa..299180a2a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ * Go: Add `ZCARD` ([#2838](https://github.com/valkey-io/valkey-glide/pull/2838)) * Java, Node, Python: Update documentation for CONFIG SET and CONFIG GET ([#2919](https://github.com/valkey-io/valkey-glide/pull/2919)) * Go: Add `BZPopMin` ([#2849](https://github.com/valkey-io/valkey-glide/pull/2849)) +* Java: Shadow `protobuf` dependency ([#2931](https://github.com/valkey-io/valkey-glide/pull/2931)) * Java: Add `RESP2` support ([#2383](https://github.com/valkey-io/valkey-glide/pull/2383)) * Node: Add `IFEQ` option ([#2909](https://github.com/valkey-io/valkey-glide/pull/2909)) diff --git a/go/Makefile b/go/Makefile index 62eabbaa8b..014bf962a4 100644 --- a/go/Makefile +++ b/go/Makefile @@ -82,15 +82,15 @@ unit-test: mkdir -p reports set -o pipefail; \ LD_LIBRARY_PATH=$(shell find . -name libglide_rs.so|grep -w release|tail -1|xargs dirname|xargs readlink -f):${LD_LIBRARY_PATH} \ - go test -v -race ./... -skip TestGlideTestSuite $(if $(test-filter), -run $(test-filter)) \ + go test -v -race ./... -skip TestGlideTestSuite $(if $(test-filter), -testify.m $(test-filter)) \ | tee >(go tool test2json -t -p github.com/valkey-io/valkey-glide/go/glide/utils | go-test-report -o reports/unit-tests.html -t unit-test > /dev/null) # integration tests - run subtask with skipping modules tests -integ-test: export TEST_FILTER = -skip TestGlideTestSuite/TestModule $(if $(test-filter), -run $(test-filter)) +integ-test: export TEST_FILTER = -skip TestGlideTestSuite/TestModule $(if $(test-filter), -testify.m $(test-filter)) integ-test: __it # modules tests - run substask with default filter -modules-test: export TEST_FILTER = $(if $(test-filter), -run $(test-filter), -run TestGlideTestSuite/TestModule) +modules-test: export TEST_FILTER = $(if $(test-filter), -run $(test-filter), -testify.m TestGlideTestSuite/TestModule) modules-test: __it __it: diff --git a/go/api/base_client.go b/go/api/base_client.go index b1922b35ca..104c444cfb 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -484,27 +484,83 @@ func (client *baseClient) HIncrByFloat(key string, field string, increment float return handleFloatResponse(result) } -func (client *baseClient) HScan(key string, cursor string) (Result[string], []Result[string], error) { +// Iterates fields of Hash types and their associated values. This definition of HSCAN command does not include the +// optional arguments of the command. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the hash. +// cursor - The cursor that points to the next iteration of results. A value of "0" indicates the start of the search. +// +// Return value: +// +// An array of the cursor and the subset of the hash held by `key`. The first element is always the `cursor` +// for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the subset. +// The second element is always an array of the subset of the set held in `key`. The array in the +// second element is always a flattened series of String pairs, where the key is at even indices +// and the value is at odd indices. +// +// Example: +// +// // Assume key contains a hash {{"a": "1"}, {"b", "2"}} +// resCursor, resCollection, err = client.HScan(key, initialCursor) +// resCursor = {0 false} +// resCollection = [{a false} {1 false} {b false} {2 false}] +// +// [valkey.io]: https://valkey.io/commands/hscan/ +func (client *baseClient) HScan(key string, cursor string) (string, []string, error) { result, err := client.executeCommand(C.HScan, []string{key, cursor}) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } return handleScanResponse(result) } +// Iterates fields of Hash types and their associated values. This definition of HSCAN includes optional arguments of the +// command. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the hash. +// cursor - The cursor that points to the next iteration of results. A value of "0" indicates the start of the search. +// options - The [api.HashScanOptions]. +// +// Return value: +// +// An array of the cursor and the subset of the hash held by `key`. The first element is always the `cursor` +// for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the subset. +// The second element is always an array of the subset of the set held in `key`. The array in the +// second element is always a flattened series of String pairs, where the key is at even indices +// and the value is at odd indices. +// +// Example: +// +// // Assume key contains a hash {{"a": "1"}, {"b", "2"}} +// opts := options.NewHashScanOptionsBuilder().SetMatch("a") +// resCursor, resCollection, err = client.HScan(key, initialCursor, opts) +// // resCursor = {0 false} +// // resCollection = [{a false} {1 false}] +// // The resCollection only contains the hash map entry that matches with the match option provided with the command +// // input. +// +// [valkey.io]: https://valkey.io/commands/hscan/ func (client *baseClient) HScanWithOptions( key string, cursor string, options *options.HashScanOptions, -) (Result[string], []Result[string], error) { +) (string, []string, error) { optionArgs, err := options.ToArgs() if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } result, err := client.executeCommand(C.HScan, append([]string{key, cursor}, optionArgs...)) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } return handleScanResponse(result) } @@ -735,27 +791,107 @@ func (client *baseClient) SUnion(keys []string) (map[Result[string]]struct{}, er return handleStringSetResponse(result) } -func (client *baseClient) SScan(key string, cursor string) (Result[string], []Result[string], error) { +// Iterates incrementally over a set. +// +// Note: When in cluster mode, all keys must map to the same hash slot. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the set. +// cursor - The cursor that points to the next iteration of results. +// A value of `"0"` indicates the start of the search. +// For Valkey 8.0 and above, negative cursors are treated like the initial cursor("0"). +// +// Return value: +// +// An array of the cursor and the subset of the set held by `key`. The first element is always the `cursor` and +// for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the set. +// The second element is always an array of the subset of the set held in `key`. +// +// Example: +// +// // assume "key" contains a set +// resCursor, resCol, err := client.sscan("key", "0") +// fmt.Println("Cursor: ", resCursor) +// fmt.Println("Members: ", resCol) +// for resCursor != "0" { +// resCursor, resCol, err = client.sscan("key", "0") +// fmt.Println("Cursor: ", resCursor) +// fmt.Println("Members: ", resCol) +// } +// // Output: +// // Cursor: 48 +// // Members: ['3', '118', '120', '86', '76', '13', '61', '111', '55', '45'] +// // Cursor: 24 +// // Members: ['38', '109', '11', '119', '34', '24', '40', '57', '20', '17'] +// // Cursor: 0 +// // Members: ['47', '122', '1', '53', '10', '14', '80'] +// +// [valkey.io]: https://valkey.io/commands/sscan/ +func (client *baseClient) SScan(key string, cursor string) (string, []string, error) { result, err := client.executeCommand(C.SScan, []string{key, cursor}) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } return handleScanResponse(result) } +// Iterates incrementally over a set. +// +// Note: When in cluster mode, all keys must map to the same hash slot. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the set. +// cursor - The cursor that points to the next iteration of results. +// A value of `"0"` indicates the start of the search. +// For Valkey 8.0 and above, negative cursors are treated like the initial cursor("0"). +// options - [options.BaseScanOptions] +// +// Return value: +// +// An array of the cursor and the subset of the set held by `key`. The first element is always the `cursor` and +// for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the set. +// The second element is always an array of the subset of the set held in `key`. +// +// Example: +// +// // assume "key" contains a set +// resCursor, resCol, err := client.sscan("key", "0", opts) +// fmt.Println("Cursor: ", resCursor) +// fmt.Println("Members: ", resCol) +// for resCursor != "0" { +// opts := options.NewBaseScanOptionsBuilder().SetMatch("*") +// resCursor, resCol, err = client.sscan("key", "0", opts) +// fmt.Println("Cursor: ", resCursor) +// fmt.Println("Members: ", resCol) +// } +// // Output: +// // Cursor: 48 +// // Members: ['3', '118', '120', '86', '76', '13', '61', '111', '55', '45'] +// // Cursor: 24 +// // Members: ['38', '109', '11', '119', '34', '24', '40', '57', '20', '17'] +// // Cursor: 0 +// // Members: ['47', '122', '1', '53', '10', '14', '80'] +// +// [valkey.io]: https://valkey.io/commands/sscan/ func (client *baseClient) SScanWithOptions( key string, cursor string, options *options.BaseScanOptions, -) (Result[string], []Result[string], error) { +) (string, []string, error) { optionArgs, err := options.ToArgs() if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } result, err := client.executeCommand(C.SScan, append([]string{key, cursor}, optionArgs...)) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } return handleScanResponse(result) } @@ -1409,6 +1545,129 @@ func (client *baseClient) XReadWithOptions( return handleXReadResponse(result) } +// Reads entries from the given streams owned by a consumer group. +// +// Note: +// +// When in cluster mode, all keys in `keysAndIds` must map to the same hash slot. +// +// See [valkey.io] for details. +// +// Parameters: +// +// group - The consumer group name. +// consumer - The group consumer. +// keysAndIds - A map of keys and entry IDs to read from. +// +// Return value: +// A `map[string]map[string][][]string` of stream keys to a map of stream entry IDs mapped to an array entries or `nil` if +// a key does not exist or does not contain requiested entries. +// +// For example: +// +// result, err := client.XReadGroup({"stream1": "0-0", "stream2": "0-1", "stream3": "0-1"}) +// err == nil: true +// result: map[string]map[string][][]string{ +// "stream1": { +// "0-1": {{"field1", "value1"}}, +// "0-2": {{"field2", "value2"}, {"field2", "value3"}}, +// }, +// "stream2": { +// "1526985676425-0": {{"name", "Virginia"}, {"surname", "Woolf"}}, +// "1526985685298-0": nil, // entry was deleted +// }, +// "stream3": {}, // stream is empty +// } +// +// [valkey.io]: https://valkey.io/commands/xreadgroup/ +func (client *baseClient) XReadGroup( + group string, + consumer string, + keysAndIds map[string]string, +) (map[string]map[string][][]string, error) { + return client.XReadGroupWithOptions(group, consumer, keysAndIds, options.NewXReadGroupOptions()) +} + +// Reads entries from the given streams owned by a consumer group. +// +// Note: +// +// When in cluster mode, all keys in `keysAndIds` must map to the same hash slot. +// +// See [valkey.io] for details. +// +// Parameters: +// +// group - The consumer group name. +// consumer - The group consumer. +// keysAndIds - A map of keys and entry IDs to read from. +// options - Options detailing how to read the stream. +// +// Return value: +// A `map[string]map[string][][]string` of stream keys to a map of stream entry IDs mapped to an array entries or `nil` if +// a key does not exist or does not contain requiested entries. +// +// For example: +// +// options := options.NewXReadGroupOptions().SetNoAck() +// result, err := client.XReadGroupWithOptions({"stream1": "0-0", "stream2": "0-1", "stream3": "0-1"}, options) +// err == nil: true +// result: map[string]map[string][][]string{ +// "stream1": { +// "0-1": {{"field1", "value1"}}, +// "0-2": {{"field2", "value2"}, {"field2", "value3"}}, +// }, +// "stream2": { +// "1526985676425-0": {{"name", "Virginia"}, {"surname", "Woolf"}}, +// "1526985685298-0": nil, // entry was deleted +// }, +// "stream3": {}, // stream is empty +// } +// +// [valkey.io]: https://valkey.io/commands/xreadgroup/ +func (client *baseClient) XReadGroupWithOptions( + group string, + consumer string, + keysAndIds map[string]string, + options *options.XReadGroupOptions, +) (map[string]map[string][][]string, error) { + args, err := createStreamCommandArgs([]string{"GROUP", group, consumer}, keysAndIds, options) + if err != nil { + return nil, err + } + + result, err := client.executeCommand(C.XReadGroup, args) + if err != nil { + return nil, err + } + + return handleXReadGroupResponse(result) +} + +// Combine `args` with `keysAndIds` and `options` into arguments for a stream command +func createStreamCommandArgs( + args []string, + keysAndIds map[string]string, + options interface{ ToArgs() ([]string, error) }, +) ([]string, error) { + optionArgs, err := options.ToArgs() + if err != nil { + return nil, err + } + args = append(args, optionArgs...) + // Note: this loop iterates in an indeterminate order, but it is OK for that case + keys := make([]string, 0, len(keysAndIds)) + values := make([]string, 0, len(keysAndIds)) + for key := range keysAndIds { + keys = append(keys, key) + values = append(values, keysAndIds[key]) + } + args = append(args, "STREAMS") + args = append(args, keys...) + args = append(args, values...) + return args, nil +} + func (client *baseClient) ZAdd( key string, membersScoreMap map[string]float64, @@ -1749,6 +2008,257 @@ func (client *baseClient) XLen(key string) (int64, error) { return handleIntResponse(result) } +// Transfers ownership of pending stream entries that match the specified criteria. +// +// Since: +// +// Valkey 6.2.0 and above. +// +// See [valkey.io] for more details. +// +// Parameters: +// +// key - The key of the stream. +// group - The consumer group name. +// consumer - The group consumer. +// minIdleTime - The minimum idle time for the message to be claimed. +// start - Filters the claimed entries to those that have an ID equal or greater than the specified value. +// +// Return value: +// +// An object containing the following elements: +// - A stream ID to be used as the start argument for the next call to `XAUTOCLAIM`. This ID is +// equivalent to the next ID in the stream after the entries that were scanned, or "0-0" if +// the entire stream was scanned. +// - A map of the claimed entries. +// - If you are using Valkey 7.0.0 or above, the response will also include an array containing +// the message IDs that were in the Pending Entries List but no longer exist in the stream. +// These IDs are deleted from the Pending Entries List. +// +// Example: +// +// result, err := client.XAutoClaim("myStream", "myGroup", "myConsumer", 42, "0-0") +// result: +// // &{ +// // "1609338788321-0" // value to be used as `start` argument for the next `xautoclaim` call +// // map[ +// // "1609338752495-0": [ // claimed entries +// // ["field 1", "value 1"] +// // ["field 2", "value 2"] +// // ] +// // ] +// // [ +// // "1594324506465-0", // array of IDs of deleted messages, +// // "1594568784150-0" // included in the response only on valkey 7.0.0 and above +// // ] +// // } +// +// [valkey.io]: https://valkey.io/commands/xautoclaim/ +func (client *baseClient) XAutoClaim( + key string, + group string, + consumer string, + minIdleTime int64, + start string, +) (XAutoClaimResponse, error) { + return client.XAutoClaimWithOptions(key, group, consumer, minIdleTime, start, nil) +} + +// Transfers ownership of pending stream entries that match the specified criteria. +// +// Since: +// +// Valkey 6.2.0 and above. +// +// See [valkey.io] for more details. +// +// Parameters: +// +// key - The key of the stream. +// group - The consumer group name. +// consumer - The group consumer. +// minIdleTime - The minimum idle time for the message to be claimed. +// start - Filters the claimed entries to those that have an ID equal or greater than the specified value. +// options - Options detailing how to read the stream. +// +// Return value: +// +// An object containing the following elements: +// - A stream ID to be used as the start argument for the next call to `XAUTOCLAIM`. This ID is +// equivalent to the next ID in the stream after the entries that were scanned, or "0-0" if +// the entire stream was scanned. +// - A map of the claimed entries. +// - If you are using Valkey 7.0.0 or above, the response will also include an array containing +// the message IDs that were in the Pending Entries List but no longer exist in the stream. +// These IDs are deleted from the Pending Entries List. +// +// Example: +// +// opts := options.NewXAutoClaimOptionsWithCount(1) +// result, err := client.XAutoClaimWithOptions("myStream", "myGroup", "myConsumer", 42, "0-0", opts) +// result: +// // &{ +// // "1609338788321-0" // value to be used as `start` argument for the next `xautoclaim` call +// // map[ +// // "1609338752495-0": [ // claimed entries +// // ["field 1", "value 1"] +// // ["field 2", "value 2"] +// // ] +// // ] +// // [ +// // "1594324506465-0", // array of IDs of deleted messages, +// // "1594568784150-0" // included in the response only on valkey 7.0.0 and above +// // ] +// // } +// +// [valkey.io]: https://valkey.io/commands/xautoclaim/ +func (client *baseClient) XAutoClaimWithOptions( + key string, + group string, + consumer string, + minIdleTime int64, + start string, + options *options.XAutoClaimOptions, +) (XAutoClaimResponse, error) { + args := []string{key, group, consumer, utils.IntToString(minIdleTime), start} + if options != nil { + optArgs, err := options.ToArgs() + if err != nil { + return XAutoClaimResponse{}, err + } + args = append(args, optArgs...) + } + result, err := client.executeCommand(C.XAutoClaim, args) + if err != nil { + return XAutoClaimResponse{}, err + } + return handleXAutoClaimResponse(result) +} + +// Transfers ownership of pending stream entries that match the specified criteria. +// +// Since: +// +// Valkey 6.2.0 and above. +// +// See [valkey.io] for more details. +// +// Parameters: +// +// key - The key of the stream. +// group - The consumer group name. +// consumer - The group consumer. +// minIdleTime - The minimum idle time for the message to be claimed. +// start - Filters the claimed entries to those that have an ID equal or greater than the specified value. +// +// Return value: +// +// An object containing the following elements: +// - A stream ID to be used as the start argument for the next call to `XAUTOCLAIM`. This ID is +// equivalent to the next ID in the stream after the entries that were scanned, or "0-0" if +// the entire stream was scanned. +// - An array of IDs for the claimed entries. +// - If you are using Valkey 7.0.0 or above, the response will also include an array containing +// the message IDs that were in the Pending Entries List but no longer exist in the stream. +// These IDs are deleted from the Pending Entries List. +// +// Example: +// +// result, err := client.XAutoClaimJustId("myStream", "myGroup", "myConsumer", 42, "0-0") +// result: +// // &{ +// // "1609338788321-0" // value to be used as `start` argument for the next `xautoclaim` call +// // [ +// // "1609338752495-0", // claimed entries +// // "1609338752495-1" +// // ] +// // [ +// // "1594324506465-0", // array of IDs of deleted messages, +// // "1594568784150-0" // included in the response only on valkey 7.0.0 and above +// // ] +// // } +// +// [valkey.io]: https://valkey.io/commands/xautoclaim/ +func (client *baseClient) XAutoClaimJustId( + key string, + group string, + consumer string, + minIdleTime int64, + start string, +) (XAutoClaimJustIdResponse, error) { + return client.XAutoClaimJustIdWithOptions(key, group, consumer, minIdleTime, start, nil) +} + +// Transfers ownership of pending stream entries that match the specified criteria. +// +// Since: +// +// Valkey 6.2.0 and above. +// +// See [valkey.io] for more details. +// +// Parameters: +// +// key - The key of the stream. +// group - The consumer group name. +// consumer - The group consumer. +// minIdleTime - The minimum idle time for the message to be claimed. +// start - Filters the claimed entries to those that have an ID equal or greater than the specified value. +// options - Options detailing how to read the stream. +// +// Return value: +// +// An object containing the following elements: +// - A stream ID to be used as the start argument for the next call to `XAUTOCLAIM`. This ID is +// equivalent to the next ID in the stream after the entries that were scanned, or "0-0" if +// the entire stream was scanned. +// - An array of IDs for the claimed entries. +// - If you are using Valkey 7.0.0 or above, the response will also include an array containing +// the message IDs that were in the Pending Entries List but no longer exist in the stream. +// These IDs are deleted from the Pending Entries List. +// +// Example: +// +// opts := options.NewXAutoClaimOptionsWithCount(1) +// result, err := client.XAutoClaimJustIdWithOptions("myStream", "myGroup", "myConsumer", 42, "0-0", opts) +// result: +// // &{ +// // "1609338788321-0" // value to be used as `start` argument for the next `xautoclaim` call +// // [ +// // "1609338752495-0", // claimed entries +// // "1609338752495-1" +// // ] +// // [ +// // "1594324506465-0", // array of IDs of deleted messages, +// // "1594568784150-0" // included in the response only on valkey 7.0.0 and above +// // ] +// // } +// +// [valkey.io]: https://valkey.io/commands/xautoclaim/ +func (client *baseClient) XAutoClaimJustIdWithOptions( + key string, + group string, + consumer string, + minIdleTime int64, + start string, + options *options.XAutoClaimOptions, +) (XAutoClaimJustIdResponse, error) { + args := []string{key, group, consumer, utils.IntToString(minIdleTime), start} + if options != nil { + optArgs, err := options.ToArgs() + if err != nil { + return XAutoClaimJustIdResponse{}, err + } + args = append(args, optArgs...) + } + args = append(args, "JUSTID") + result, err := client.executeCommand(C.XAutoClaim, args) + if err != nil { + return XAutoClaimJustIdResponse{}, err + } + return handleXAutoClaimJustIdResponse(result) +} + // Removes the specified entries by id from a stream, and returns the number of entries deleted. // // See [valkey.io] for details. @@ -1839,19 +2349,19 @@ func (client *baseClient) ZScore(key string, member string) (Result[float64], er // // // assume "key" contains a set // resCursor, resCol, err := client.ZScan("key", "0") -// fmt.Println(resCursor.Value()) -// fmt.Println(resCol.Value()) +// fmt.Println(resCursor) +// fmt.Println(resCol) // for resCursor != "0" { -// resCursor, resCol, err = client.ZScan("key", resCursor.Value()) -// fmt.Println("Cursor: ", resCursor.Value()) -// fmt.Println("Members: ", resCol.Value()) +// resCursor, resCol, err = client.ZScan("key", resCursor) +// fmt.Println("Cursor: ", resCursor) +// fmt.Println("Members: ", resCol) // } // // [valkey.io]: https://valkey.io/commands/zscan/ -func (client *baseClient) ZScan(key string, cursor string) (Result[string], []Result[string], error) { +func (client *baseClient) ZScan(key string, cursor string) (string, []string, error) { result, err := client.executeCommand(C.ZScan, []string{key, cursor}) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } return handleScanResponse(result) } @@ -1877,13 +2387,13 @@ func (client *baseClient) ZScan(key string, cursor string) (Result[string], []Re // Example: // // resCursor, resCol, err := client.ZScanWithOptions("key", "0", options.NewBaseScanOptionsBuilder().SetMatch("*")) -// fmt.Println(resCursor.Value()) -// fmt.Println(resCol.Value()) +// fmt.Println(resCursor) +// fmt.Println(resCol) // for resCursor != "0" { -// resCursor, resCol, err = client.ZScanWithOptions("key", resCursor.Value(), +// resCursor, resCol, err = client.ZScanWithOptions("key", resCursor, // options.NewBaseScanOptionsBuilder().SetMatch("*")) -// fmt.Println("Cursor: ", resCursor.Value()) -// fmt.Println("Members: ", resCol.Value()) +// fmt.Println("Cursor: ", resCursor) +// fmt.Println("Members: ", resCol) // } // // [valkey.io]: https://valkey.io/commands/zscan/ @@ -1891,15 +2401,470 @@ func (client *baseClient) ZScanWithOptions( key string, cursor string, options *options.ZScanOptions, -) (Result[string], []Result[string], error) { +) (string, []string, error) { optionArgs, err := options.ToArgs() if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } result, err := client.executeCommand(C.ZScan, append([]string{key, cursor}, optionArgs...)) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } return handleScanResponse(result) } + +// Returns stream message summary information for pending messages matching a stream and group. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - The consumer group name. +// +// Return value: +// An XPendingSummary struct that includes a summary with the following fields: +// +// NumOfMessages: The total number of pending messages for this consumer group. +// StartId: The smallest ID among the pending messages or nil if no pending messages exist. +// EndId: The greatest ID among the pending messages or nil if no pending messages exists. +// GroupConsumers: An array of ConsumerPendingMessages with the following fields: +// ConsumerName: The name of the consumer. +// MessageCount: The number of pending messages for this consumer. +// +// Example +// +// result, err := client.XPending("myStream", "myGroup") +// if err != nil { +// return err +// } +// fmt.Println("Number of pending messages: ", result.NumOfMessages) +// fmt.Println("Start and End ID of messages: ", result.StartId, result.EndId) +// for _, consumer := range result.ConsumerMessages { +// fmt.Printf("Consumer messages: %s: $v\n", consumer.ConsumerName, consumer.MessageCount) +// } +// +// [valkey.io]: https://valkey.io/commands/xpending/ +func (client *baseClient) XPending(key string, group string) (XPendingSummary, error) { + result, err := client.executeCommand(C.XPending, []string{key, group}) + if err != nil { + return XPendingSummary{}, err + } + + return handleXPendingSummaryResponse(result) +} + +// Returns stream message summary information for pending messages matching a given range of IDs. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - The consumer group name. +// opts - The options for the command. See [options.XPendingOptions] for details. +// +// Return value: +// A slice of XPendingDetail structs, where each detail struct includes the following fields: +// +// Id - The ID of the pending message. +// ConsumerName - The name of the consumer that fetched the message and has still to acknowledge it. +// IdleTime - The time in milliseconds since the last time the message was delivered to the consumer. +// DeliveryCount - The number of times this message was delivered. +// +// Example +// +// detailResult, err := client.XPendingWithOptions(key, groupName, options.NewXPendingOptions("-", "+", 10)) +// if err != nil { +// return err +// } +// fmt.Println("=========================") +// for _, detail := range detailResult { +// fmt.Println(detail.Id) +// fmt.Println(detail.ConsumerName) +// fmt.Println(detail.IdleTime) +// fmt.Println(detail.DeliveryCount) +// fmt.Println("=========================") +// } +// +// [valkey.io]: https://valkey.io/commands/xpending/ +func (client *baseClient) XPendingWithOptions( + key string, + group string, + opts *options.XPendingOptions, +) ([]XPendingDetail, error) { + optionArgs, _ := opts.ToArgs() + args := append([]string{key, group}, optionArgs...) + + result, err := client.executeCommand(C.XPending, args) + if err != nil { + return nil, err + } + return handleXPendingDetailResponse(result) +} + +// Creates a new consumer group uniquely identified by `groupname` for the stream stored at `key`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - The newly created consumer group name. +// id - Stream entry ID that specifies the last delivered entry in the stream from the new +// group’s perspective. The special ID `"$"` can be used to specify the last entry in the stream. +// +// Return value: +// +// `"OK"`. +// +// Example: +// +// ok, err := client.XGroupCreate("mystream", "mygroup", "0-0") +// if ok != "OK" || err != nil { +// // handle error +// } +// +// [valkey.io]: https://valkey.io/commands/xgroup-create/ +func (client *baseClient) XGroupCreate(key string, group string, id string) (string, error) { + return client.XGroupCreateWithOptions(key, group, id, options.NewXGroupCreateOptions()) +} + +// Creates a new consumer group uniquely identified by `groupname` for the stream stored at `key`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - The newly created consumer group name. +// id - Stream entry ID that specifies the last delivered entry in the stream from the new +// group's perspective. The special ID `"$"` can be used to specify the last entry in the stream. +// opts - The options for the command. See [options.XGroupCreateOptions] for details. +// +// Return value: +// +// `"OK"`. +// +// Example: +// +// opts := options.NewXGroupCreateOptions().SetMakeStream() +// ok, err := client.XGroupCreateWithOptions("mystream", "mygroup", "0-0", opts) +// if ok != "OK" || err != nil { +// // handle error +// } +// +// [valkey.io]: https://valkey.io/commands/xgroup-create/ +func (client *baseClient) XGroupCreateWithOptions( + key string, + group string, + id string, + opts *options.XGroupCreateOptions, +) (string, error) { + optionArgs, _ := opts.ToArgs() + args := append([]string{key, group, id}, optionArgs...) + result, err := client.executeCommand(C.XGroupCreate, args) + if err != nil { + return defaultStringResponse, err + } + return handleStringResponse(result) +} + +func (client *baseClient) Restore(key string, ttl int64, value string) (Result[string], error) { + return client.RestoreWithOptions(key, ttl, value, NewRestoreOptionsBuilder()) +} + +func (client *baseClient) RestoreWithOptions(key string, ttl int64, + value string, options *RestoreOptions, +) (Result[string], error) { + optionArgs, err := options.toArgs() + if err != nil { + return CreateNilStringResult(), err + } + result, err := client.executeCommand(C.Restore, append([]string{ + key, + utils.IntToString(ttl), value, + }, optionArgs...)) + if err != nil { + return CreateNilStringResult(), err + } + return handleStringOrNilResponse(result) +} + +func (client *baseClient) Dump(key string) (Result[string], error) { + result, err := client.executeCommand(C.Dump, []string{key}) + if err != nil { + return CreateNilStringResult(), err + } + return handleStringOrNilResponse(result) +} + +func (client *baseClient) ObjectEncoding(key string) (Result[string], error) { + result, err := client.executeCommand(C.ObjectEncoding, []string{key}) + if err != nil { + return CreateNilStringResult(), err + } + return handleStringOrNilResponse(result) +} + +// Echo the provided message back. +// The command will be routed a random node. +// +// Parameters: +// +// message - The provided message. +// +// Return value: +// +// The provided message +// +// For example: +// +// result, err := client.Echo("Hello World") +// if err != nil { +// // handle error +// } +// fmt.Println(result.Value()) // Output: Hello World +// +// [valkey.io]: https://valkey.io/commands/echo/ +func (client *baseClient) Echo(message string) (Result[string], error) { + result, err := client.executeCommand(C.Echo, []string{message}) + if err != nil { + return CreateNilStringResult(), err + } + return handleStringOrNilResponse(result) +} + +// Removes all elements in the sorted set stored at `key` with a lexicographical order +// between `rangeQuery.Start` and `rangeQuery.End`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the sorted set. +// rangeQuery - The range query object representing the minimum and maximum bound of the lexicographical range. +// can be an implementation of [options.LexBoundary]. +// +// Return value: +// +// The number of members removed from the sorted set. +// If `key` does not exist, it is treated as an empty sorted set, and the command returns `0`. +// If `rangeQuery.Start` is greater than `rangeQuery.End`, `0` is returned. +// +// Example: +// +// zRemRangeByLexResult, err := client.ZRemRangeByLex("key1", options.NewRangeByLexQuery("a", "b")) +// fmt.Println(zRemRangeByLexResult) // Output: 1 +// +// [valkey.io]: https://valkey.io/commands/zremrangebylex/ +func (client *baseClient) ZRemRangeByLex(key string, rangeQuery options.RangeByLex) (int64, error) { + result, err := client.executeCommand( + C.ZRemRangeByLex, append([]string{key}, rangeQuery.ToArgsRemRange()...)) + if err != nil { + return defaultIntResponse, err + } + return handleIntResponse(result) +} + +// Removes all elements in the sorted set stored at `key` with a rank between `start` and `stop`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the sorted set. +// start - The start rank. +// stop - The stop rank. +// +// Return value: +// +// The number of members removed from the sorted set. +// If `key` does not exist, it is treated as an empty sorted set, and the command returns `0`. +// If `start` is greater than `stop`, `0` is returned. +// +// Example: +// +// zRemRangeByRankResult, err := client.ZRemRangeByRank("key1", 0, 1) +// fmt.Println(zRemRangeByRankResult) // Output: 1 +// +// [valkey.io]: https://valkey.io/commands/zremrangebyrank/ +func (client *baseClient) ZRemRangeByRank(key string, start int64, stop int64) (int64, error) { + result, err := client.executeCommand(C.ZRemRangeByRank, []string{key, utils.IntToString(start), utils.IntToString(stop)}) + if err != nil { + return 0, err + } + return handleIntResponse(result) +} + +// Removes all elements in the sorted set stored at `key` with a score between `rangeQuery.Start` and `rangeQuery.End`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the sorted set. +// rangeQuery - The range query object representing the minimum and maximum bound of the score range. +// can be an implementation of [options.RangeByScore]. +// +// Return value: +// +// The number of members removed from the sorted set. +// If `key` does not exist, it is treated as an empty sorted set, and the command returns `0`. +// If `rangeQuery.Start` is greater than `rangeQuery.End`, `0` is returned. +// +// Example: +// +// zRemRangeByScoreResult, err := client.ZRemRangeByScore("key1", options.NewRangeByScoreBuilder( +// options.NewInfiniteScoreBoundary(options.NegativeInfinity), +// options.NewInfiniteScoreBoundary(options.PositiveInfinity), +// )) +// fmt.Println(zRemRangeByScoreResult) // Output: 1 +// +// [valkey.io]: https://valkey.io/commands/zremrangebyscore/ +func (client *baseClient) ZRemRangeByScore(key string, rangeQuery options.RangeByScore) (int64, error) { + result, err := client.executeCommand(C.ZRemRangeByScore, append([]string{key}, rangeQuery.ToArgsRemRange()...)) + if err != nil { + return 0, err + } + return handleIntResponse(result) +} + +// Returns the logarithmic access frequency counter of a Valkey object stored at key. +// +// Parameters: +// +// key - The key of the object to get the logarithmic access frequency counter of. +// +// Return value: +// +// If key exists, returns the logarithmic access frequency counter of the +// object stored at key as a long. Otherwise, returns `nil`. +// +// Example: +// +// result, err := client.ObjectFreq(key) +// if err != nil { +// // handle error +// } +// fmt.Println(result.Value()) // Output: 1 +// +// [valkey.io]: https://valkey.io/commands/object-freq/ +func (client *baseClient) ObjectFreq(key string) (Result[int64], error) { + result, err := client.executeCommand(C.ObjectFreq, []string{key}) + if err != nil { + return CreateNilInt64Result(), err + } + return handleIntOrNilResponse(result) +} + +// Returns the logarithmic access frequency counter of a Valkey object stored at key. +// +// Parameters: +// +// key - The key of the object to get the logarithmic access frequency counter of. +// +// Return value: +// +// If key exists, returns the idle time in seconds. Otherwise, returns `nil`. +// +// Example: +// +// result, err := client.ObjectIdleTime(key) +// if err != nil { +// // handle error +// } +// fmt.Println(result.Value()) // Output: 1 +// +// [valkey.io]: https://valkey.io/commands/object-idletime/ +func (client *baseClient) ObjectIdleTime(key string) (Result[int64], error) { + result, err := client.executeCommand(C.ObjectIdleTime, []string{key}) + if err != nil { + return CreateNilInt64Result(), err + } + return handleIntOrNilResponse(result) +} + +// Returns the reference count of the object stored at key. +// +// Parameters: +// +// key - The key of the object to get the reference count of. +// +// Return value: +// +// If key exists, returns the reference count of the object stored at key. +// Otherwise, returns `nil`. +// +// Example: +// +// result, err := client.ObjectRefCount(key) +// if err != nil { +// // handle error +// } +// fmt.Println(result.Value()) // Output: 1 +// +// [valkey.io]: https://valkey.io/commands/object-refcount/ +func (client *baseClient) ObjectRefCount(key string) (Result[int64], error) { + result, err := client.executeCommand(C.ObjectRefCount, []string{key}) + if err != nil { + return CreateNilInt64Result(), err + } + return handleIntOrNilResponse(result) +} + +func (client *baseClient) Sort(key string) ([]Result[string], error) { + result, err := client.executeCommand(C.Sort, []string{key}) + if err != nil { + return nil, err + } + return handleStringArrayResponse(result) +} + +func (client *baseClient) SortWithOptions(key string, options *options.SortOptions) ([]Result[string], error) { + optionArgs := options.ToArgs() + result, err := client.executeCommand(C.Sort, append([]string{key}, optionArgs...)) + if err != nil { + return nil, err + } + return handleStringArrayResponse(result) +} + +func (client *baseClient) SortReadOnly(key string) ([]Result[string], error) { + result, err := client.executeCommand(C.SortReadOnly, []string{key}) + if err != nil { + return nil, err + } + return handleStringArrayResponse(result) +} + +func (client *baseClient) SortReadOnlyWithOptions(key string, options *options.SortOptions) ([]Result[string], error) { + optionArgs := options.ToArgs() + result, err := client.executeCommand(C.SortReadOnly, append([]string{key}, optionArgs...)) + if err != nil { + return nil, err + } + return handleStringArrayResponse(result) +} + +func (client *baseClient) SortStore(key string, destination string) (Result[int64], error) { + result, err := client.executeCommand(C.Sort, []string{key, "STORE", destination}) + if err != nil { + return CreateNilInt64Result(), err + } + return handleIntOrNilResponse(result) +} + +func (client *baseClient) SortStoreWithOptions( + key string, + destination string, + options *options.SortOptions, +) (Result[int64], error) { + optionArgs := options.ToArgs() + result, err := client.executeCommand(C.Sort, append([]string{key, "STORE", destination}, optionArgs...)) + if err != nil { + return CreateNilInt64Result(), err + } + return handleIntOrNilResponse(result) +} diff --git a/go/api/command_options.go b/go/api/command_options.go index f77902ca6c..dcf17446bc 100644 --- a/go/api/command_options.go +++ b/go/api/command_options.go @@ -278,3 +278,80 @@ func (listDirection ListDirection) toString() (string, error) { return "", &RequestError{"Invalid list direction"} } } + +// Optional arguments to Restore(key string, ttl int64, value string, option *RestoreOptions) +// +// Note IDLETIME and FREQ modifiers cannot be set at the same time. +// +// [valkey.io]: https://valkey.io/commands/restore/ +type RestoreOptions struct { + // Subcommand string to replace existing key. + replace string + // Subcommand string to represent absolute timestamp (in milliseconds) for TTL. + absTTL string + // It represents the idletime/frequency of object. + eviction Eviction +} + +func NewRestoreOptionsBuilder() *RestoreOptions { + return &RestoreOptions{} +} + +const ( + // Subcommand string to replace existing key. + Replace_keyword = "REPLACE" + + // Subcommand string to represent absolute timestamp (in milliseconds) for TTL. + ABSTTL_keyword string = "ABSTTL" +) + +// Custom setter methods to replace existing key. +func (restoreOption *RestoreOptions) SetReplace() *RestoreOptions { + restoreOption.replace = Replace_keyword + return restoreOption +} + +// Custom setter methods to represent absolute timestamp (in milliseconds) for TTL. +func (restoreOption *RestoreOptions) SetABSTTL() *RestoreOptions { + restoreOption.absTTL = ABSTTL_keyword + return restoreOption +} + +// For eviction purpose, you may use IDLETIME or FREQ modifiers. +type Eviction struct { + // It represent IDLETIME or FREQ. + Type EvictionType + // It represents count(int) of the idletime/frequency of object. + Count int64 +} + +type EvictionType string + +const ( + // It represents the idletime of object + IDLETIME EvictionType = "IDLETIME" + // It represents the frequency of object + FREQ EvictionType = "FREQ" +) + +// Custom setter methods set the idletime/frequency of object. +func (restoreOption *RestoreOptions) SetEviction(evictionType EvictionType, count int64) *RestoreOptions { + restoreOption.eviction.Type = evictionType + restoreOption.eviction.Count = count + return restoreOption +} + +func (opts *RestoreOptions) toArgs() ([]string, error) { + args := []string{} + var err error + if opts.replace != "" { + args = append(args, string(opts.replace)) + } + if opts.absTTL != "" { + args = append(args, string(opts.absTTL)) + } + if (opts.eviction != Eviction{}) { + args = append(args, string(opts.eviction.Type), utils.IntToString(opts.eviction.Count)) + } + return args, err +} diff --git a/go/api/connection_management_commands.go b/go/api/connection_management_commands.go index 16c08f0a78..f9d4a1dada 100644 --- a/go/api/connection_management_commands.go +++ b/go/api/connection_management_commands.go @@ -32,4 +32,23 @@ type ConnectionManagementCommands interface { // // [valkey.io]: https://valkey.io/commands/ping/ PingWithMessage(message string) (string, error) + + // Echo the provided message back. + // The command will be routed a random node. + // + // Parameters: + // message - The provided message. + // + // Return value: + // The provided message + // + // For example: + // result, err := client.Echo("Hello World") + // if err != nil { + // // handle error + // } + // fmt.Println(result.Value()) // Output: Hello World + // + // [valkey.io]: https://valkey.io/commands/echo/ + Echo(message string) (Result[string], error) } diff --git a/go/api/generic_base_commands.go b/go/api/generic_base_commands.go index 53996c902b..becf0d10c3 100644 --- a/go/api/generic_base_commands.go +++ b/go/api/generic_base_commands.go @@ -2,6 +2,8 @@ package api +import "github.com/valkey-io/valkey-glide/go/glide/api/options" + // Supports commands and transactions for the "Generic Commands" group for standalone and cluster clients. // // See [valkey.io] for details. @@ -442,4 +444,265 @@ type GenericBaseCommands interface { // // [valkey.io]: https://valkey.io/commands/persist/ Persist(key string) (bool, error) + + // Create a key associated with a value that is obtained by + // deserializing the provided serialized value (obtained via [valkey.io]: Https://valkey.io/commands/dump/). + // + // Parameters: + // key - The key to create. + // ttl - The expiry time (in milliseconds). If 0, the key will persist. + // value - The serialized value to deserialize and assign to key. + // + // Return value: + // Return OK if successfully create a key with a value . + // + // Example: + // result, err := client.Restore("key",ttl, value) + // if err != nil { + // // handle error + // } + // fmt.Println(result.Value()) // Output: OK + // + // [valkey.io]: https://valkey.io/commands/restore/ + Restore(key string, ttl int64, value string) (Result[string], error) + + // Create a key associated with a value that is obtained by + // deserializing the provided serialized value (obtained via [valkey.io]: Https://valkey.io/commands/dump/). + // + // Parameters: + // key - The key to create. + // ttl - The expiry time (in milliseconds). If 0, the key will persist. + // value - The serialized value to deserialize and assign to key. + // restoreOptions - Set restore options with replace and absolute TTL modifiers, object idletime and frequency + // + // Return value: + // Return OK if successfully create a key with a value. + // + // Example: + // restoreOptions := api.NewRestoreOptionsBuilder().SetReplace().SetABSTTL().SetEviction(api.FREQ, 10) + // resultRestoreOpt, err := client.RestoreWithOptions(key, ttl, value, restoreOptions) + // if err != nil { + // // handle error + // } + // fmt.Println(result.Value()) // Output: OK + // + // [valkey.io]: https://valkey.io/commands/restore/ + RestoreWithOptions(key string, ttl int64, value string, option *RestoreOptions) (Result[string], error) + + // Returns the internal encoding for the Valkey object stored at key. + // + // Note: + // When in cluster mode, both key and newkey must map to the same hash slot. + // + // Parameters: + // The key of the object to get the internal encoding of. + // + // Return value: + // If key exists, returns the internal encoding of the object stored at + // key as a String. Otherwise, returns null. + // + // Example: + // result, err := client.ObjectEncoding("mykeyRenamenx") + // if err != nil { + // // handle error + // } + // fmt.Println(result.Value()) // Output: embstr + // + // [valkey.io]: https://valkey.io/commands/object-encoding/ + ObjectEncoding(key string) (Result[string], error) + + // Serialize the value stored at key in a Valkey-specific format and return it to the user. + // + // Parameters: + // The key to serialize. + // + // Return value: + // The serialized value of the data stored at key + // If key does not exist, null will be returned. + // + // Example: + // result, err := client.Dump([]string{"key"}) + // if err != nil { + // // handle error + // } + // fmt.Println(result.Value()) // Output: (Serialized Value) + // + // [valkey.io]: https://valkey.io/commands/dump/ + Dump(key string) (Result[string], error) + + ObjectFreq(key string) (Result[int64], error) + + ObjectIdleTime(key string) (Result[int64], error) + + ObjectRefCount(key string) (Result[int64], error) + + // Sorts the elements in the list, set, or sorted set at key and returns the result. + // The sort command can be used to sort elements based on different criteria and apply + // transformations on sorted elements. + // To store the result into a new key, see the sortStore function. + // + // Parameters: + // key - The key of the list, set, or sorted set to be sorted. + // + // Return value: + // An Array of sorted elements. + // + // Example: + // + // result, err := client.Sort("key") + // result.Value(): [{1 false} {2 false} {3 false}] + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/sort/ + Sort(key string) ([]Result[string], error) + + // Sorts the elements in the list, set, or sorted set at key and returns the result. + // The sort command can be used to sort elements based on different criteria and apply + // transformations on sorted elements. + // To store the result into a new key, see the sortStore function. + // + // Note: + // In cluster mode, if `key` map to different hash slots, the command + // will be split across these slots and executed separately for each. This means the command + // is atomic only at the slot level. If one or more slot-specific requests fail, the entire + // call will return the first encountered error, even though some requests may have succeeded + // while others did not. If this behavior impacts your application logic, consider splitting + // the request into sub-requests per slot to ensure atomicity. + // The use of SortOptions.byPattern and SortOptions.getPatterns in cluster mode is + // supported since Valkey version 8.0. + // + // Parameters: + // key - The key of the list, set, or sorted set to be sorted. + // sortOptions - The SortOptions type. + // + // Return value: + // An Array of sorted elements. + // + // Example: + // + // options := api.NewSortOptions().SetByPattern("weight_*").SetIsAlpha(false).AddGetPattern("object_*").AddGetPattern("#") + // result, err := client.Sort("key", options) + // result.Value(): [{Object_3 false} {c false} {Object_1 false} {a false} {Object_2 false} {b false}] + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/sort/ + SortWithOptions(key string, sortOptions *options.SortOptions) ([]Result[string], error) + + // Sorts the elements in the list, set, or sorted set at key and stores the result in + // destination. The sort command can be used to sort elements based on + // different criteria, apply transformations on sorted elements, and store the result in a new key. + // The sort command can be used to sort elements based on different criteria and apply + // transformations on sorted elements. + // To get the sort result without storing it into a key, see the sort or sortReadOnly function. + // + // Note: + // In cluster mode, if `key` and `destination` map to different hash slots, the command + // will be split across these slots and executed separately for each. This means the command + // is atomic only at the slot level. If one or more slot-specific requests fail, the entire + // call will return the first encountered error, even though some requests may have succeeded + // while others did not. If this behavior impacts your application logic, consider splitting + // the request into sub-requests per slot to ensure atomicity. + // + // Parameters: + // key - The key of the list, set, or sorted set to be sorted. + // destination - The key where the sorted result will be stored. + // + // Return value: + // The number of elements in the sorted key stored at destination. + // + // Example: + // + // result, err := client.SortStore("key","destkey") + // result.Value(): 1 + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/sort/ + SortStore(key string, destination string) (Result[int64], error) + + // Sorts the elements in the list, set, or sorted set at key and stores the result in + // destination. The sort command can be used to sort elements based on + // different criteria, apply transformations on sorted elements, and store the result in a new key. + // The sort command can be used to sort elements based on different criteria and apply + // transformations on sorted elements. + // To get the sort result without storing it into a key, see the sort or sortReadOnly function. + // + // Note: + // In cluster mode, if `key` and `destination` map to different hash slots, the command + // will be split across these slots and executed separately for each. This means the command + // is atomic only at the slot level. If one or more slot-specific requests fail, the entire + // call will return the first encountered error, even though some requests may have succeeded + // while others did not. If this behavior impacts your application logic, consider splitting + // the request into sub-requests per slot to ensure atomicity. + // The use of SortOptions.byPattern and SortOptions.getPatterns + // in cluster mode is supported since Valkey version 8.0. + // + // Parameters: + // key - The key of the list, set, or sorted set to be sorted. + // destination - The key where the sorted result will be stored. + // sortOptions - The SortOptions type. + // + // Return value: + // The number of elements in the sorted key stored at destination. + // + // Example: + // + // options := api.NewSortOptions().SetByPattern("weight_*").SetIsAlpha(false).AddGetPattern("object_*").AddGetPattern("#") + // result, err := client.SortStore("key","destkey",options) + // result.Value(): 1 + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/sort/ + SortStoreWithOptions(key string, destination string, sortOptions *options.SortOptions) (Result[int64], error) + + // Sorts the elements in the list, set, or sorted set at key and returns the result. + // The sortReadOnly command can be used to sort elements based on different criteria and apply + // transformations on sorted elements. + // This command is routed depending on the client's ReadFrom strategy. + // + // Parameters: + // key - The key of the list, set, or sorted set to be sorted. + // + // Return value: + // An Array of sorted elements. + // + // Example: + // + // result, err := client.SortReadOnly("key") + // result.Value(): [{1 false} {2 false} {3 false}] + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/sort/ + SortReadOnly(key string) ([]Result[string], error) + + // Sorts the elements in the list, set, or sorted set at key and returns the result. + // The sort command can be used to sort elements based on different criteria and apply + // transformations on sorted elements. + // This command is routed depending on the client's ReadFrom strategy. + // + // Note: + // In cluster mode, if `key` map to different hash slots, the command + // will be split across these slots and executed separately for each. This means the command + // is atomic only at the slot level. If one or more slot-specific requests fail, the entire + // call will return the first encountered error, even though some requests may have succeeded + // while others did not. If this behavior impacts your application logic, consider splitting + // the request into sub-requests per slot to ensure atomicity. + // The use of SortOptions.byPattern and SortOptions.getPatterns in cluster mode is + // supported since Valkey version 8.0. + // + // Parameters: + // key - The key of the list, set, or sorted set to be sorted. + // sortOptions - The SortOptions type. + // + // Return value: + // An Array of sorted elements. + // + // Example: + // + // options := api.NewSortOptions().SetByPattern("weight_*").SetIsAlpha(false).AddGetPattern("object_*").AddGetPattern("#") + // result, err := client.SortReadOnly("key", options) + // result.Value(): [{Object_3 false} {c false} {Object_1 false} {a false} {Object_2 false} {b false}] + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/sort/ + SortReadOnlyWithOptions(key string, sortOptions *options.SortOptions) ([]Result[string], error) } diff --git a/go/api/hash_commands.go b/go/api/hash_commands.go index a4a588004e..ba2f248e8f 100644 --- a/go/api/hash_commands.go +++ b/go/api/hash_commands.go @@ -285,57 +285,7 @@ type HashCommands interface { // [valkey.io]: https://valkey.io/commands/hincrbyfloat/ HIncrByFloat(key string, field string, increment float64) (float64, error) - // Iterates fields of Hash types and their associated values. This definition of HSCAN command does not include the - // optional arguments of the command. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the hash. - // cursor - The cursor that points to the next iteration of results. A value of "0" indicates the start of the search. - // - // Return value: - // An array of the cursor and the subset of the hash held by `key`. The first element is always the `cursor` - // for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the subset. - // The second element is always an array of the subset of the set held in `key`. The array in the - // second element is always a flattened series of String pairs, where the key is at even indices - // and the value is at odd indices. - // - // Example: - // // Assume key contains a hash {{"a": "1"}, {"b", "2"}} - // resCursor, resCollection, err = client.HScan(key, initialCursor) - // // resCursor = {0 false} - // // resCollection = [{a false} {1 false} {b false} {2 false}] - // - // [valkey.io]: https://valkey.io/commands/hscan/ - HScan(key string, cursor string) (Result[string], []Result[string], error) + HScan(key string, cursor string) (string, []string, error) - // Iterates fields of Hash types and their associated values. This definition of HSCAN includes optional arguments of the - // command. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the hash. - // cursor - The cursor that points to the next iteration of results. A value of "0" indicates the start of the search. - // options - The [api.HashScanOptions]. - // - // Return value: - // An array of the cursor and the subset of the hash held by `key`. The first element is always the `cursor` - // for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the subset. - // The second element is always an array of the subset of the set held in `key`. The array in the - // second element is always a flattened series of String pairs, where the key is at even indices - // and the value is at odd indices. - // - // Example: - // // Assume key contains a hash {{"a": "1"}, {"b", "2"}} - // opts := options.NewHashScanOptionsBuilder().SetMatch("a") - // resCursor, resCollection, err = client.HScan(key, initialCursor, opts) - // // resCursor = {0 false} - // // resCollection = [{a false} {1 false}] - // // The resCollection only contains the hash map entry that matches with the match option provided with the command - // // input. - // - // [valkey.io]: https://valkey.io/commands/hscan/ - HScanWithOptions(key string, cursor string, options *options.HashScanOptions) (Result[string], []Result[string], error) + HScanWithOptions(key string, cursor string, options *options.HashScanOptions) (string, []string, error) } diff --git a/go/api/options/sort_options.go b/go/api/options/sort_options.go new file mode 100644 index 0000000000..6ff883295c --- /dev/null +++ b/go/api/options/sort_options.go @@ -0,0 +1,131 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +import ( + "github.com/valkey-io/valkey-glide/go/glide/utils" +) + +const ( + // LIMIT subcommand string to include in the SORT and SORT_RO commands. + LIMIT_COMMAND_STRING = "LIMIT" + // ALPHA subcommand string to include in the SORT and SORT_RO commands. + ALPHA_COMMAND_STRING = "ALPHA" + // BY subcommand string to include in the SORT and SORT_RO commands. + // Supported in cluster mode since Valkey version 8.0 and above. + BY_COMMAND_STRING = "BY" + // GET subcommand string to include in the SORT and SORT_RO commands. + GET_COMMAND_STRING = "GET" +) + +// SortLimit struct represents the range of elements to retrieve +// The LIMIT argument is commonly used to specify a subset of results from the matching elements, similar to the +// LIMIT clause in SQL (e.g., `SELECT LIMIT offset, count`). +type SortLimit struct { + Offset int64 + Count int64 +} + +// OrderBy specifies the order to sort the elements. Can be ASC (ascending) or DESC(descending). +type OrderBy string + +const ( + ASC OrderBy = "ASC" + DESC OrderBy = "DESC" +) + +// SortOptions struct combines both the base options and additional sorting options +type SortOptions struct { + SortLimit *SortLimit + OrderBy OrderBy + IsAlpha bool + ByPattern string + GetPatterns []string +} + +func NewSortOptions() *SortOptions { + return &SortOptions{ + OrderBy: ASC, // Default order is ascending + IsAlpha: false, // Default is numeric sorting + } +} + +// SortLimit Limits the range of elements +// Offset is the starting position of the range, zero based. +// Count is the maximum number of elements to include in the range. +// A negative count returns all elements from the offset. +func (opts *SortOptions) SetSortLimit(offset, count int64) *SortOptions { + opts.SortLimit = &SortLimit{Offset: offset, Count: count} + return opts +} + +// OrderBy sets the order to sort by (ASC or DESC) +func (opts *SortOptions) SetOrderBy(order OrderBy) *SortOptions { + opts.OrderBy = order + return opts +} + +// IsAlpha determines whether to sort lexicographically (true) or numerically (false) +func (opts *SortOptions) SetIsAlpha(isAlpha bool) *SortOptions { + opts.IsAlpha = isAlpha + return opts +} + +// ByPattern - a pattern to sort by external keys instead of by the elements stored at the key themselves. The +// pattern should contain an asterisk (*) as a placeholder for the element values, where the value +// from the key replaces the asterisk to create the key name. For example, if key +// contains IDs of objects, byPattern can be used to sort these IDs based on an +// attribute of the objects, like their weights or timestamps. +// Supported in cluster mode since Valkey version 8.0 and above. +func (opts *SortOptions) SetByPattern(byPattern string) *SortOptions { + opts.ByPattern = byPattern + return opts +} + +// A pattern used to retrieve external keys' values, instead of the elements at key. +// The pattern should contain an asterisk (*) as a placeholder for the element values, where the +// value from key replaces the asterisk to create the key name. This +// allows the sorted elements to be transformed based on the related keys values. For example, if +// key< contains IDs of users, getPatterns can be used to retrieve +// specific attributes of these users, such as their names or email addresses. E.g., if +// getPatterns is name_*, the command will return the values of the keys +// name_<element> for each sorted element. Multiple getPatterns +// arguments can be provided to retrieve multiple attributes. The special value # can +// be used to include the actual element from key being sorted. If not provided, only +// the sorted elements themselves are returned. +// Supported in cluster mode since Valkey version 8.0 and above. +func (opts *SortOptions) AddGetPattern(getPattern string) *SortOptions { + opts.GetPatterns = append(opts.GetPatterns, getPattern) + return opts +} + +// ToArgs creates the arguments to be used in SORT and SORT_RO commands. +func (opts *SortOptions) ToArgs() []string { + var args []string + + if opts.SortLimit != nil { + args = append( + args, + LIMIT_COMMAND_STRING, + utils.IntToString(opts.SortLimit.Offset), + utils.IntToString(opts.SortLimit.Count), + ) + } + + if opts.OrderBy != "" { + args = append(args, string(opts.OrderBy)) + } + + if opts.IsAlpha { + args = append(args, ALPHA_COMMAND_STRING) + } + + if opts.ByPattern != "" { + args = append(args, BY_COMMAND_STRING, opts.ByPattern) + } + + for _, getPattern := range opts.GetPatterns { + args = append(args, GET_COMMAND_STRING, getPattern) + } + return args +} diff --git a/go/api/options/stream_options.go b/go/api/options/stream_options.go index 2d2f2318a2..cb27269f3b 100644 --- a/go/api/options/stream_options.go +++ b/go/api/options/stream_options.go @@ -116,6 +116,20 @@ func (xTrimOptions *XTrimOptions) ToArgs() ([]string, error) { return args, nil } +// Optional arguments for `XAutoClaim` in [StreamCommands] +type XAutoClaimOptions struct { + count int64 +} + +// Option to trim the stream according to minimum ID. +func NewXAutoClaimOptionsWithCount(count int64) *XAutoClaimOptions { + return &XAutoClaimOptions{count} +} + +func (xacp *XAutoClaimOptions) ToArgs() ([]string, error) { + return []string{"COUNT", utils.IntToString(xacp.count)}, nil +} + // Optional arguments for `XRead` in [StreamCommands] type XReadOptions struct { count, block int64 @@ -149,3 +163,142 @@ func (xro *XReadOptions) ToArgs() ([]string, error) { } return args, nil } + +// Optional arguments for `XReadGroup` in [StreamCommands] +type XReadGroupOptions struct { + count, block int64 + noAck bool +} + +// Create new empty `XReadOptions` +func NewXReadGroupOptions() *XReadGroupOptions { + return &XReadGroupOptions{-1, -1, false} +} + +// The maximal number of elements requested. Equivalent to `COUNT` in the Valkey API. +func (xrgo *XReadGroupOptions) SetCount(count int64) *XReadGroupOptions { + xrgo.count = count + return xrgo +} + +// If set, the request will be blocked for the set amount of milliseconds or until the server has +// the required number of entries. A value of `0` will block indefinitely. Equivalent to `BLOCK` in the Valkey API. +func (xrgo *XReadGroupOptions) SetBlock(block int64) *XReadGroupOptions { + xrgo.block = block + return xrgo +} + +// If set, messages are not added to the Pending Entries List (PEL). This is equivalent to +// acknowledging the message when it is read. +func (xrgo *XReadGroupOptions) SetNoAck() *XReadGroupOptions { + xrgo.noAck = true + return xrgo +} + +func (xrgo *XReadGroupOptions) ToArgs() ([]string, error) { + args := []string{} + if xrgo.count >= 0 { + args = append(args, "COUNT", utils.IntToString(xrgo.count)) + } + if xrgo.block >= 0 { + args = append(args, "BLOCK", utils.IntToString(xrgo.block)) + } + if xrgo.noAck { + args = append(args, "NOACK") + } + return args, nil +} + +// Optional arguments for `XPending` in [StreamCommands] +type XPendingOptions struct { + minIdleTime int64 + start string + end string + count int64 + consumer string +} + +// Create new empty `XPendingOptions`. The `start`, `end` and `count` arguments are required. +func NewXPendingOptions(start string, end string, count int64) *XPendingOptions { + options := &XPendingOptions{} + options.start = start + options.end = end + options.count = count + return options +} + +// SetMinIdleTime sets the minimum idle time for the XPendingOptions. +// minIdleTime is the amount of time (in milliseconds) that a message must be idle to be considered. +// It returns the updated XPendingOptions. +func (xpo *XPendingOptions) SetMinIdleTime(minIdleTime int64) *XPendingOptions { + xpo.minIdleTime = minIdleTime + return xpo +} + +// SetConsumer sets the consumer for the XPendingOptions. +// consumer is the name of the consumer to filter the pending messages. +// It returns the updated XPendingOptions. +func (xpo *XPendingOptions) SetConsumer(consumer string) *XPendingOptions { + xpo.consumer = consumer + return xpo +} + +func (xpo *XPendingOptions) ToArgs() ([]string, error) { + args := []string{} + + // if minIdleTime is set, we need to add an `IDLE` argument along with the minIdleTime + if xpo.minIdleTime > 0 { + args = append(args, "IDLE") + args = append(args, utils.IntToString(xpo.minIdleTime)) + } + + args = append(args, xpo.start) + args = append(args, xpo.end) + args = append(args, utils.IntToString(xpo.count)) + + if xpo.consumer != "" { + args = append(args, xpo.consumer) + } + + return args, nil +} + +// Optional arguments for `XGroupCreate` in [StreamCommands] +type XGroupCreateOptions struct { + mkStream bool + entriesRead int64 +} + +// Create new empty `XGroupCreateOptions` +func NewXGroupCreateOptions() *XGroupCreateOptions { + return &XGroupCreateOptions{false, -1} +} + +// Once set and if the stream doesn't exist, creates a new stream with a length of `0`. +func (xgco *XGroupCreateOptions) SetMakeStream() *XGroupCreateOptions { + xgco.mkStream = true + return xgco +} + +// A value representing the number of stream entries already read by the group. +// +// Since Valkey version 7.0.0. +func (xgco *XGroupCreateOptions) SetEntriesRead(entriesRead int64) *XGroupCreateOptions { + xgco.entriesRead = entriesRead + return xgco +} + +func (xgco *XGroupCreateOptions) ToArgs() ([]string, error) { + var args []string + + // if minIdleTime is set, we need to add an `IDLE` argument along with the minIdleTime + if xgco.mkStream { + args = append(args, "MKSTREAM") + } + + if xgco.entriesRead > -1 { + args = append(args, "ENTRIESREAD", utils.IntToString(xgco.entriesRead)) + } + + return args, nil +} diff --git a/go/api/options/zrange_options.go b/go/api/options/zrange_options.go index 002dc38e24..d89e9124b8 100644 --- a/go/api/options/zrange_options.go +++ b/go/api/options/zrange_options.go @@ -14,6 +14,10 @@ type ZRangeQuery interface { ToArgs() []string } +type ZRemRangeQuery interface { + ToArgsRemRange() []string +} + // Queries a range of elements from a sorted set by theirs index. type RangeByIndex struct { start, end int64 @@ -152,6 +156,10 @@ func (rbs *RangeByScore) ToArgs() []string { return args } +func (rbs *RangeByScore) ToArgsRemRange() []string { + return []string{string(rbs.start), string(rbs.end)} +} + // Queries a range of elements from a sorted set by theirs lexicographical order. // // Parameters: @@ -186,6 +194,10 @@ func (rbl *RangeByLex) ToArgs() []string { return args } +func (rbl *RangeByLex) ToArgsRemRange() []string { + return []string{string(rbl.start), string(rbl.end)} +} + // Query for `ZRangeWithScores` in [SortedSetCommands] // - For range queries by index (rank), use `RangeByIndex`. // - For range queries by score, use `RangeByScore`. diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index eac703fd88..98ba2713d2 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -9,6 +9,7 @@ import "C" import ( "fmt" "reflect" + "strconv" "unsafe" ) @@ -472,40 +473,38 @@ func handleKeyWithMemberAndScoreResponse(response *C.struct_CommandResponse) (Re return CreateKeyWithMemberAndScoreResult(KeyWithMemberAndScore{key, member, score}), nil } -func handleScanResponse( - response *C.struct_CommandResponse, -) (Result[string], []Result[string], error) { +func handleScanResponse(response *C.struct_CommandResponse) (string, []string, error) { defer C.free_command_response(response) typeErr := checkResponseType(response, C.Array, false) if typeErr != nil { - return CreateNilStringResult(), nil, typeErr + return "", nil, typeErr } slice, err := parseArray(response) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } if arr, ok := slice.([]interface{}); ok { - resCollection, err := convertToResultStringArray(arr[1].([]interface{})) + resCollection, err := convertToStringArray(arr[1].([]interface{})) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } - return CreateStringResult(arr[0].(string)), resCollection, nil + return arr[0].(string), resCollection, nil } - return CreateNilStringResult(), nil, err + return "", nil, err } -func convertToResultStringArray(input []interface{}) ([]Result[string], error) { - result := make([]Result[string], len(input)) +func convertToStringArray(input []interface{}) ([]string, error) { + result := make([]string, len(input)) for i, v := range input { str, ok := v.(string) if !ok { return nil, fmt.Errorf("element at index %d is not a string: %v", i, v) } - result[i] = CreateStringResult(str) + result[i] = str } return result, nil } @@ -524,17 +523,25 @@ type responseConverter interface { // convert maps, T - type of the value, key is string type mapConverter[T any] struct { - next responseConverter + next responseConverter + canBeNil bool } func (node mapConverter[T]) convert(data interface{}) (interface{}, error) { + if data == nil { + if node.canBeNil { + return nil, nil + } else { + return nil, &RequestError{fmt.Sprintf("Unexpected type received: nil, expected: map[string]%v", getType[T]())} + } + } result := make(map[string]T) for key, value := range data.(map[string]interface{}) { if node.next == nil { valueT, ok := value.(T) if !ok { - return nil, &RequestError{fmt.Sprintf("Unexpected type received: %T, expected: %v", value, getType[T]())} + return nil, &RequestError{fmt.Sprintf("Unexpected type of map element: %T, expected: %v", value, getType[T]())} } result[key] = valueT } else { @@ -542,9 +549,14 @@ func (node mapConverter[T]) convert(data interface{}) (interface{}, error) { if err != nil { return nil, err } + if val == nil { + var null T + result[key] = null + continue + } valueT, ok := val.(T) if !ok { - return nil, &RequestError{fmt.Sprintf("Unexpected type received: %T, expected: %v", valueT, getType[T]())} + return nil, &RequestError{fmt.Sprintf("Unexpected type of map element: %T, expected: %v", val, getType[T]())} } result[key] = valueT } @@ -555,17 +567,27 @@ func (node mapConverter[T]) convert(data interface{}) (interface{}, error) { // convert arrays, T - type of the value type arrayConverter[T any] struct { - next responseConverter + next responseConverter + canBeNil bool } func (node arrayConverter[T]) convert(data interface{}) (interface{}, error) { + if data == nil { + if node.canBeNil { + return nil, nil + } else { + return nil, &RequestError{fmt.Sprintf("Unexpected type received: nil, expected: []%v", getType[T]())} + } + } arrData := data.([]interface{}) result := make([]T, 0, len(arrData)) for _, value := range arrData { if node.next == nil { valueT, ok := value.(T) if !ok { - return nil, &RequestError{fmt.Sprintf("Unexpected type received: %T, expected: %v", value, getType[T]())} + return nil, &RequestError{ + fmt.Sprintf("Unexpected type of array element: %T, expected: %v", value, getType[T]()), + } } result = append(result, valueT) } else { @@ -573,9 +595,14 @@ func (node arrayConverter[T]) convert(data interface{}) (interface{}, error) { if err != nil { return nil, err } + if val == nil { + var null T + result = append(result, null) + continue + } valueT, ok := val.(T) if !ok { - return nil, &RequestError{fmt.Sprintf("Unexpected type received: %T, expected: %v", valueT, getType[T]())} + return nil, &RequestError{fmt.Sprintf("Unexpected type of array element: %T, expected: %v", val, getType[T]())} } result = append(result, valueT) } @@ -586,6 +613,102 @@ func (node arrayConverter[T]) convert(data interface{}) (interface{}, error) { // TODO: convert sets +func handleXAutoClaimResponse(response *C.struct_CommandResponse) (XAutoClaimResponse, error) { + defer C.free_command_response(response) + var null XAutoClaimResponse // default response + typeErr := checkResponseType(response, C.Array, false) + if typeErr != nil { + return null, typeErr + } + slice, err := parseArray(response) + if err != nil { + return null, err + } + arr := slice.([]interface{}) + len := len(arr) + if len < 2 || len > 3 { + return null, &RequestError{fmt.Sprintf("Unexpected response array length: %d", len)} + } + converted, err := mapConverter[[][]string]{ + arrayConverter[[]string]{ + arrayConverter[string]{ + nil, + false, + }, + false, + }, + false, + }.convert(arr[1]) + if err != nil { + return null, err + } + claimedEntries, ok := converted.(map[string][][]string) + if !ok { + return null, &RequestError{fmt.Sprintf("unexpected type of second element: %T", converted)} + } + var deletedMessages []string + deletedMessages = nil + if len == 3 { + converted, err = arrayConverter[string]{ + nil, + false, + }.convert(arr[2]) + if err != nil { + return null, err + } + deletedMessages, ok = converted.([]string) + if !ok { + return null, &RequestError{fmt.Sprintf("unexpected type of third element: %T", converted)} + } + } + return XAutoClaimResponse{arr[0].(string), claimedEntries, deletedMessages}, nil +} + +func handleXAutoClaimJustIdResponse(response *C.struct_CommandResponse) (XAutoClaimJustIdResponse, error) { + defer C.free_command_response(response) + var null XAutoClaimJustIdResponse // default response + typeErr := checkResponseType(response, C.Array, false) + if typeErr != nil { + return null, typeErr + } + slice, err := parseArray(response) + if err != nil { + return null, err + } + arr := slice.([]interface{}) + len := len(arr) + if len < 2 || len > 3 { + return null, &RequestError{fmt.Sprintf("Unexpected response array length: %d", len)} + } + converted, err := arrayConverter[string]{ + nil, + false, + }.convert(arr[1]) + if err != nil { + return null, err + } + claimedEntries, ok := converted.([]string) + if !ok { + return null, &RequestError{fmt.Sprintf("unexpected type of second element: %T", converted)} + } + var deletedMessages []string + deletedMessages = nil + if len == 3 { + converted, err = arrayConverter[string]{ + nil, + false, + }.convert(arr[2]) + if err != nil { + return null, err + } + deletedMessages, ok = converted.([]string) + if !ok { + return null, &RequestError{fmt.Sprintf("unexpected type of third element: %T", converted)} + } + } + return XAutoClaimJustIdResponse{arr[0].(string), claimedEntries, deletedMessages}, nil +} + func handleXReadResponse(response *C.struct_CommandResponse) (map[string]map[string][][]string, error) { defer C.free_command_response(response) data, err := parseMap(response) @@ -599,9 +722,15 @@ func handleXReadResponse(response *C.struct_CommandResponse) (map[string]map[str converters := mapConverter[map[string][][]string]{ mapConverter[[][]string]{ arrayConverter[[]string]{ - arrayConverter[string]{}, + arrayConverter[string]{ + nil, + false, + }, + false, }, + false, }, + false, } res, err := converters.convert(data) @@ -613,3 +742,128 @@ func handleXReadResponse(response *C.struct_CommandResponse) (map[string]map[str } return nil, &RequestError{fmt.Sprintf("unexpected type received: %T", res)} } + +func handleXReadGroupResponse(response *C.struct_CommandResponse) (map[string]map[string][][]string, error) { + defer C.free_command_response(response) + data, err := parseMap(response) + if err != nil { + return nil, err + } + if data == nil { + return nil, nil + } + + converters := mapConverter[map[string][][]string]{ + mapConverter[[][]string]{ + arrayConverter[[]string]{ + arrayConverter[string]{ + nil, + false, + }, + true, + }, + false, + }, + false, + } + + res, err := converters.convert(data) + if err != nil { + return nil, err + } + if result, ok := res.(map[string]map[string][][]string); ok { + return result, nil + } + return nil, &RequestError{fmt.Sprintf("unexpected type received: %T", res)} +} + +func handleXPendingSummaryResponse(response *C.struct_CommandResponse) (XPendingSummary, error) { + defer C.free_command_response(response) + + typeErr := checkResponseType(response, C.Array, true) + if typeErr != nil { + return CreateNilXPendingSummary(), typeErr + } + + slice, err := parseArray(response) + if err != nil { + return CreateNilXPendingSummary(), err + } + + arr := slice.([]interface{}) + NumOfMessages := arr[0].(int64) + var StartId, EndId Result[string] + if arr[1] == nil { + StartId = CreateNilStringResult() + } else { + StartId = CreateStringResult(arr[1].(string)) + } + if arr[2] == nil { + EndId = CreateNilStringResult() + } else { + EndId = CreateStringResult(arr[2].(string)) + } + + if pendingMessages, ok := arr[3].([]interface{}); ok { + var ConsumerPendingMessages []ConsumerPendingMessage + for _, msg := range pendingMessages { + consumerMessage := msg.([]interface{}) + count, err := strconv.ParseInt(consumerMessage[1].(string), 10, 64) + if err == nil { + ConsumerPendingMessages = append(ConsumerPendingMessages, ConsumerPendingMessage{ + ConsumerName: consumerMessage[0].(string), + MessageCount: count, + }) + } + } + return XPendingSummary{NumOfMessages, StartId, EndId, ConsumerPendingMessages}, nil + } else { + return XPendingSummary{NumOfMessages, StartId, EndId, make([]ConsumerPendingMessage, 0)}, nil + } +} + +func handleXPendingDetailResponse(response *C.struct_CommandResponse) ([]XPendingDetail, error) { + // response should be [][]interface{} + + defer C.free_command_response(response) + + // TODO: Not sure if this is correct for a nill response + if response == nil || response.response_type == uint32(C.Null) { + return make([]XPendingDetail, 0), nil + } + + typeErr := checkResponseType(response, C.Array, true) + if typeErr != nil { + return make([]XPendingDetail, 0), typeErr + } + + // parse first level of array + slice, err := parseArray(response) + arr := slice.([]interface{}) + + if err != nil { + return make([]XPendingDetail, 0), err + } + + pendingDetails := make([]XPendingDetail, 0, len(arr)) + + for _, message := range arr { + switch detail := message.(type) { + case []interface{}: + pDetail := XPendingDetail{ + Id: detail[0].(string), + ConsumerName: detail[1].(string), + IdleTime: detail[2].(int64), + DeliveryCount: detail[3].(int64), + } + pendingDetails = append(pendingDetails, pDetail) + + case XPendingDetail: + pendingDetails = append(pendingDetails, detail) + default: + fmt.Printf("handleXPendingDetailResponse - unhandled type: %s\n", reflect.TypeOf(detail)) + } + } + + return pendingDetails, nil +} diff --git a/go/api/response_types.go b/go/api/response_types.go index 2c7f3244b8..84de6aed7f 100644 --- a/go/api/response_types.go +++ b/go/api/response_types.go @@ -23,6 +23,20 @@ type KeyWithMemberAndScore struct { Score float64 } +// Response type of [XAutoClaim] command. +type XAutoClaimResponse struct { + NextEntry string + ClaimedEntries map[string][][]string + DeletedMessages []string +} + +// Response type of [XAutoClaimJustId] command. +type XAutoClaimJustIdResponse struct { + NextEntry string + ClaimedEntries []string + DeletedMessages []string +} + func (result Result[T]) IsNil() bool { return result.isNil } @@ -146,3 +160,50 @@ func CreateEmptyClusterValue() ClusterValue[interface{}] { value: Result[interface{}]{val: empty, isNil: true}, } } + +// XPendingSummary represents a summary of pending messages in a stream group. +// It includes the total number of pending messages, the ID of the first and last pending messages, +// and a list of consumer pending messages. +type XPendingSummary struct { + // NumOfMessages is the total number of pending messages in the stream group. + NumOfMessages int64 + + // StartId is the ID of the first pending message in the stream group. + StartId Result[string] + + // EndId is the ID of the last pending message in the stream group. + EndId Result[string] + + // ConsumerMessages is a list of pending messages for each consumer in the stream group. + ConsumerMessages []ConsumerPendingMessage +} + +// ConsumerPendingMessage represents a pending message for a consumer in a Redis stream group. +// It includes the consumer's name and the count of pending messages for that consumer. +type ConsumerPendingMessage struct { + // ConsumerName is the name of the consumer. + ConsumerName string + + // MessageCount is the number of pending messages for the consumer. + MessageCount int64 +} + +// XPendingDetail represents the details of a pending message in a stream group. +// It includes the message ID, the consumer's name, the idle time, and the delivery count. +type XPendingDetail struct { + // Id is the ID of the pending message. + Id string + + // ConsumerName is the name of the consumer who has the pending message. + ConsumerName string + + // IdleTime is the amount of time (in milliseconds) that the message has been idle. + IdleTime int64 + + // DeliveryCount is the number of times the message has been delivered. + DeliveryCount int64 +} + +func CreateNilXPendingSummary() XPendingSummary { + return XPendingSummary{0, CreateNilStringResult(), CreateNilStringResult(), make([]ConsumerPendingMessage, 0)} +} diff --git a/go/api/set_commands.go b/go/api/set_commands.go index 7e045d96e8..5d2315ae74 100644 --- a/go/api/set_commands.go +++ b/go/api/set_commands.go @@ -377,79 +377,9 @@ type SetCommands interface { // [valkey.io]: https://valkey.io/commands/sunion/ SUnion(keys []string) (map[Result[string]]struct{}, error) - // Iterates incrementally over a set. - // - // Note: When in cluster mode, all keys must map to the same hash slot. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the set. - // cursor - The cursor that points to the next iteration of results. - // A value of `"0"` indicates the start of the search. - // For Valkey 8.0 and above, negative cursors are treated like the initial cursor("0"). - // - // Return value: - // An array of the cursor and the subset of the set held by `key`. The first element is always the `cursor` and - // for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the set. - // The second element is always an array of the subset of the set held in `key`. - // - // Example: - // // assume "key" contains a set - // resCursor, resCol, err := client.sscan("key", "0") - // for resCursor != "0" { - // resCursor, resCol, err = client.sscan("key", "0") - // fmt.Println("Cursor: ", resCursor.Value()) - // fmt.Println("Members: ", resCol.Value()) - // } - // // Output: - // // Cursor: 48 - // // Members: ['3', '118', '120', '86', '76', '13', '61', '111', '55', '45'] - // // Cursor: 24 - // // Members: ['38', '109', '11', '119', '34', '24', '40', '57', '20', '17'] - // // Cursor: 0 - // // Members: ['47', '122', '1', '53', '10', '14', '80'] - // - // [valkey.io]: https://valkey.io/commands/sscan/ - SScan(key string, cursor string) (Result[string], []Result[string], error) + SScan(key string, cursor string) (string, []string, error) - // Iterates incrementally over a set. - // - // Note: When in cluster mode, all keys must map to the same hash slot. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the set. - // cursor - The cursor that points to the next iteration of results. - // A value of `"0"` indicates the start of the search. - // For Valkey 8.0 and above, negative cursors are treated like the initial cursor("0"). - // options - [options.BaseScanOptions] - // - // Return value: - // An array of the cursor and the subset of the set held by `key`. The first element is always the `cursor` and - // for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the set. - // The second element is always an array of the subset of the set held in `key`. - // - // Example: - // // assume "key" contains a set - // resCursor resCol, err := client.sscan("key", "0", opts) - // for resCursor != "0" { - // opts := options.NewBaseScanOptionsBuilder().SetMatch("*") - // resCursor, resCol, err = client.sscan("key", "0", opts) - // fmt.Println("Cursor: ", resCursor.Value()) - // fmt.Println("Members: ", resCol.Value()) - // } - // // Output: - // // Cursor: 48 - // // Members: ['3', '118', '120', '86', '76', '13', '61', '111', '55', '45'] - // // Cursor: 24 - // // Members: ['38', '109', '11', '119', '34', '24', '40', '57', '20', '17'] - // // Cursor: 0 - // // Members: ['47', '122', '1', '53', '10', '14', '80'] - // - // [valkey.io]: https://valkey.io/commands/sscan/ - SScanWithOptions(key string, cursor string, options *options.BaseScanOptions) (Result[string], []Result[string], error) + SScanWithOptions(key string, cursor string, options *options.BaseScanOptions) (string, []string, error) // Moves `member` from the set at `source` to the set at `destination`, removing it from the source set. // Creates a new destination set if needed. The operation is atomic. diff --git a/go/api/sorted_set_commands.go b/go/api/sorted_set_commands.go index 1abe85b314..4010d62d05 100644 --- a/go/api/sorted_set_commands.go +++ b/go/api/sorted_set_commands.go @@ -382,7 +382,13 @@ type SortedSetCommands interface { ZCount(key string, rangeOptions *options.ZCountRange) (int64, error) - ZScan(key string, cursor string) (Result[string], []Result[string], error) + ZScan(key string, cursor string) (string, []string, error) - ZScanWithOptions(key string, cursor string, options *options.ZScanOptions) (Result[string], []Result[string], error) + ZScanWithOptions(key string, cursor string, options *options.ZScanOptions) (string, []string, error) + + ZRemRangeByLex(key string, rangeQuery options.RangeByLex) (int64, error) + + ZRemRangeByRank(key string, start int64, stop int64) (int64, error) + + ZRemRangeByScore(key string, rangeQuery options.RangeByScore) (int64, error) } diff --git a/go/api/stream_commands.go b/go/api/stream_commands.go index e5fd216848..c212879d54 100644 --- a/go/api/stream_commands.go +++ b/go/api/stream_commands.go @@ -102,9 +102,54 @@ type StreamCommands interface { // [valkey.io]: https://valkey.io/commands/xlen/ XLen(key string) (int64, error) + XAutoClaim(key string, group string, consumer string, minIdleTime int64, start string) (XAutoClaimResponse, error) + + XAutoClaimWithOptions( + key string, + group string, + consumer string, + minIdleTime int64, + start string, + options *options.XAutoClaimOptions, + ) (XAutoClaimResponse, error) + + XAutoClaimJustId( + key string, + group string, + consumer string, + minIdleTime int64, + start string, + ) (XAutoClaimJustIdResponse, error) + + XAutoClaimJustIdWithOptions( + key string, + group string, + consumer string, + minIdleTime int64, + start string, + options *options.XAutoClaimOptions, + ) (XAutoClaimJustIdResponse, error) + + XReadGroup(group string, consumer string, keysAndIds map[string]string) (map[string]map[string][][]string, error) + + XReadGroupWithOptions( + group string, + consumer string, + keysAndIds map[string]string, + options *options.XReadGroupOptions, + ) (map[string]map[string][][]string, error) + XRead(keysAndIds map[string]string) (map[string]map[string][][]string, error) XReadWithOptions(keysAndIds map[string]string, options *options.XReadOptions) (map[string]map[string][][]string, error) XDel(key string, ids []string) (int64, error) + + XPending(key string, group string) (XPendingSummary, error) + + XPendingWithOptions(key string, group string, options *options.XPendingOptions) ([]XPendingDetail, error) + + XGroupCreate(key string, group string, id string) (string, error) + + XGroupCreateWithOptions(key string, group string, id string, opts *options.XGroupCreateOptions) (string, error) } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 905f55521c..93ba2df16d 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -1133,7 +1133,7 @@ func (suite *GlideTestSuite) TestHScan() { // Check for empty set. resCursor, resCollection, err := client.HScan(key1, initialCursor) assert.NoError(t, err) - assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, initialCursor, resCursor) assert.Empty(t, resCollection) // Negative cursor check. @@ -1142,7 +1142,7 @@ func (suite *GlideTestSuite) TestHScan() { assert.NotEmpty(t, err) } else { resCursor, resCollection, _ = client.HScan(key1, "-1") - assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, initialCursor, resCursor) assert.Empty(t, resCollection) } @@ -1151,27 +1151,27 @@ func (suite *GlideTestSuite) TestHScan() { assert.Equal(t, int64(len(charMembers)), hsetResult) resCursor, resCollection, _ = client.HScan(key1, initialCursor) - assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, initialCursor, resCursor) // Length includes the score which is twice the map size assert.Equal(t, len(charMap)*2, len(resCollection)) - resultKeys := make([]api.Result[string], 0) - resultValues := make([]api.Result[string], 0) + resultKeys := make([]string, 0) + resultValues := make([]string, 0) for i := 0; i < len(resCollection); i += 2 { resultKeys = append(resultKeys, resCollection[i]) resultValues = append(resultValues, resCollection[i+1]) } - keysList, valuesList := convertMapKeysAndValuesToResultList(charMap) + keysList, valuesList := convertMapKeysAndValuesToLists(charMap) assert.True(t, isSubset(resultKeys, keysList) && isSubset(keysList, resultKeys)) assert.True(t, isSubset(resultValues, valuesList) && isSubset(valuesList, resultValues)) opts := options.NewHashScanOptionsBuilder().SetMatch("a") resCursor, resCollection, _ = client.HScanWithOptions(key1, initialCursor, opts) - assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, initialCursor, resCursor) assert.Equal(t, len(resCollection), 2) - assert.Equal(t, resCollection[0].Value(), "a") - assert.Equal(t, resCollection[1].Value(), "0") + assert.Equal(t, resCollection[0], "a") + assert.Equal(t, resCollection[1], "0") // Result contains a subset of the key combinedMap := make(map[string]string) @@ -1185,12 +1185,12 @@ func (suite *GlideTestSuite) TestHScan() { hsetResult, _ = client.HSet(key1, combinedMap) assert.Equal(t, int64(len(numberMap)), hsetResult) resultCursor := "0" - secondResultAllKeys := make([]api.Result[string], 0) - secondResultAllValues := make([]api.Result[string], 0) + secondResultAllKeys := make([]string, 0) + secondResultAllValues := make([]string, 0) isFirstLoop := true for { resCursor, resCollection, _ = client.HScan(key1, resultCursor) - resultCursor = resCursor.Value() + resultCursor = resCursor for i := 0; i < len(resCollection); i += 2 { secondResultAllKeys = append(secondResultAllKeys, resCollection[i]) secondResultAllValues = append(secondResultAllValues, resCollection[i+1]) @@ -1205,7 +1205,7 @@ func (suite *GlideTestSuite) TestHScan() { // Scan with result cursor to get the next set of data. newResultCursor, secondResult, _ := client.HScan(key1, resultCursor) assert.NotEqual(t, resultCursor, newResultCursor) - resultCursor = newResultCursor.Value() + resultCursor = newResultCursor assert.False(t, reflect.DeepEqual(secondResult, resCollection)) for i := 0; i < len(secondResult); i += 2 { secondResultAllKeys = append(secondResultAllKeys, secondResult[i]) @@ -1217,41 +1217,41 @@ func (suite *GlideTestSuite) TestHScan() { break } } - numberKeysList, numberValuesList := convertMapKeysAndValuesToResultList(numberMap) + numberKeysList, numberValuesList := convertMapKeysAndValuesToLists(numberMap) assert.True(t, isSubset(numberKeysList, secondResultAllKeys)) assert.True(t, isSubset(numberValuesList, secondResultAllValues)) // Test match pattern opts = options.NewHashScanOptionsBuilder().SetMatch("*") resCursor, resCollection, _ = client.HScanWithOptions(key1, initialCursor, opts) - resCursorInt, _ := strconv.Atoi(resCursor.Value()) + resCursorInt, _ := strconv.Atoi(resCursor) assert.True(t, resCursorInt >= 0) assert.True(t, int(len(resCollection)) >= defaultCount) // Test count opts = options.NewHashScanOptionsBuilder().SetCount(int64(20)) resCursor, resCollection, _ = client.HScanWithOptions(key1, initialCursor, opts) - resCursorInt, _ = strconv.Atoi(resCursor.Value()) + resCursorInt, _ = strconv.Atoi(resCursor) assert.True(t, resCursorInt >= 0) assert.True(t, len(resCollection) >= 20) // Test count with match returns a non-empty list opts = options.NewHashScanOptionsBuilder().SetMatch("1*").SetCount(int64(20)) resCursor, resCollection, _ = client.HScanWithOptions(key1, initialCursor, opts) - resCursorInt, _ = strconv.Atoi(resCursor.Value()) + resCursorInt, _ = strconv.Atoi(resCursor) assert.True(t, resCursorInt >= 0) assert.True(t, len(resCollection) >= 0) if suite.serverVersion >= "8.0.0" { opts = options.NewHashScanOptionsBuilder().SetNoValue(true) resCursor, resCollection, _ = client.HScanWithOptions(key1, initialCursor, opts) - resCursorInt, _ = strconv.Atoi(resCursor.Value()) + resCursorInt, _ = strconv.Atoi(resCursor) assert.True(t, resCursorInt >= 0) // Check if all fields don't start with "num" containsElementsWithNumKeyword := false for i := 0; i < len(resCollection); i++ { - if strings.Contains(resCollection[i].Value(), "num") { + if strings.Contains(resCollection[i], "num") { containsElementsWithNumKeyword = true break } @@ -2298,34 +2298,27 @@ func (suite *GlideTestSuite) TestSScan() { defaultCount := 10 // use large dataset to force an iterative cursor. numMembers := make([]string, 50000) - numMembersResult := make([]api.Result[string], 50000) + numMembersResult := make([]string, 50000) charMembers := []string{"a", "b", "c", "d", "e"} - charMembersResult := []api.Result[string]{ - api.CreateStringResult("a"), - api.CreateStringResult("b"), - api.CreateStringResult("c"), - api.CreateStringResult("d"), - api.CreateStringResult("e"), - } t := suite.T() // populate the dataset slice for i := 0; i < 50000; i++ { numMembers[i] = strconv.Itoa(i) - numMembersResult[i] = api.CreateStringResult(strconv.Itoa(i)) + numMembersResult[i] = strconv.Itoa(i) } // empty set resCursor, resCollection, err := client.SScan(key1, initialCursor) assert.NoError(t, err) - assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, initialCursor, resCursor) assert.Empty(t, resCollection) // negative cursor if suite.serverVersion < "8.0.0" { resCursor, resCollection, err = client.SScan(key1, "-1") assert.NoError(t, err) - assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, initialCursor, resCursor) assert.Empty(t, resCollection) } else { _, _, err = client.SScan(key1, "-1") @@ -2339,15 +2332,15 @@ func (suite *GlideTestSuite) TestSScan() { assert.Equal(t, int64(len(charMembers)), res) resCursor, resCollection, err = client.SScan(key1, initialCursor) assert.NoError(t, err) - assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, initialCursor, resCursor) assert.Equal(t, len(charMembers), len(resCollection)) - assert.True(t, isSubset(resCollection, charMembersResult)) + assert.True(t, isSubset(resCollection, charMembers)) opts := options.NewBaseScanOptionsBuilder().SetMatch("a") resCursor, resCollection, err = client.SScanWithOptions(key1, initialCursor, opts) assert.NoError(t, err) - assert.Equal(t, initialCursor, resCursor.Value()) - assert.True(t, isSubset(resCollection, []api.Result[string]{api.CreateStringResult("a")})) + assert.Equal(t, initialCursor, resCursor) + assert.True(t, isSubset(resCollection, []string{"a"})) // result contains a subset of the key res, err = client.SAdd(key1, numMembers) @@ -2358,8 +2351,8 @@ func (suite *GlideTestSuite) TestSScan() { resultCollection := resCollection // 0 is returned for the cursor of the last iteration - for resCursor.Value() != "0" { - nextCursor, nextCol, err := client.SScan(key1, resCursor.Value()) + for resCursor != "0" { + nextCursor, nextCol, err := client.SScan(key1, resCursor) assert.NoError(t, err) assert.NotEqual(t, nextCursor, resCursor) assert.False(t, isSubset(resultCollection, nextCol)) @@ -2368,27 +2361,27 @@ func (suite *GlideTestSuite) TestSScan() { } assert.NotEmpty(t, resultCollection) assert.True(t, isSubset(numMembersResult, resultCollection)) - assert.True(t, isSubset(charMembersResult, resultCollection)) + assert.True(t, isSubset(charMembers, resultCollection)) // test match pattern opts = options.NewBaseScanOptionsBuilder().SetMatch("*") resCursor, resCollection, err = client.SScanWithOptions(key1, initialCursor, opts) assert.NoError(t, err) - assert.NotEqual(t, initialCursor, resCursor.Value()) + assert.NotEqual(t, initialCursor, resCursor) assert.GreaterOrEqual(t, len(resCollection), defaultCount) // test count opts = options.NewBaseScanOptionsBuilder().SetCount(20) resCursor, resCollection, err = client.SScanWithOptions(key1, initialCursor, opts) assert.NoError(t, err) - assert.NotEqual(t, initialCursor, resCursor.Value()) + assert.NotEqual(t, initialCursor, resCursor) assert.GreaterOrEqual(t, len(resCollection), 20) // test count with match, returns a non-empty array opts = options.NewBaseScanOptionsBuilder().SetMatch("1*").SetCount(20) resCursor, resCollection, err = client.SScanWithOptions(key1, initialCursor, opts) assert.NoError(t, err) - assert.NotEqual(t, initialCursor, resCursor.Value()) + assert.NotEqual(t, initialCursor, resCursor) assert.GreaterOrEqual(t, len(resCollection), 0) // exceptions @@ -3708,6 +3701,225 @@ func (suite *GlideTestSuite) TestPfCount_NoExistingKeys() { }) } +func (suite *GlideTestSuite) TestSortWithOptions_AscendingOrder() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"b", "a", "c"}) + + options := options.NewSortOptions(). + SetOrderBy(options.ASC). + SetIsAlpha(true) + + sortResult, err := client.SortWithOptions(key, options) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("a"), + api.CreateStringResult("b"), + api.CreateStringResult("c"), + } + assert.Equal(suite.T(), resultList, sortResult) + }) +} + +func (suite *GlideTestSuite) TestSortWithOptions_DescendingOrder() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"b", "a", "c"}) + + options := options.NewSortOptions(). + SetOrderBy(options.DESC). + SetIsAlpha(true). + SetSortLimit(0, 3) + + sortResult, err := client.SortWithOptions(key, options) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("c"), + api.CreateStringResult("b"), + api.CreateStringResult("a"), + } + + assert.Equal(suite.T(), resultList, sortResult) + }) +} + +func (suite *GlideTestSuite) TestSort_SuccessfulSort() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"3", "1", "2"}) + + sortResult, err := client.Sort(key) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("1"), + api.CreateStringResult("2"), + api.CreateStringResult("3"), + } + + assert.Equal(suite.T(), resultList, sortResult) + }) +} + +func (suite *GlideTestSuite) TestSortStore_BasicSorting() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "{listKey}" + uuid.New().String() + sortedKey := "{listKey}" + uuid.New().String() + client.LPush(key, []string{"10", "2", "5", "1", "4"}) + + result, err := client.SortStore(key, sortedKey) + + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.Equal(suite.T(), int64(5), result.Value()) + + sortedValues, err := client.LRange(sortedKey, 0, -1) + resultList := []api.Result[string]{ + api.CreateStringResult("1"), + api.CreateStringResult("2"), + api.CreateStringResult("4"), + api.CreateStringResult("5"), + api.CreateStringResult("10"), + } + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), resultList, sortedValues) + }) +} + +func (suite *GlideTestSuite) TestSortStore_ErrorHandling() { + suite.runWithDefaultClients(func(client api.BaseClient) { + result, err := client.SortStore("{listKey}nonExistingKey", "{listKey}mydestinationKey") + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(0), result.Value()) + }) +} + +func (suite *GlideTestSuite) TestSortStoreWithOptions_DescendingOrder() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "{key}" + uuid.New().String() + sortedKey := "{key}" + uuid.New().String() + client.LPush(key, []string{"30", "20", "10", "40", "50"}) + + options := options.NewSortOptions().SetOrderBy(options.DESC).SetIsAlpha(false) + result, err := client.SortStoreWithOptions(key, sortedKey, options) + + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.Equal(suite.T(), int64(5), result.Value()) + + sortedValues, err := client.LRange(sortedKey, 0, -1) + resultList := []api.Result[string]{ + api.CreateStringResult("50"), + api.CreateStringResult("40"), + api.CreateStringResult("30"), + api.CreateStringResult("20"), + api.CreateStringResult("10"), + } + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), resultList, sortedValues) + }) +} + +func (suite *GlideTestSuite) TestSortStoreWithOptions_AlphaSorting() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "{listKey}" + uuid.New().String() + sortedKey := "{listKey}" + uuid.New().String() + client.LPush(key, []string{"apple", "banana", "cherry", "date", "elderberry"}) + + options := options.NewSortOptions().SetIsAlpha(true) + result, err := client.SortStoreWithOptions(key, sortedKey, options) + + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.Equal(suite.T(), int64(5), result.Value()) + + sortedValues, err := client.LRange(sortedKey, 0, -1) + resultList := []api.Result[string]{ + api.CreateStringResult("apple"), + api.CreateStringResult("banana"), + api.CreateStringResult("cherry"), + api.CreateStringResult("date"), + api.CreateStringResult("elderberry"), + } + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), resultList, sortedValues) + }) +} + +func (suite *GlideTestSuite) TestSortStoreWithOptions_Limit() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "{listKey}" + uuid.New().String() + sortedKey := "{listKey}" + uuid.New().String() + client.LPush(key, []string{"10", "20", "30", "40", "50"}) + + options := options.NewSortOptions().SetSortLimit(1, 3) + result, err := client.SortStoreWithOptions(key, sortedKey, options) + + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.Equal(suite.T(), int64(3), result.Value()) + + sortedValues, err := client.LRange(sortedKey, 0, -1) + resultList := []api.Result[string]{ + api.CreateStringResult("20"), + api.CreateStringResult("30"), + api.CreateStringResult("40"), + } + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), resultList, sortedValues) + }) +} + +func (suite *GlideTestSuite) TestSortReadOnly_SuccessfulSort() { + suite.SkipIfServerVersionLowerThanBy("7.0.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"3", "1", "2"}) + + sortResult, err := client.SortReadOnly(key) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("1"), + api.CreateStringResult("2"), + api.CreateStringResult("3"), + } + + assert.Equal(suite.T(), resultList, sortResult) + }) +} + +func (suite *GlideTestSuite) TestSortReadyOnlyWithOptions_DescendingOrder() { + suite.SkipIfServerVersionLowerThanBy("7.0.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"b", "a", "c"}) + + options := options.NewSortOptions(). + SetOrderBy(options.DESC). + SetIsAlpha(true). + SetSortLimit(0, 3) + + sortResult, err := client.SortReadOnlyWithOptions(key, options) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("c"), + api.CreateStringResult("b"), + api.CreateStringResult("a"), + } + assert.Equal(suite.T(), resultList, sortResult) + }) +} + func (suite *GlideTestSuite) TestBLMove() { if suite.serverVersion < "6.2.0" { suite.T().Skip("This feature is added in version 6.2.0") @@ -3962,6 +4174,263 @@ func (suite *GlideTestSuite) TestXAddWithOptions() { }) } +// submit args with custom command API, check that no error returned. +// returns a response or raises `errMsg` if failed to submit the command. +func sendWithCustomCommand(suite *GlideTestSuite, client api.BaseClient, args []string, errMsg string) any { + var res any + var err error + switch c := client.(type) { + case api.GlideClient: + res, err = c.CustomCommand(args) + case api.GlideClusterClient: + res, err = c.CustomCommand(args) + default: + suite.FailNow(errMsg) + } + assert.NoError(suite.T(), err) + return res +} + +func (suite *GlideTestSuite) TestXAutoClaim() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + group := uuid.NewString() + consumer := uuid.NewString() + + sendWithCustomCommand( + suite, + client, + []string{"xgroup", "create", key, group, "0", "MKSTREAM"}, + "Can't send XGROUP CREATE as a custom command", + ) + sendWithCustomCommand( + suite, + client, + []string{"xgroup", "createconsumer", key, group, consumer}, + "Can't send XGROUP CREATECONSUMER as a custom command", + ) + + xadd, err := client.XAddWithOptions( + key, + [][]string{{"entry1_field1", "entry1_value1"}, {"entry1_field2", "entry1_value2"}}, + options.NewXAddOptions().SetId("0-1"), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "0-1", xadd.Value()) + xadd, err = client.XAddWithOptions( + key, + [][]string{{"entry2_field1", "entry2_value1"}}, + options.NewXAddOptions().SetId("0-2"), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "0-2", xadd.Value()) + + xreadgroup, err := client.XReadGroup(group, consumer, map[string]string{key: ">"}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string]map[string][][]string{ + key: { + "0-1": {{"entry1_field1", "entry1_value1"}, {"entry1_field2", "entry1_value2"}}, + "0-2": {{"entry2_field1", "entry2_value1"}}, + }, + }, xreadgroup) + + opts := options.NewXAutoClaimOptionsWithCount(1) + xautoclaim, err := client.XAutoClaimWithOptions(key, group, consumer, 0, "0-0", opts) + assert.NoError(suite.T(), err) + var deletedEntries []string + if suite.serverVersion >= "7.0.0" { + deletedEntries = []string{} + } + assert.Equal( + suite.T(), + api.XAutoClaimResponse{ + NextEntry: "0-2", + ClaimedEntries: map[string][][]string{ + "0-1": {{"entry1_field1", "entry1_value1"}, {"entry1_field2", "entry1_value2"}}, + }, + DeletedMessages: deletedEntries, + }, + xautoclaim, + ) + + justId, err := client.XAutoClaimJustId(key, group, consumer, 0, "0-0") + assert.NoError(suite.T(), err) + assert.Equal( + suite.T(), + api.XAutoClaimJustIdResponse{ + NextEntry: "0-0", + ClaimedEntries: []string{"0-1", "0-2"}, + DeletedMessages: deletedEntries, + }, + justId, + ) + + // add one more entry + xadd, err = client.XAddWithOptions( + key, + [][]string{{"entry3_field1", "entry3_value1"}}, + options.NewXAddOptions().SetId("0-3"), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "0-3", xadd.Value()) + + // incorrect IDs - response is empty + xautoclaim, err = client.XAutoClaim(key, group, consumer, 0, "5-0") + assert.NoError(suite.T(), err) + assert.Equal( + suite.T(), + api.XAutoClaimResponse{ + NextEntry: "0-0", + ClaimedEntries: map[string][][]string{}, + DeletedMessages: deletedEntries, + }, + xautoclaim, + ) + + justId, err = client.XAutoClaimJustId(key, group, consumer, 0, "5-0") + assert.NoError(suite.T(), err) + assert.Equal( + suite.T(), + api.XAutoClaimJustIdResponse{ + NextEntry: "0-0", + ClaimedEntries: []string{}, + DeletedMessages: deletedEntries, + }, + justId, + ) + + // key exists, but it is not a stream + key2 := uuid.New().String() + suite.verifyOK(client.Set(key2, key2)) + _, err = client.XAutoClaim(key2, "_", "_", 0, "_") + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestXReadGroup() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := "{xreadgroup}-1-" + uuid.NewString() + key2 := "{xreadgroup}-2-" + uuid.NewString() + key3 := "{xreadgroup}-3-" + uuid.NewString() + group := uuid.NewString() + consumer := uuid.NewString() + + sendWithCustomCommand( + suite, + client, + []string{"xgroup", "create", key1, group, "0", "MKSTREAM"}, + "Can't send XGROUP CREATE as a custom command", + ) + sendWithCustomCommand( + suite, + client, + []string{"xgroup", "createconsumer", key1, group, consumer}, + "Can't send XGROUP CREATECONSUMER as a custom command", + ) + + entry1, err := client.XAdd(key1, [][]string{{"a", "b"}}) + assert.NoError(suite.T(), err) + assert.False(suite.T(), entry1.IsNil()) + entry2, err := client.XAdd(key1, [][]string{{"c", "d"}}) + assert.NoError(suite.T(), err) + assert.False(suite.T(), entry2.IsNil()) + + // read the entire stream for the consumer and mark messages as pending + res, err := client.XReadGroup(group, consumer, map[string]string{key1: ">"}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string]map[string][][]string{ + key1: { + entry1.Value(): {{"a", "b"}}, + entry2.Value(): {{"c", "d"}}, + }, + }, res) + + // delete one of the entries + sendWithCustomCommand(suite, client, []string{"xdel", key1, entry1.Value()}, "Can't send XDEL as a custom command") + + // now xreadgroup returns one empty entry and one non-empty entry + res, err = client.XReadGroup(group, consumer, map[string]string{key1: "0"}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string]map[string][][]string{ + key1: { + entry1.Value(): nil, + entry2.Value(): {{"c", "d"}}, + }, + }, res) + + // try to read new messages only + res, err = client.XReadGroup(group, consumer, map[string]string{key1: ">"}) + assert.NoError(suite.T(), err) + assert.Nil(suite.T(), res) + + // add a message and read it with ">" + entry3, err := client.XAdd(key1, [][]string{{"e", "f"}}) + assert.NoError(suite.T(), err) + assert.False(suite.T(), entry3.IsNil()) + res, err = client.XReadGroup(group, consumer, map[string]string{key1: ">"}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string]map[string][][]string{ + key1: { + entry3.Value(): {{"e", "f"}}, + }, + }, res) + + // add second key with a group and a consumer, but no messages + sendWithCustomCommand( + suite, + client, + []string{"xgroup", "create", key2, group, "0", "MKSTREAM"}, + "Can't send XGROUP CREATE as a custom command", + ) + sendWithCustomCommand( + suite, + client, + []string{"xgroup", "createconsumer", key2, group, consumer}, + "Can't send XGROUP CREATECONSUMER as a custom command", + ) + + // read both keys + res, err = client.XReadGroup(group, consumer, map[string]string{key1: "0", key2: "0"}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string]map[string][][]string{ + key1: { + entry1.Value(): nil, + entry2.Value(): {{"c", "d"}}, + entry3.Value(): {{"e", "f"}}, + }, + key2: {}, + }, res) + + // error cases: + // key does not exist + _, err = client.XReadGroup("_", "_", map[string]string{key3: "0"}) + assert.IsType(suite.T(), &api.RequestError{}, err) + // key is not a stream + suite.verifyOK(client.Set(key3, uuid.New().String())) + _, err = client.XReadGroup("_", "_", map[string]string{key3: "0"}) + assert.IsType(suite.T(), &api.RequestError{}, err) + del, err := client.Del([]string{key3}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(1), del) + // group and consumer don't exist + xadd, err := client.XAdd(key3, [][]string{{"a", "b"}}) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), xadd) + _, err = client.XReadGroup("_", "_", map[string]string{key3: "0"}) + assert.IsType(suite.T(), &api.RequestError{}, err) + // consumer don't exist + sendWithCustomCommand( + suite, + client, + []string{"xgroup", "create", key3, group, "0-0"}, + "Can't send XGROUP CREATE as a custom command", + ) + res, err = client.XReadGroup(group, "_", map[string]string{key3: "0"}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string]map[string][][]string{key3: {}}, res) + }) +} + func (suite *GlideTestSuite) TestXRead() { suite.runWithDefaultClients(func(client api.BaseClient) { key1 := "{xread}" + uuid.NewString() @@ -4871,30 +5340,23 @@ func (suite *GlideTestSuite) TestZScan() { // Set up test data - use a large number of entries to force an iterative cursor numberMap := make(map[string]float64) - numMembersResult := make([]api.Result[string], 50000) + numMembers := make([]string, 50000) charMembers := []string{"a", "b", "c", "d", "e"} - charMembersResult := []api.Result[string]{ - api.CreateStringResult("a"), - api.CreateStringResult("b"), - api.CreateStringResult("c"), - api.CreateStringResult("d"), - api.CreateStringResult("e"), - } for i := 0; i < 50000; i++ { numberMap["member"+strconv.Itoa(i)] = float64(i) - numMembersResult[i] = api.CreateStringResult("member" + strconv.Itoa(i)) + numMembers[i] = "member" + strconv.Itoa(i) } charMap := make(map[string]float64) - charMapValues := []api.Result[string]{} + charMapValues := []string{} for i, val := range charMembers { charMap[val] = float64(i) - charMapValues = append(charMapValues, api.CreateStringResult(strconv.Itoa(i))) + charMapValues = append(charMapValues, strconv.Itoa(i)) } // Empty set resCursor, resCollection, err := client.ZScan(key1, initialCursor) assert.NoError(suite.T(), err) - assert.Equal(suite.T(), initialCursor, resCursor.Value()) + assert.Equal(suite.T(), initialCursor, resCursor) assert.Empty(suite.T(), resCollection) // Negative cursor @@ -4905,7 +5367,7 @@ func (suite *GlideTestSuite) TestZScan() { } else { resCursor, resCollection, err = client.ZScan(key1, "-1") assert.NoError(suite.T(), err) - assert.Equal(suite.T(), initialCursor, resCursor.Value()) + assert.Equal(suite.T(), initialCursor, resCursor) assert.Empty(suite.T(), resCollection) } @@ -4916,11 +5378,11 @@ func (suite *GlideTestSuite) TestZScan() { resCursor, resCollection, err = client.ZScan(key1, initialCursor) assert.NoError(suite.T(), err) - assert.Equal(suite.T(), initialCursor, resCursor.Value()) + assert.Equal(suite.T(), initialCursor, resCursor) assert.Equal(suite.T(), len(charMap)*2, len(resCollection)) - resultKeySet := make([]api.Result[string], 0, len(charMap)) - resultValueSet := make([]api.Result[string], 0, len(charMap)) + resultKeySet := make([]string, 0, len(charMap)) + resultValueSet := make([]string, 0, len(charMap)) // Iterate through array taking pairs of items for i := 0; i < len(resCollection); i += 2 { @@ -4929,7 +5391,7 @@ func (suite *GlideTestSuite) TestZScan() { } // Verify all expected keys exist in result - assert.True(suite.T(), isSubset(charMembersResult, resultKeySet)) + assert.True(suite.T(), isSubset(charMembers, resultKeySet)) // Scores come back as integers converted to a string when the fraction is zero. assert.True(suite.T(), isSubset(charMapValues, resultValueSet)) @@ -4937,8 +5399,8 @@ func (suite *GlideTestSuite) TestZScan() { opts := options.NewZScanOptionsBuilder().SetMatch("a") resCursor, resCollection, err = client.ZScanWithOptions(key1, initialCursor, opts) assert.NoError(suite.T(), err) - assert.Equal(suite.T(), initialCursor, resCursor.Value()) - assert.Equal(suite.T(), resCollection, []api.Result[string]{api.CreateStringResult("a"), api.CreateStringResult("0")}) + assert.Equal(suite.T(), initialCursor, resCursor) + assert.Equal(suite.T(), resCollection, []string{"a", "0"}) // Result contains a subset of the key res, err = client.ZAdd(key1, numberMap) @@ -4948,11 +5410,11 @@ func (suite *GlideTestSuite) TestZScan() { resCursor, resCollection, err = client.ZScan(key1, "0") assert.NoError(suite.T(), err) resultCollection := resCollection - resKeys := []api.Result[string]{} + resKeys := []string{} // 0 is returned for the cursor of the last iteration - for resCursor.Value() != "0" { - nextCursor, nextCol, err := client.ZScan(key1, resCursor.Value()) + for resCursor != "0" { + nextCursor, nextCol, err := client.ZScan(key1, resCursor) assert.NoError(suite.T(), err) assert.NotEqual(suite.T(), nextCursor, resCursor) assert.False(suite.T(), isSubset(resultCollection, nextCol)) @@ -4966,27 +5428,27 @@ func (suite *GlideTestSuite) TestZScan() { assert.NotEmpty(suite.T(), resultCollection) // Verify we got all keys and values - assert.True(suite.T(), isSubset(numMembersResult, resKeys)) + assert.True(suite.T(), isSubset(numMembers, resKeys)) // Test match pattern opts = options.NewZScanOptionsBuilder().SetMatch("*") resCursor, resCollection, err = client.ZScanWithOptions(key1, initialCursor, opts) assert.NoError(suite.T(), err) - assert.NotEqual(suite.T(), initialCursor, resCursor.Value()) + assert.NotEqual(suite.T(), initialCursor, resCursor) assert.GreaterOrEqual(suite.T(), len(resCollection), defaultCount) // test count opts = options.NewZScanOptionsBuilder().SetCount(20) resCursor, resCollection, err = client.ZScanWithOptions(key1, initialCursor, opts) assert.NoError(suite.T(), err) - assert.NotEqual(suite.T(), initialCursor, resCursor.Value()) + assert.NotEqual(suite.T(), initialCursor, resCursor) assert.GreaterOrEqual(suite.T(), len(resCollection), 20) // test count with match, returns a non-empty array opts = options.NewZScanOptionsBuilder().SetMatch("1*").SetCount(20) resCursor, resCollection, err = client.ZScanWithOptions(key1, initialCursor, opts) assert.NoError(suite.T(), err) - assert.NotEqual(suite.T(), initialCursor, resCursor.Value()) + assert.NotEqual(suite.T(), initialCursor, resCursor) assert.GreaterOrEqual(suite.T(), len(resCollection), 0) // Test NoScores option for Redis 8.0.0+ @@ -4994,13 +5456,13 @@ func (suite *GlideTestSuite) TestZScan() { opts = options.NewZScanOptionsBuilder().SetNoScores(true) resCursor, resCollection, err = client.ZScanWithOptions(key1, initialCursor, opts) assert.NoError(suite.T(), err) - cursor, err := strconv.ParseInt(resCursor.Value(), 10, 64) + cursor, err := strconv.ParseInt(resCursor, 10, 64) assert.NoError(suite.T(), err) assert.GreaterOrEqual(suite.T(), cursor, int64(0)) // Verify all fields start with "member" for _, field := range resCollection { - assert.True(suite.T(), strings.HasPrefix(field.Value(), "member")) + assert.True(suite.T(), strings.HasPrefix(field, "member")) } } @@ -5027,3 +5489,1041 @@ func (suite *GlideTestSuite) TestZScan() { assert.IsType(suite.T(), &api.RequestError{}, err) }) } + +func (suite *GlideTestSuite) TestXPending() { + suite.runWithDefaultClients(func(client api.BaseClient) { + // TODO: Update tests when XGroupCreate, XGroupCreateConsumer, XReadGroup, XClaim, XClaimJustId and XAck are added to + // the Go client. + // + // This test splits out the cluster and standalone tests into their own functions because we are forced to use + // CustomCommands for many stream commands which are not included in the preview Go client. Using a type switch for + // each use of CustomCommand would make the tests difficult to read and maintain. These tests can be + // collapsed once the native commands are added in a subsequent release. + + execStandalone := func(client api.GlideClient) { + // 1. Arrange the data + key := uuid.New().String() + groupName := "group" + uuid.New().String() + zeroStreamId := "0" + consumer1 := "consumer-1-" + uuid.New().String() + consumer2 := "consumer-2-" + uuid.New().String() + + command := []string{"XGroup", "Create", key, groupName, zeroStreamId, "MKSTREAM"} + + resp, err := client.CustomCommand(command) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "OK", resp.(string)) + + command = []string{"XGroup", "CreateConsumer", key, groupName, consumer1} + resp, err = client.CustomCommand(command) + assert.NoError(suite.T(), err) + assert.True(suite.T(), resp.(bool)) + + command = []string{"XGroup", "CreateConsumer", key, groupName, consumer2} + resp, err = client.CustomCommand(command) + assert.NoError(suite.T(), err) + assert.True(suite.T(), resp.(bool)) + + streamid_1, err := client.XAdd(key, [][]string{{"field1", "value1"}}) + assert.NoError(suite.T(), err) + streamid_2, err := client.XAdd(key, [][]string{{"field2", "value2"}}) + assert.NoError(suite.T(), err) + + _, err = client.XReadGroup(groupName, consumer1, map[string]string{key: ">"}) + assert.NoError(suite.T(), err) + + _, err = client.XAdd(key, [][]string{{"field3", "value3"}}) + assert.NoError(suite.T(), err) + _, err = client.XAdd(key, [][]string{{"field4", "value4"}}) + assert.NoError(suite.T(), err) + streamid_5, err := client.XAdd(key, [][]string{{"field5", "value5"}}) + assert.NoError(suite.T(), err) + + _, err = client.XReadGroup(groupName, consumer2, map[string]string{key: ">"}) + assert.NoError(suite.T(), err) + + expectedSummary := api.XPendingSummary{ + NumOfMessages: 5, + StartId: streamid_1, + EndId: streamid_5, + ConsumerMessages: []api.ConsumerPendingMessage{ + {ConsumerName: consumer1, MessageCount: 2}, + {ConsumerName: consumer2, MessageCount: 3}, + }, + } + + // 2. Act + summaryResult, err := client.XPending(key, groupName) + + // 3a. Assert that we get 5 messages in total, 2 for consumer1 and 3 for consumer2 + assert.NoError(suite.T(), err) + assert.True( + suite.T(), + reflect.DeepEqual(expectedSummary, summaryResult), + "Expected and actual results do not match", + ) + + // 3b. Assert that we get 2 details for consumer1 that includes + detailResult, _ := client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", 10).SetConsumer(consumer1), + ) + assert.Equal(suite.T(), len(detailResult), 2) + assert.Equal(suite.T(), streamid_1.Value(), detailResult[0].Id) + assert.Equal(suite.T(), streamid_2.Value(), detailResult[1].Id) + } + + execCluster := func(client api.GlideClusterClient) { + // 1. Arrange the data + key := uuid.New().String() + groupName := "group" + uuid.New().String() + zeroStreamId := "0" + consumer1 := "consumer-1-" + uuid.New().String() + consumer2 := "consumer-2-" + uuid.New().String() + + command := []string{"XGroup", "Create", key, groupName, zeroStreamId, "MKSTREAM"} + + resp, err := client.CustomCommand(command) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "OK", resp.Value().(string)) + + command = []string{"XGroup", "CreateConsumer", key, groupName, consumer1} + resp, err = client.CustomCommand(command) + assert.NoError(suite.T(), err) + assert.True(suite.T(), resp.Value().(bool)) + + command = []string{"XGroup", "CreateConsumer", key, groupName, consumer2} + resp, err = client.CustomCommand(command) + assert.NoError(suite.T(), err) + assert.True(suite.T(), resp.Value().(bool)) + + streamid_1, err := client.XAdd(key, [][]string{{"field1", "value1"}}) + assert.NoError(suite.T(), err) + streamid_2, err := client.XAdd(key, [][]string{{"field2", "value2"}}) + assert.NoError(suite.T(), err) + + _, err = client.XReadGroup(groupName, consumer1, map[string]string{key: ">"}) + assert.NoError(suite.T(), err) + + _, err = client.XAdd(key, [][]string{{"field3", "value3"}}) + assert.NoError(suite.T(), err) + _, err = client.XAdd(key, [][]string{{"field4", "value4"}}) + assert.NoError(suite.T(), err) + streamid_5, err := client.XAdd(key, [][]string{{"field5", "value5"}}) + assert.NoError(suite.T(), err) + + _, err = client.XReadGroup(groupName, consumer2, map[string]string{key: ">"}) + assert.NoError(suite.T(), err) + + expectedSummary := api.XPendingSummary{ + NumOfMessages: 5, + StartId: streamid_1, + EndId: streamid_5, + ConsumerMessages: []api.ConsumerPendingMessage{ + {ConsumerName: consumer1, MessageCount: 2}, + {ConsumerName: consumer2, MessageCount: 3}, + }, + } + + // 2. Act + summaryResult, err := client.XPending(key, groupName) + + // 3a. Assert that we get 5 messages in total, 2 for consumer1 and 3 for consumer2 + assert.NoError(suite.T(), err) + assert.True( + suite.T(), + reflect.DeepEqual(expectedSummary, summaryResult), + "Expected and actual results do not match", + ) + + // 3b. Assert that we get 2 details for consumer1 that includes + detailResult, _ := client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", 10).SetConsumer(consumer1), + ) + assert.Equal(suite.T(), len(detailResult), 2) + assert.Equal(suite.T(), streamid_1.Value(), detailResult[0].Id) + assert.Equal(suite.T(), streamid_2.Value(), detailResult[1].Id) + + // + } + + assert.Equal(suite.T(), "OK", "OK") + + // create group and consumer for the group + // this is only needed in order to be able to use custom commands. + // Once the native commands are added, this logic will be refactored. + switch c := client.(type) { + case api.GlideClient: + execStandalone(c) + case api.GlideClusterClient: + execCluster(c) + } + }) +} + +func (suite *GlideTestSuite) TestXPendingFailures() { + suite.runWithDefaultClients(func(client api.BaseClient) { + // TODO: Update tests when XGroupCreate, XGroupCreateConsumer, XReadGroup, XClaim, XClaimJustId and XAck are added to + // the Go client. + // + // This test splits out the cluster and standalone tests into their own functions because we are forced to use + // CustomCommands for many stream commands which are not included in the preview Go client. Using a type switch for + // each use of CustomCommand would make the tests difficult to read and maintain. These tests can be + // collapsed once the native commands are added in a subsequent release. + + execStandalone := func(client api.GlideClient) { + // 1. Arrange the data + key := uuid.New().String() + missingKey := uuid.New().String() + nonStreamKey := uuid.New().String() + groupName := "group" + uuid.New().String() + zeroStreamId := "0" + consumer1 := "consumer-1-" + uuid.New().String() + invalidConsumer := "invalid-consumer-" + uuid.New().String() + + suite.verifyOK( + client.XGroupCreateWithOptions(key, groupName, zeroStreamId, options.NewXGroupCreateOptions().SetMakeStream()), + ) + + command := []string{"XGroup", "CreateConsumer", key, groupName, consumer1} + resp, err := client.CustomCommand(command) + assert.NoError(suite.T(), err) + assert.True(suite.T(), resp.(bool)) + + _, err = client.XAdd(key, [][]string{{"field1", "value1"}}) + assert.NoError(suite.T(), err) + _, err = client.XAdd(key, [][]string{{"field2", "value2"}}) + assert.NoError(suite.T(), err) + + // no pending messages yet... + summaryResult, err := client.XPending(key, groupName) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), summaryResult.NumOfMessages) + + detailResult, err := client.XPendingWithOptions(key, groupName, options.NewXPendingOptions("-", "+", 10)) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(detailResult)) + + // read the entire stream for the consumer and mark messages as pending + _, err = client.XReadGroup(groupName, consumer1, map[string]string{key: ">"}) + assert.NoError(suite.T(), err) + + // sanity check - expect some results: + summaryResult, err = client.XPending(key, groupName) + assert.NoError(suite.T(), err) + assert.True(suite.T(), summaryResult.NumOfMessages > 0) + + detailResult, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", 1).SetConsumer(consumer1), + ) + assert.NoError(suite.T(), err) + assert.True(suite.T(), len(detailResult) > 0) + + // returns empty if + before - + detailResult, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("+", "-", 10).SetConsumer(consumer1), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(detailResult)) + + // min idletime of 100 seconds shouldn't produce any results + detailResult, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", 10).SetMinIdleTime(100000), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(detailResult)) + + // invalid consumer - no results + detailResult, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", 10).SetConsumer(invalidConsumer), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(detailResult)) + + // Return an error when range bound is not a valid ID + _, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("invalid-id", "+", 10), + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + _, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "invalid-id", 10), + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + // invalid count should return no results + detailResult, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", -1), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(detailResult)) + + // Return an error when an invalid group is provided + _, err = client.XPending( + key, + "invalid-group", + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "NOGROUP")) + + // non-existent key throws a RequestError (NOGROUP) + _, err = client.XPending( + missingKey, + groupName, + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "NOGROUP")) + + _, err = client.XPendingWithOptions( + missingKey, + groupName, + options.NewXPendingOptions("-", "+", 10), + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "NOGROUP")) + + // Key exists, but it is not a stream + _, _ = client.Set(nonStreamKey, "bar") + _, err = client.XPending( + nonStreamKey, + groupName, + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "WRONGTYPE")) + + _, err = client.XPendingWithOptions( + nonStreamKey, + groupName, + options.NewXPendingOptions("-", "+", 10), + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "WRONGTYPE")) + } + + execCluster := func(client api.GlideClusterClient) { + // 1. Arrange the data + key := uuid.New().String() + missingKey := uuid.New().String() + nonStreamKey := uuid.New().String() + groupName := "group" + uuid.New().String() + zeroStreamId := "0" + consumer1 := "consumer-1-" + uuid.New().String() + invalidConsumer := "invalid-consumer-" + uuid.New().String() + + suite.verifyOK( + client.XGroupCreateWithOptions(key, groupName, zeroStreamId, options.NewXGroupCreateOptions().SetMakeStream()), + ) + + command := []string{"XGroup", "CreateConsumer", key, groupName, consumer1} + resp, err := client.CustomCommand(command) + assert.NoError(suite.T(), err) + assert.True(suite.T(), resp.Value().(bool)) + + _, err = client.XAdd(key, [][]string{{"field1", "value1"}}) + assert.NoError(suite.T(), err) + _, err = client.XAdd(key, [][]string{{"field2", "value2"}}) + assert.NoError(suite.T(), err) + + // no pending messages yet... + summaryResult, err := client.XPending(key, groupName) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), summaryResult.NumOfMessages) + + detailResult, err := client.XPendingWithOptions(key, groupName, options.NewXPendingOptions("-", "+", 10)) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(detailResult)) + + // read the entire stream for the consumer and mark messages as pending + _, err = client.XReadGroup(groupName, consumer1, map[string]string{key: ">"}) + assert.NoError(suite.T(), err) + + // sanity check - expect some results: + summaryResult, err = client.XPending(key, groupName) + assert.NoError(suite.T(), err) + assert.True(suite.T(), summaryResult.NumOfMessages > 0) + + detailResult, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", 1).SetConsumer(consumer1), + ) + assert.NoError(suite.T(), err) + assert.True(suite.T(), len(detailResult) > 0) + + // returns empty if + before - + detailResult, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("+", "-", 10).SetConsumer(consumer1), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(detailResult)) + + // min idletime of 100 seconds shouldn't produce any results + detailResult, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", 10).SetMinIdleTime(100000), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(detailResult)) + + // invalid consumer - no results + detailResult, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", 10).SetConsumer(invalidConsumer), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(detailResult)) + + // Return an error when range bound is not a valid ID + _, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("invalid-id", "+", 10), + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + _, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "invalid-id", 10), + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + // invalid count should return no results + detailResult, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", -1), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(detailResult)) + + // Return an error when an invalid group is provided + _, err = client.XPending( + key, + "invalid-group", + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "NOGROUP")) + + // non-existent key throws a RequestError (NOGROUP) + _, err = client.XPending( + missingKey, + groupName, + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "NOGROUP")) + + _, err = client.XPendingWithOptions( + missingKey, + groupName, + options.NewXPendingOptions("-", "+", 10), + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "NOGROUP")) + + // Key exists, but it is not a stream + _, _ = client.Set(nonStreamKey, "bar") + _, err = client.XPending( + nonStreamKey, + groupName, + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "WRONGTYPE")) + + _, err = client.XPendingWithOptions( + nonStreamKey, + groupName, + options.NewXPendingOptions("-", "+", 10), + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "WRONGTYPE")) + } + + assert.Equal(suite.T(), "OK", "OK") + + // create group and consumer for the group + // this is only needed in order to be able to use custom commands. + // Once the native commands are added, this logic will be refactored. + switch c := client.(type) { + case api.GlideClient: + execStandalone(c) + case api.GlideClusterClient: + execCluster(c) + } + }) +} + +// TODO add XGroupDestroy tests there +func (suite *GlideTestSuite) TestXGroupCreate_XGroupDestroy() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + group1 := uuid.NewString() + group2 := uuid.NewString() + id := "0-1" + + // Stream not created results in error + _, err := client.XGroupCreate(key, group1, id) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + // Stream with option to create creates stream & Group + opts := options.NewXGroupCreateOptions().SetMakeStream() + suite.verifyOK(client.XGroupCreateWithOptions(key, group1, id, opts)) + + // ...and again results in BUSYGROUP error, because group names must be unique + _, err = client.XGroupCreate(key, group1, id) + assert.ErrorContains(suite.T(), err, "BUSYGROUP") + assert.IsType(suite.T(), &api.RequestError{}, err) + + // TODO add XGroupDestroy tests there + + // ENTRIESREAD option was added in valkey 7.0.0 + opts = options.NewXGroupCreateOptions().SetEntriesRead(100) + if suite.serverVersion >= "7.0.0" { + suite.verifyOK(client.XGroupCreateWithOptions(key, group2, id, opts)) + } else { + _, err = client.XGroupCreateWithOptions(key, group2, id, opts) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + } + + // key is not a stream + key = uuid.NewString() + suite.verifyOK(client.Set(key, id)) + _, err = client.XGroupCreate(key, group1, id) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestObjectEncoding() { + suite.runWithDefaultClients(func(client api.BaseClient) { + // Test 1: Check object encoding for embstr + key := "{keyName}" + uuid.NewString() + value1 := "Hello" + t := suite.T() + suite.verifyOK(client.Set(key, value1)) + resultObjectEncoding, err := client.ObjectEncoding(key) + assert.Nil(t, err) + assert.Equal(t, "embstr", resultObjectEncoding.Value(), "The result should be embstr") + + // Test 2: Check object encoding command for non existing key + key2 := "{keyName}" + uuid.NewString() + resultDumpNull, err := client.ObjectEncoding(key2) + assert.Nil(t, err) + assert.Equal(t, "", resultDumpNull.Value()) + }) +} + +func (suite *GlideTestSuite) TestDumpRestore() { + suite.runWithDefaultClients(func(client api.BaseClient) { + // Test 1: Check restore command for deleted key and check value + key := "testKey1_" + uuid.New().String() + value := "hello" + t := suite.T() + suite.verifyOK(client.Set(key, value)) + resultDump, err := client.Dump(key) + assert.Nil(t, err) + assert.NotNil(t, resultDump) + deletedCount, err := client.Del([]string{key}) + assert.Nil(t, err) + assert.Equal(t, int64(1), deletedCount) + result_test1, err := client.Restore(key, int64(0), resultDump.Value()) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "OK", result_test1.Value()) + resultGetRestoreKey, err := client.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGetRestoreKey.Value()) + + // Test 2: Check dump command for non existing key + key1 := "{keyName}" + uuid.NewString() + resultDumpNull, err := client.Dump(key1) + assert.Nil(t, err) + assert.Equal(t, "", resultDumpNull.Value()) + }) +} + +func (suite *GlideTestSuite) TestRestoreWithOptions() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "testKey1_" + uuid.New().String() + value := "hello" + t := suite.T() + suite.verifyOK(client.Set(key, value)) + + resultDump, err := client.Dump(key) + assert.Nil(t, err) + assert.NotNil(t, resultDump) + + // Test 1: Check restore command with restoreOptions REPLACE modifier + deletedCount, err := client.Del([]string{key}) + assert.Nil(t, err) + assert.Equal(t, int64(1), deletedCount) + optsReplace := api.NewRestoreOptionsBuilder().SetReplace() + result_test1, err := client.RestoreWithOptions(key, int64(0), resultDump.Value(), optsReplace) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "OK", result_test1.Value()) + resultGetRestoreKey, err := client.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGetRestoreKey.Value()) + + // Test 2: Check restore command with restoreOptions ABSTTL modifier + delete_test2, err := client.Del([]string{key}) + assert.Nil(t, err) + assert.Equal(t, int64(1), delete_test2) + opts_test2 := api.NewRestoreOptionsBuilder().SetABSTTL() + result_test2, err := client.RestoreWithOptions(key, int64(0), resultDump.Value(), opts_test2) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "OK", result_test2.Value()) + resultGet_test2, err := client.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGet_test2.Value()) + + // Test 3: Check restore command with restoreOptions FREQ modifier + delete_test3, err := client.Del([]string{key}) + assert.Nil(t, err) + assert.Equal(t, int64(1), delete_test3) + opts_test3 := api.NewRestoreOptionsBuilder().SetEviction(api.FREQ, 10) + result_test3, err := client.RestoreWithOptions(key, int64(0), resultDump.Value(), opts_test3) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "OK", result_test3.Value()) + resultGet_test3, err := client.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGet_test3.Value()) + + // Test 4: Check restore command with restoreOptions IDLETIME modifier + delete_test4, err := client.Del([]string{key}) + assert.Nil(t, err) + assert.Equal(t, int64(1), delete_test4) + opts_test4 := api.NewRestoreOptionsBuilder().SetEviction(api.IDLETIME, 10) + result_test4, err := client.RestoreWithOptions(key, int64(0), resultDump.Value(), opts_test4) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "OK", result_test4.Value()) + resultGet_test4, err := client.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGet_test4.Value()) + }) +} + +func (suite *GlideTestSuite) TestEcho() { + suite.runWithDefaultClients(func(client api.BaseClient) { + // Test 1: Check if Echo command return the message + value := "Hello world" + t := suite.T() + resultEcho, err := client.Echo(value) + assert.Nil(t, err) + assert.Equal(t, value, resultEcho.Value()) + }) +} + +func (suite *GlideTestSuite) TestZRemRangeByRank() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := uuid.New().String() + stringKey := uuid.New().String() + membersScores := map[string]float64{ + "one": 1.0, + "two": 2.0, + "three": 3.0, + } + zAddResult, err := client.ZAdd(key1, membersScores) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(3), zAddResult) + + // Incorrect range start > stop + zRemRangeByRankResult, err := client.ZRemRangeByRank(key1, 2, 1) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), zRemRangeByRankResult) + + // Remove first two members + zRemRangeByRankResult, err = client.ZRemRangeByRank(key1, 0, 1) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(2), zRemRangeByRankResult) + + // Remove all members + zRemRangeByRankResult, err = client.ZRemRangeByRank(key1, 0, 10) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(1), zRemRangeByRankResult) + + zRangeWithScoresResult, err := client.ZRangeWithScores(key1, options.NewRangeByIndexQuery(0, -1)) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(zRangeWithScoresResult)) + + // Non-existing key + zRemRangeByRankResult, err = client.ZRemRangeByRank("non_existing_key", 0, 10) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), zRemRangeByRankResult) + + // Key exists, but it is not a set + setResult, err := client.Set(stringKey, "test") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "OK", setResult) + + _, err = client.ZRemRangeByRank(stringKey, 0, 10) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestZRemRangeByLex() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := uuid.New().String() + stringKey := uuid.New().String() + + // Add members to the set + zAddResult, err := client.ZAdd(key1, map[string]float64{"a": 1.0, "b": 2.0, "c": 3.0, "d": 4.0}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(4), zAddResult) + + // min > max + zRemRangeByLexResult, err := client.ZRemRangeByLex( + key1, + *options.NewRangeByLexQuery(options.NewLexBoundary("d", false), options.NewLexBoundary("a", false)), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), zRemRangeByLexResult) + + // Remove members with lexicographical range + zRemRangeByLexResult, err = client.ZRemRangeByLex( + key1, + *options.NewRangeByLexQuery(options.NewLexBoundary("a", false), options.NewLexBoundary("c", true)), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(2), zRemRangeByLexResult) + + zRemRangeByLexResult, err = client.ZRemRangeByLex( + key1, + *options.NewRangeByLexQuery(options.NewLexBoundary("d", true), options.NewInfiniteLexBoundary(options.PositiveInfinity)), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(1), zRemRangeByLexResult) + + // Non-existing key + zRemRangeByLexResult, err = client.ZRemRangeByLex( + "non_existing_key", + *options.NewRangeByLexQuery(options.NewLexBoundary("a", false), options.NewLexBoundary("c", false)), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), zRemRangeByLexResult) + + // Key exists, but it is not a set + setResult, err := client.Set(stringKey, "test") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "OK", setResult) + + _, err = client.ZRemRangeByLex( + stringKey, + *options.NewRangeByLexQuery(options.NewLexBoundary("a", false), options.NewLexBoundary("c", false)), + ) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestZRemRangeByScore() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := uuid.New().String() + stringKey := uuid.New().String() + + // Add members to the set + zAddResult, err := client.ZAdd(key1, map[string]float64{"one": 1.0, "two": 2.0, "three": 3.0, "four": 4.0}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(4), zAddResult) + + // min > max + zRemRangeByScoreResult, err := client.ZRemRangeByScore( + key1, + *options.NewRangeByScoreQuery(options.NewScoreBoundary(2.0, false), options.NewScoreBoundary(1.0, false)), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), zRemRangeByScoreResult) + + // Remove members with score range + zRemRangeByScoreResult, err = client.ZRemRangeByScore( + key1, + *options.NewRangeByScoreQuery(options.NewScoreBoundary(1.0, false), options.NewScoreBoundary(3.0, true)), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(2), zRemRangeByScoreResult) + + // Remove all members + zRemRangeByScoreResult, err = client.ZRemRangeByScore( + key1, + *options.NewRangeByScoreQuery(options.NewScoreBoundary(1.0, false), options.NewScoreBoundary(10.0, true)), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(1), zRemRangeByScoreResult) + + // Non-existing key + zRemRangeByScoreResult, err = client.ZRemRangeByScore( + "non_existing_key", + *options.NewRangeByScoreQuery(options.NewScoreBoundary(1.0, false), options.NewScoreBoundary(10.0, true)), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), zRemRangeByScoreResult) + + // Key exists, but it is not a set + setResult, err := client.Set(stringKey, "test") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "OK", setResult) + + _, err = client.ZRemRangeByScore( + stringKey, + *options.NewRangeByScoreQuery(options.NewScoreBoundary(1.0, false), options.NewScoreBoundary(10.0, true)), + ) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestObjectIdleTime() { + suite.runWithDefaultClients(func(client api.BaseClient) { + defaultClient := suite.defaultClient() + key := "testKey1_" + uuid.New().String() + value := "hello" + sleepSec := int64(5) + t := suite.T() + suite.verifyOK(defaultClient.Set(key, value)) + keyValueMap := map[string]string{ + "maxmemory-policy": "noeviction", + } + suite.verifyOK(defaultClient.ConfigSet(keyValueMap)) + key1 := api.CreateStringResult("maxmemory-policy") + value1 := api.CreateStringResult("noeviction") + resultConfigMap := map[api.Result[string]]api.Result[string]{key1: value1} + resultConfig, err := defaultClient.ConfigGet([]string{"maxmemory-policy"}) + assert.Nil(t, err, "Failed to get configuration") + assert.Equal(t, resultConfigMap, resultConfig, "Configuration mismatch for maxmemory-policy") + resultGet, err := defaultClient.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGet.Value()) + time.Sleep(time.Duration(sleepSec) * time.Second) + resultIdleTime, err := defaultClient.ObjectIdleTime(key) + assert.Nil(t, err) + assert.GreaterOrEqual(t, resultIdleTime.Value(), sleepSec) + }) +} + +func (suite *GlideTestSuite) TestObjectRefCount() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "testKey1_" + uuid.New().String() + value := "hello" + t := suite.T() + suite.verifyOK(client.Set(key, value)) + resultGetRestoreKey, err := client.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGetRestoreKey.Value()) + resultObjectRefCount, err := client.ObjectRefCount(key) + assert.Nil(t, err) + assert.GreaterOrEqual(t, resultObjectRefCount.Value(), int64(1)) + }) +} + +func (suite *GlideTestSuite) TestObjectFreq() { + suite.runWithDefaultClients(func(client api.BaseClient) { + defaultClient := suite.defaultClient() + key := "testKey1_" + uuid.New().String() + value := "hello" + t := suite.T() + suite.verifyOK(defaultClient.Set(key, value)) + keyValueMap := map[string]string{ + "maxmemory-policy": "volatile-lfu", + } + suite.verifyOK(defaultClient.ConfigSet(keyValueMap)) + key1 := api.CreateStringResult("maxmemory-policy") + value1 := api.CreateStringResult("volatile-lfu") + resultConfigMap := map[api.Result[string]]api.Result[string]{key1: value1} + resultConfig, err := defaultClient.ConfigGet([]string{"maxmemory-policy"}) + assert.Nil(t, err, "Failed to get configuration") + assert.Equal(t, resultConfigMap, resultConfig, "Configuration mismatch for maxmemory-policy") + sleepSec := int64(5) + time.Sleep(time.Duration(sleepSec) * time.Second) + resultGet, err := defaultClient.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGet.Value()) + resultGet2, err := defaultClient.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGet2.Value()) + resultObjFreq, err := defaultClient.ObjectFreq(key) + assert.Nil(t, err) + assert.GreaterOrEqual(t, resultObjFreq.Value(), int64(2)) + }) +} + +func (suite *GlideTestSuite) TestSortWithOptions_ExternalWeights() { + suite.SkipIfServerVersionLowerThanBy("8.1.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"item1", "item2", "item3"}) + + client.Set("weight_item1", "3") + client.Set("weight_item2", "1") + client.Set("weight_item3", "2") + + options := options.NewSortOptions(). + SetByPattern("weight_*"). + SetOrderBy(options.ASC). + SetIsAlpha(false) + + sortResult, err := client.SortWithOptions(key, options) + + assert.Nil(suite.T(), err) + resultList := []api.Result[string]{ + api.CreateStringResult("item2"), + api.CreateStringResult("item3"), + api.CreateStringResult("item1"), + } + + assert.Equal(suite.T(), resultList, sortResult) + }) +} + +func (suite *GlideTestSuite) TestSortWithOptions_GetPatterns() { + suite.SkipIfServerVersionLowerThanBy("8.1.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"item1", "item2", "item3"}) + + client.Set("object_item1", "Object_1") + client.Set("object_item2", "Object_2") + client.Set("object_item3", "Object_3") + + options := options.NewSortOptions(). + SetByPattern("weight_*"). + SetOrderBy(options.ASC). + SetIsAlpha(false). + AddGetPattern("object_*") + + sortResult, err := client.SortWithOptions(key, options) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("Object_2"), + api.CreateStringResult("Object_3"), + api.CreateStringResult("Object_1"), + } + + assert.Equal(suite.T(), resultList, sortResult) + }) +} + +func (suite *GlideTestSuite) TestSortWithOptions_SuccessfulSortByWeightAndGet() { + suite.SkipIfServerVersionLowerThanBy("8.1.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"item1", "item2", "item3"}) + + client.Set("weight_item1", "10") + client.Set("weight_item2", "5") + client.Set("weight_item3", "15") + + client.Set("object_item1", "Object 1") + client.Set("object_item2", "Object 2") + client.Set("object_item3", "Object 3") + + options := options.NewSortOptions(). + SetOrderBy(options.ASC). + SetIsAlpha(false). + SetByPattern("weight_*"). + AddGetPattern("object_*"). + AddGetPattern("#") + + sortResult, err := client.SortWithOptions(key, options) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("Object 2"), + api.CreateStringResult("item2"), + api.CreateStringResult("Object 1"), + api.CreateStringResult("item1"), + api.CreateStringResult("Object 3"), + api.CreateStringResult("item3"), + } + + assert.Equal(suite.T(), resultList, sortResult) + + objectItem2, err := client.Get("object_item2") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "Object 2", objectItem2.Value()) + + objectItem1, err := client.Get("object_item1") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "Object 1", objectItem1.Value()) + + objectItem3, err := client.Get("object_item3") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "Object 3", objectItem3.Value()) + + assert.Equal(suite.T(), "item2", sortResult[1].Value()) + assert.Equal(suite.T(), "item1", sortResult[3].Value()) + assert.Equal(suite.T(), "item3", sortResult[5].Value()) + }) +} + +func (suite *GlideTestSuite) TestSortStoreWithOptions_ByPattern() { + suite.SkipIfServerVersionLowerThanBy("8.1.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "{listKey}" + uuid.New().String() + sortedKey := "{listKey}" + uuid.New().String() + client.LPush(key, []string{"a", "b", "c", "d", "e"}) + client.Set("{listKey}weight_a", "5") + client.Set("{listKey}weight_b", "2") + client.Set("{listKey}weight_c", "3") + client.Set("{listKey}weight_d", "1") + client.Set("{listKey}weight_e", "4") + + options := options.NewSortOptions().SetByPattern("{listKey}weight_*") + + result, err := client.SortStoreWithOptions(key, sortedKey, options) + + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.Equal(suite.T(), int64(5), result.Value()) + + sortedValues, err := client.LRange(sortedKey, 0, -1) + resultList := []api.Result[string]{ + api.CreateStringResult("d"), + api.CreateStringResult("b"), + api.CreateStringResult("c"), + api.CreateStringResult("e"), + api.CreateStringResult("a"), + } + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), resultList, sortedValues) + }) +} diff --git a/go/integTest/standalone_commands_test.go b/go/integTest/standalone_commands_test.go index 063e884a5d..3c298f1ee6 100644 --- a/go/integTest/standalone_commands_test.go +++ b/go/integTest/standalone_commands_test.go @@ -8,6 +8,7 @@ import ( "github.com/google/uuid" "github.com/valkey-io/valkey-glide/go/glide/api" + "github.com/valkey-io/valkey-glide/go/glide/api/options" "github.com/stretchr/testify/assert" ) @@ -273,3 +274,117 @@ func (suite *GlideTestSuite) TestSelect_SwitchBetweenDatabases() { assert.Nil(suite.T(), err) assert.Equal(suite.T(), value2, result.Value()) } + +func (suite *GlideTestSuite) TestSortReadOnlyWithOptions_ExternalWeights() { + client := suite.defaultClient() + if suite.serverVersion < "7.0.0" { + suite.T().Skip("This feature is added in version 7") + } + key := uuid.New().String() + client.LPush(key, []string{"item1", "item2", "item3"}) + + client.Set("weight_item1", "3") + client.Set("weight_item2", "1") + client.Set("weight_item3", "2") + + options := options.NewSortOptions(). + SetByPattern("weight_*"). + SetOrderBy(options.ASC). + SetIsAlpha(false) + + sortResult, err := client.SortReadOnlyWithOptions(key, options) + + assert.Nil(suite.T(), err) + resultList := []api.Result[string]{ + api.CreateStringResult("item2"), + api.CreateStringResult("item3"), + api.CreateStringResult("item1"), + } + assert.Equal(suite.T(), resultList, sortResult) +} + +func (suite *GlideTestSuite) TestSortReadOnlyWithOptions_GetPatterns() { + client := suite.defaultClient() + if suite.serverVersion < "7.0.0" { + suite.T().Skip("This feature is added in version 7") + } + key := uuid.New().String() + client.LPush(key, []string{"item1", "item2", "item3"}) + + client.Set("object_item1", "Object_1") + client.Set("object_item2", "Object_2") + client.Set("object_item3", "Object_3") + + options := options.NewSortOptions(). + SetByPattern("weight_*"). + SetOrderBy(options.ASC). + SetIsAlpha(false). + AddGetPattern("object_*") + + sortResult, err := client.SortReadOnlyWithOptions(key, options) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("Object_2"), + api.CreateStringResult("Object_3"), + api.CreateStringResult("Object_1"), + } + + assert.Equal(suite.T(), resultList, sortResult) +} + +func (suite *GlideTestSuite) TestSortReadOnlyWithOptions_SuccessfulSortByWeightAndGet() { + client := suite.defaultClient() + if suite.serverVersion < "7.0.0" { + suite.T().Skip("This feature is added in version 7") + } + key := uuid.New().String() + client.LPush(key, []string{"item1", "item2", "item3"}) + + client.Set("weight_item1", "10") + client.Set("weight_item2", "5") + client.Set("weight_item3", "15") + + client.Set("object_item1", "Object 1") + client.Set("object_item2", "Object 2") + client.Set("object_item3", "Object 3") + + options := options.NewSortOptions(). + SetOrderBy(options.ASC). + SetIsAlpha(false). + SetByPattern("weight_*"). + AddGetPattern("object_*"). + AddGetPattern("#") + + sortResult, err := client.SortReadOnlyWithOptions(key, options) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("Object 2"), + api.CreateStringResult("item2"), + api.CreateStringResult("Object 1"), + api.CreateStringResult("item1"), + api.CreateStringResult("Object 3"), + api.CreateStringResult("item3"), + } + + assert.Equal(suite.T(), resultList, sortResult) + + objectItem2, err := client.Get("object_item2") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "Object 2", objectItem2.Value()) + + objectItem1, err := client.Get("object_item1") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "Object 1", objectItem1.Value()) + + objectItem3, err := client.Get("object_item3") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "Object 3", objectItem3.Value()) + + assert.Equal(suite.T(), "item2", sortResult[1].Value()) + assert.Equal(suite.T(), "item1", sortResult[3].Value()) + assert.Equal(suite.T(), "item3", sortResult[5].Value()) +} diff --git a/go/integTest/test_utils.go b/go/integTest/test_utils.go index 144d019dfc..8e7b37bb8f 100644 --- a/go/integTest/test_utils.go +++ b/go/integTest/test_utils.go @@ -2,28 +2,26 @@ package integTest -import "github.com/valkey-io/valkey-glide/go/glide/api" - // check if sliceA is a subset of sliceB -func isSubset(sliceA []api.Result[string], sliceB []api.Result[string]) bool { - setB := make(map[string]struct{}) +func isSubset[T comparable](sliceA []T, sliceB []T) bool { + setB := make(map[T]struct{}) for _, v := range sliceB { - setB[v.Value()] = struct{}{} + setB[v] = struct{}{} } for _, v := range sliceA { - if _, found := setB[v.Value()]; !found { + if _, found := setB[v]; !found { return false } } return true } -func convertMapKeysAndValuesToResultList(m map[string]string) ([]api.Result[string], []api.Result[string]) { - keys := make([]api.Result[string], 0) - values := make([]api.Result[string], 0) +func convertMapKeysAndValuesToLists(m map[string]string) ([]string, []string) { + keys := make([]string, 0) + values := make([]string, 0) for key, value := range m { - keys = append(keys, api.CreateStringResult(key)) - values = append(values, api.CreateStringResult(value)) + keys = append(keys, key) + values = append(values, value) } return keys, values } diff --git a/java/benchmarks/build.gradle b/java/benchmarks/build.gradle index b4777ee410..f8e62ab6d7 100644 --- a/java/benchmarks/build.gradle +++ b/java/benchmarks/build.gradle @@ -7,16 +7,13 @@ plugins { repositories { // Use Maven Central for resolving dependencies. mavenCentral() + mavenLocal() } dependencies { - def releaseVersion = System.getenv("GLIDE_RELEASE_VERSION"); + version = System.getenv("GLIDE_RELEASE_VERSION") ?: project.ext.defaultReleaseVersion - if (releaseVersion) { - implementation "io.valkey:valkey-glide:" + releaseVersion + ":${osdetector.classifier}" - } else { - implementation project(':client') - } + implementation "io.valkey:valkey-glide:${version}:${osdetector.classifier}" // This dependency is used internally, and not exposed to consumers on their own compile classpath. implementation 'com.google.guava:guava:32.1.1-jre' @@ -28,7 +25,7 @@ dependencies { implementation group: 'com.google.code.gson', name: 'gson', version: '2.10.1' } -run.dependsOn ':client:buildRust' +compileJava.dependsOn ':client:publishToMavenLocal' application { // Define the main class for the application. diff --git a/java/build.gradle b/java/build.gradle index 1db21c0404..31acdb0902 100644 --- a/java/build.gradle +++ b/java/build.gradle @@ -41,7 +41,10 @@ subprojects { // finalizedBy jacocoTestReport, jacocoTestCoverageVerification } - ext.failedTests = [] + ext { + defaultReleaseVersion = "255.255.255" + failedTests = [] + } tasks.withType(Test) { afterTest { TestDescriptor descriptor, TestResult result -> diff --git a/java/client/build.gradle b/java/client/build.gradle index 7ae0d7c429..efdbd6b7f8 100644 --- a/java/client/build.gradle +++ b/java/client/build.gradle @@ -7,22 +7,27 @@ plugins { id 'io.freefair.lombok' version '8.6' id 'com.github.spotbugs' version '6.0.18' id 'com.google.osdetector' version '1.7.3' + id 'com.gradleup.shadow' version '8.3.5' } repositories { mavenCentral() } +configurations { + testImplementation { extendsFrom shadow } +} + dependencies { implementation group: 'com.google.protobuf', name: 'protobuf-java', version: '4.29.1' - implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.13.0' + shadow group: 'org.apache.commons', name: 'commons-lang3', version: '3.13.0' - implementation group: 'io.netty', name: 'netty-handler', version: '4.1.115.Final' + shadow group: 'io.netty', name: 'netty-handler', version: '4.1.115.Final' // https://github.com/netty/netty/wiki/Native-transports // At the moment, Windows is not supported - implementation group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.1.115.Final', classifier: 'linux-x86_64' - implementation group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.1.115.Final', classifier: 'linux-aarch_64' - implementation group: 'io.netty', name: 'netty-transport-native-kqueue', version: '4.1.115.Final', classifier: 'osx-aarch_64' + shadow group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.1.115.Final', classifier: 'linux-x86_64' + shadow group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.1.115.Final', classifier: 'linux-aarch_64' + shadow group: 'io.netty', name: 'netty-transport-native-kqueue', version: '4.1.115.Final', classifier: 'osx-aarch_64' // junit testImplementation group: 'org.mockito', name: 'mockito-inline', version: '3.12.4' @@ -130,8 +135,6 @@ tasks.register('copyNativeLib', Copy) { into sourceSets.main.output.resourcesDir } -def defaultReleaseVersion = "255.255.255"; - delombok.dependsOn('compileJava') jar.dependsOn('copyNativeLib') javadoc.dependsOn('copyNativeLib') @@ -155,13 +158,26 @@ sourceSets { } } +task javadocJar(type: Jar, dependsOn: javadoc) { + archiveClassifier = 'javadoc' + from javadoc.destinationDir +} + +task sourcesJar(type: Jar, dependsOn: classes) { + archiveClassifier = 'sources' + from sourceSets.main.allSource + exclude 'glide/models' // exclude protobuf files +} + publishing { publications { mavenJava(MavenPublication) { - from components.java + from components.shadow + artifact javadocJar + artifact sourcesJar groupId = 'io.valkey' artifactId = 'valkey-glide' - version = System.getenv("GLIDE_RELEASE_VERSION") ?: defaultReleaseVersion; + version = System.getenv("GLIDE_RELEASE_VERSION") ?: project.ext.defaultReleaseVersion pom { name = 'valkey-glide' description = 'General Language Independent Driver for the Enterprise (GLIDE) for Valkey' @@ -193,6 +209,10 @@ publishing { } } +tasks.withType(GenerateModuleMetadata) { + dependsOn jar, shadowJar +} + java { modularity.inferModulePath = true withSourcesJar() @@ -223,6 +243,11 @@ jar { archiveClassifier = osdetector.classifier } +shadowJar { + dependsOn('copyNativeLib') + archiveClassifier = osdetector.classifier +} + sourcesJar { // suppress following error // Entry glide/api/BaseClient.java is a duplicate but no duplicate handling strategy has been set diff --git a/java/integTest/build.gradle b/java/integTest/build.gradle index 8ebd7f272e..ebe42ce072 100644 --- a/java/integTest/build.gradle +++ b/java/integTest/build.gradle @@ -1,14 +1,16 @@ plugins { id 'java-library' + id "com.google.osdetector" version "1.7.3" } repositories { mavenCentral() + mavenLocal() } dependencies { // client - implementation project(':client') + implementation group: 'io.valkey', name: 'valkey-glide', version: project.ext.defaultReleaseVersion, classifier: osdetector.classifier implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.13.0' implementation 'com.google.code.gson:gson:2.10.1' @@ -129,7 +131,7 @@ clearDirs.finalizedBy 'startStandalone' clearDirs.finalizedBy 'startCluster' clearDirs.finalizedBy 'startClusterForAz' test.finalizedBy 'stopAllAfterTests' -test.dependsOn ':client:buildRust' +compileTestJava.dependsOn ':client:publishToMavenLocal' tasks.withType(Test) { doFirst { diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 61c2681d36..025d0218bf 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -744,7 +744,6 @@ export class BaseClient { private readonly pubsubFutures: [PromiseFunction, ErrorFunction][] = []; private pendingPushNotification: response.Response[] = []; private readonly inflightRequestsLimit: number; - private readonly clientAz: string | undefined; private config: BaseClientConfiguration | undefined; protected configurePubsub( diff --git a/python/python/glide/config.py b/python/python/glide/config.py index 94b3822ad6..9ee018d2a4 100644 --- a/python/python/glide/config.py +++ b/python/python/glide/config.py @@ -209,7 +209,7 @@ def __init__( if read_from == ReadFrom.AZ_AFFINITY and not client_az: raise ValueError( - "client_az mus t be set when read_from is set to AZ_AFFINITY" + "client_az must be set when read_from is set to AZ_AFFINITY" ) def _create_a_protobuf_conn_request(