diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c7585a2f8..2c724e6c52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ #### Changes + +* Go: Add `HScan` command ([#2917](https://github.com/valkey-io/valkey-glide/pull/2917)) * Java, Node, Python: Add transaction commands for JSON module ([#2862](https://github.com/valkey-io/valkey-glide/pull/2862)) * Go: Add HINCRBY command ([#2847](https://github.com/valkey-io/valkey-glide/pull/2847)) * Go: Add HINCRBYFLOAT command ([#2846](https://github.com/valkey-io/valkey-glide/pull/2846)) @@ -13,6 +15,7 @@ * Go: Add `ZPopMin` and `ZPopMax` ([#2850](https://github.com/valkey-io/valkey-glide/pull/2850)) * Java: Add binary version of `ZRANK WITHSCORE` ([#2896](https://github.com/valkey-io/valkey-glide/pull/2896)) * Go: Add `ZCARD` ([#2838](https://github.com/valkey-io/valkey-glide/pull/2838)) +* Go: Add `BZPopMin` ([#2849](https://github.com/valkey-io/valkey-glide/pull/2849)) #### Breaking Changes diff --git a/deny.toml b/deny.toml index 526ce9bd1e..fe489ba905 100644 --- a/deny.toml +++ b/deny.toml @@ -22,8 +22,7 @@ yanked = "deny" # A list of advisory IDs to ignore. Note that ignored advisories will still # output a note when they are encountered. ignore = [ - # Unmaintained dependency error that needs more attention due to nested dependencies - "RUSTSEC-2024-0370", + #"RUSTSEC-0000-0000", ] # Threshold for security vulnerabilities, any vulnerability with a CVSS score # lower than the range specified will be ignored. Note that ignored advisories diff --git a/go/api/base_client.go b/go/api/base_client.go index 8849212360..8212bc8c6e 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -484,6 +484,31 @@ func (client *baseClient) HIncrByFloat(key string, field string, increment float return handleDoubleResponse(result) } +func (client *baseClient) HScan(key string, cursor string) (Result[string], []Result[string], error) { + result, err := client.executeCommand(C.HScan, []string{key, cursor}) + if err != nil { + return CreateNilStringResult(), nil, err + } + return handleScanResponse(result) +} + +func (client *baseClient) HScanWithOptions( + key string, + cursor string, + options *options.HashScanOptions, +) (Result[string], []Result[string], error) { + optionArgs, err := options.ToArgs() + if err != nil { + return CreateNilStringResult(), nil, err + } + + result, err := client.executeCommand(C.HScan, append([]string{key, cursor}, optionArgs...)) + if err != nil { + return CreateNilStringResult(), nil, err + } + return handleScanResponse(result) +} + func (client *baseClient) LPush(key string, elements []string) (Result[int64], error) { result, err := client.executeCommand(C.LPush, append([]string{key}, elements...)) if err != nil { @@ -721,9 +746,9 @@ func (client *baseClient) SScan(key string, cursor string) (Result[string], []Re func (client *baseClient) SScanWithOptions( key string, cursor string, - options *BaseScanOptions, + options *options.BaseScanOptions, ) (Result[string], []Result[string], error) { - optionArgs, err := options.toArgs() + optionArgs, err := options.ToArgs() if err != nil { return CreateNilStringResult(), nil, err } @@ -1442,6 +1467,15 @@ func (client *baseClient) ZCard(key string) (Result[int64], error) { return handleLongResponse(result) } +func (client *baseClient) BZPopMin(keys []string, timeoutSecs float64) (Result[KeyWithMemberAndScore], error) { + result, err := client.executeCommand(C.BZPopMin, append(keys, utils.FloatToString(timeoutSecs))) + if err != nil { + return CreateNilKeyWithMemberAndScoreResult(), err + } + + return handleKeyWithMemberAndScoreResponse(result) +} + func (client *baseClient) Echo(message string) (Result[string], error) { result, err := client.executeCommand(C.Echo, []string{message}) if err != nil { diff --git a/go/api/command_options.go b/go/api/command_options.go index d2934b869e..f77902ca6c 100644 --- a/go/api/command_options.go +++ b/go/api/command_options.go @@ -278,46 +278,3 @@ func (listDirection ListDirection) toString() (string, error) { return "", &RequestError{"Invalid list direction"} } } - -// This base option struct represents the common set of optional arguments for the SCAN family of commands. -// Concrete implementations of this class are tied to specific SCAN commands (`SCAN`, `SSCAN`). -type BaseScanOptions struct { - match string - count int64 -} - -func NewBaseScanOptionsBuilder() *BaseScanOptions { - return &BaseScanOptions{} -} - -// The match filter is applied to the result of the command and will only include -// strings that match the pattern specified. If the sorted set is large enough for scan commands to return -// only a subset of the sorted set then there could be a case where the result is empty although there are -// items that match the pattern specified. This is due to the default `COUNT` being `10` which indicates -// that it will only fetch and match `10` items from the list. -func (scanOptions *BaseScanOptions) SetMatch(m string) *BaseScanOptions { - scanOptions.match = m - return scanOptions -} - -// `COUNT` is a just a hint for the command for how many elements to fetch from the -// sorted set. `COUNT` could be ignored until the sorted set is large enough for the `SCAN` commands to -// represent the results as compact single-allocation packed encoding. -func (scanOptions *BaseScanOptions) SetCount(c int64) *BaseScanOptions { - scanOptions.count = c - return scanOptions -} - -func (opts *BaseScanOptions) toArgs() ([]string, error) { - args := []string{} - var err error - if opts.match != "" { - args = append(args, MatchKeyword, opts.match) - } - - if opts.count != 0 { - args = append(args, CountKeyword, strconv.FormatInt(opts.count, 10)) - } - - return args, err -} diff --git a/go/api/hash_commands.go b/go/api/hash_commands.go index b1ef215339..be07c715f6 100644 --- a/go/api/hash_commands.go +++ b/go/api/hash_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 "Hash" group of commands for standalone and cluster clients. // // See [valkey.io] for details. @@ -292,4 +294,58 @@ type HashCommands interface { // // [valkey.io]: https://valkey.io/commands/hincrbyfloat/ HIncrByFloat(key string, field string, increment float64) (Result[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) + + // 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) } diff --git a/go/api/options/base_scan_options.go b/go/api/options/base_scan_options.go new file mode 100644 index 0000000000..77cf06da76 --- /dev/null +++ b/go/api/options/base_scan_options.go @@ -0,0 +1,54 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +import ( + "strconv" +) + +// This base option struct represents the common set of optional arguments for the SCAN family of commands. +// Concrete implementations of this class are tied to specific SCAN commands (`SCAN`, `SSCAN`, `HSCAN`). +type BaseScanOptions struct { + match string + count int64 +} + +func NewBaseScanOptionsBuilder() *BaseScanOptions { + return &BaseScanOptions{} +} + +/* +The match filter is applied to the result of the command and will only include +strings that match the pattern specified. If the sorted set is large enough for scan commands to return +only a subset of the sorted set then there could be a case where the result is empty although there are +items that match the pattern specified. This is due to the default `COUNT` being `10` which indicates +that it will only fetch and match `10` items from the list. +*/ +func (scanOptions *BaseScanOptions) SetMatch(m string) *BaseScanOptions { + scanOptions.match = m + return scanOptions +} + +/* +`COUNT` is a just a hint for the command for how many elements to fetch from the +sorted set. `COUNT` could be ignored until the sorted set is large enough for the `SCAN` commands to +represent the results as compact single-allocation packed encoding. +*/ +func (scanOptions *BaseScanOptions) SetCount(c int64) *BaseScanOptions { + scanOptions.count = c + return scanOptions +} + +func (opts *BaseScanOptions) ToArgs() ([]string, error) { + args := []string{} + var err error + if opts.match != "" { + args = append(args, MatchKeyword, opts.match) + } + + if opts.count != 0 { + args = append(args, CountKeyword, strconv.FormatInt(opts.count, 10)) + } + + return args, err +} diff --git a/go/api/options/constants.go b/go/api/options/constants.go new file mode 100644 index 0000000000..f38b0f4541 --- /dev/null +++ b/go/api/options/constants.go @@ -0,0 +1,9 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +const ( + CountKeyword string = "COUNT" // Valkey API keyword used to extract specific number of matching indices from a list. + MatchKeyword string = "MATCH" // Valkey API keyword used to indicate the match filter. + NoValue string = "NOVALUE" // Valkey API keyword for the no value option for hcsan command. +) diff --git a/go/api/options/hscan_options.go b/go/api/options/hscan_options.go new file mode 100644 index 0000000000..a90b2d369a --- /dev/null +++ b/go/api/options/hscan_options.go @@ -0,0 +1,43 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +// This struct represents the optional arguments for the HSCAN command. +type HashScanOptions struct { + BaseScanOptions + noValue bool +} + +func NewHashScanOptionsBuilder() *HashScanOptions { + return &HashScanOptions{} +} + +/* +If this value is set to true, the HSCAN command will be called with NOVALUES option. +In the NOVALUES option, values are not included in the response. +*/ +func (hashScanOptions *HashScanOptions) SetNoValue(noValue bool) *HashScanOptions { + hashScanOptions.noValue = noValue + return hashScanOptions +} + +func (hashScanOptions *HashScanOptions) SetMatch(match string) *HashScanOptions { + hashScanOptions.BaseScanOptions.SetMatch(match) + return hashScanOptions +} + +func (hashScanOptions *HashScanOptions) SetCount(count int64) *HashScanOptions { + hashScanOptions.BaseScanOptions.SetCount(count) + return hashScanOptions +} + +func (options *HashScanOptions) ToArgs() ([]string, error) { + args := []string{} + baseArgs, err := options.BaseScanOptions.ToArgs() + args = append(args, baseArgs...) + + if options.noValue { + args = append(args, NoValue) + } + return args, err +} diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index dd4c2d1f24..4a5056c0c6 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -397,6 +397,30 @@ func handleStringSetResponse(response *C.struct_CommandResponse) (map[Result[str return slice, nil } +func handleKeyWithMemberAndScoreResponse(response *C.struct_CommandResponse) (Result[KeyWithMemberAndScore], error) { + defer C.free_command_response(response) + + if response == nil || response.response_type == uint32(C.Null) { + return CreateNilKeyWithMemberAndScoreResult(), nil + } + + typeErr := checkResponseType(response, C.Array, true) + if typeErr != nil { + return CreateNilKeyWithMemberAndScoreResult(), typeErr + } + + slice, err := parseArray(response) + if err != nil { + return CreateNilKeyWithMemberAndScoreResult(), err + } + + arr := slice.([]interface{}) + key := arr[0].(string) + member := arr[1].(string) + score := arr[2].(float64) + return CreateKeyWithMemberAndScoreResult(KeyWithMemberAndScore{key, member, score}), nil +} + func handleScanResponse( response *C.struct_CommandResponse, ) (Result[string], []Result[string], error) { diff --git a/go/api/response_types.go b/go/api/response_types.go index 3146032b04..6172c4ff2b 100644 --- a/go/api/response_types.go +++ b/go/api/response_types.go @@ -7,6 +7,14 @@ type Result[T any] struct { isNil bool } +// KeyWithMemberAndScore is used by BZPOPMIN/BZPOPMAX, which return an object consisting of the key of the sorted set that was +// popped, the popped member, and its score. +type KeyWithMemberAndScore struct { + Key string + Member string + Score float64 +} + func (result Result[T]) IsNil() bool { return result.isNil } @@ -47,6 +55,14 @@ func CreateNilBoolResult() Result[bool] { return Result[bool]{val: false, isNil: true} } +func CreateKeyWithMemberAndScoreResult(kmsVal KeyWithMemberAndScore) Result[KeyWithMemberAndScore] { + return Result[KeyWithMemberAndScore]{val: kmsVal, isNil: false} +} + +func CreateNilKeyWithMemberAndScoreResult() Result[KeyWithMemberAndScore] { + return Result[KeyWithMemberAndScore]{val: KeyWithMemberAndScore{"", "", 0.0}, isNil: true} +} + // Enum to distinguish value types stored in `ClusterValue` type ValueType int diff --git a/go/api/set_commands.go b/go/api/set_commands.go index 14a088db6a..73ac66ecc1 100644 --- a/go/api/set_commands.go +++ b/go/api/set_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 "Set" group of commands for standalone and cluster clients. // // See [valkey.io] for details. @@ -429,7 +431,7 @@ type SetCommands interface { // 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 - [BaseScanOptions] + // 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 @@ -440,7 +442,7 @@ type SetCommands interface { // // assume "key" contains a set // resCursor resCol, err := client.sscan("key", "0", opts) // for resCursor != "0" { - // opts := api.NewBaseScanOptionsBuilder().SetMatch("*") + // opts := options.NewBaseScanOptionsBuilder().SetMatch("*") // resCursor, resCol, err = client.sscan("key", "0", opts) // fmt.Println("Cursor: ", resCursor.Value()) // fmt.Println("Members: ", resCol.Value()) @@ -454,7 +456,7 @@ type SetCommands interface { // // Members: ['47', '122', '1', '53', '10', '14', '80'] // // [valkey.io]: https://valkey.io/commands/sscan/ - SScanWithOptions(key string, cursor string, options *BaseScanOptions) (Result[string], []Result[string], error) + SScanWithOptions(key string, cursor string, options *options.BaseScanOptions) (Result[string], []Result[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 4159acabe1..4b63b70091 100644 --- a/go/api/sorted_set_commands.go +++ b/go/api/sorted_set_commands.go @@ -232,4 +232,34 @@ type SortedSetCommands interface { // // [valkey.io]: https://valkey.io/commands/zcard/ ZCard(key string) (Result[int64], error) + + // Blocks the connection until it removes and returns a member with the lowest score from the + // first non-empty sorted set, with the given `keys` being checked in the order they + // are provided. + // `BZPOPMIN` is the blocking variant of `ZPOPMIN`. + // + // Note: + // - When in cluster mode, all `keys` must map to the same hash slot. + // - `BZPOPMIN` is a client blocking command, see [Blocking Commands] for more details and best practices. + // + // See [valkey.io] for more details. + // + // Parameters: + // keys - The keys of the sorted sets. + // timeout - The number of seconds to wait for a blocking operation to complete. A value of + // `0` will block indefinitely. + // + // Return value: + // A `KeyWithMemberAndScore` struct containing the key where the member was popped out, the member + // itself, and the member score. If no member could be popped and the `timeout` expired, returns `nil`. + // + // example + // zaddResult1, err := client.ZAdd(key1, map[string]float64{"a": 1.0, "b": 1.5}) + // zaddResult2, err := client.ZAdd(key2, map[string]float64{"c": 2.0}) + // result, err := client.BZPopMin([]string{key1, key2}, float64(.5)) + // fmt.Println(res.Value()) // Output: {key: key1 member:a, score:1} + // + // [valkey.io]: https://valkey.io/commands/bzpopmin/ + // [blocking commands]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands + BZPopMin(keys []string, timeoutSecs float64) (Result[KeyWithMemberAndScore], error) } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 3254bd1871..a633004c80 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -6,6 +6,7 @@ import ( "math" "reflect" "strconv" + "strings" "time" "github.com/google/uuid" @@ -1106,6 +1107,175 @@ func (suite *GlideTestSuite) TestHIncrByFloat_WithNonExistingField() { }) } +func (suite *GlideTestSuite) TestHScan() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := "{key}-1" + uuid.NewString() + key2 := "{key}-2" + uuid.NewString() + initialCursor := "0" + defaultCount := 20 + + // Setup test data + numberMap := make(map[string]string) + // This is an unusually large dataset because the server can ignore the COUNT option if the dataset is small enough + // because it is more efficient to transfer its entire content at once. + for i := 0; i < 50000; i++ { + numberMap[strconv.Itoa(i)] = "num" + strconv.Itoa(i) + } + charMembers := []string{"a", "b", "c", "d", "e"} + charMap := make(map[string]string) + for i, val := range charMembers { + charMap[val] = strconv.Itoa(i) + } + + t := suite.T() + + // Check for empty set. + resCursor, resCollection, err := client.HScan(key1, initialCursor) + assert.NoError(t, err) + assert.Equal(t, initialCursor, resCursor.Value()) + assert.Empty(t, resCollection) + + // Negative cursor check. + if suite.serverVersion >= "8.0.0" { + _, _, err = client.HScan(key1, "-1") + assert.NotEmpty(t, err) + } else { + resCursor, resCollection, _ = client.HScan(key1, "-1") + assert.Equal(t, initialCursor, resCursor.Value()) + assert.Empty(t, resCollection) + } + + // Result contains the whole set + hsetResult, _ := client.HSet(key1, charMap) + assert.Equal(t, int64(len(charMembers)), hsetResult.Value()) + + resCursor, resCollection, _ = client.HScan(key1, initialCursor) + assert.Equal(t, initialCursor, resCursor.Value()) + // 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) + + for i := 0; i < len(resCollection); i += 2 { + resultKeys = append(resultKeys, resCollection[i]) + resultValues = append(resultValues, resCollection[i+1]) + } + keysList, valuesList := convertMapKeysAndValuesToResultList(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, len(resCollection), 2) + assert.Equal(t, resCollection[0].Value(), "a") + assert.Equal(t, resCollection[1].Value(), "0") + + // Result contains a subset of the key + combinedMap := make(map[string]string) + for key, value := range numberMap { + combinedMap[key] = value + } + for key, value := range charMap { + combinedMap[key] = value + } + + hsetResult, _ = client.HSet(key1, combinedMap) + assert.Equal(t, int64(len(numberMap)), hsetResult.Value()) + resultCursor := "0" + secondResultAllKeys := make([]api.Result[string], 0) + secondResultAllValues := make([]api.Result[string], 0) + isFirstLoop := true + for { + resCursor, resCollection, _ = client.HScan(key1, resultCursor) + resultCursor = resCursor.Value() + for i := 0; i < len(resCollection); i += 2 { + secondResultAllKeys = append(secondResultAllKeys, resCollection[i]) + secondResultAllValues = append(secondResultAllValues, resCollection[i+1]) + } + if isFirstLoop { + assert.NotEqual(t, "0", resultCursor) + isFirstLoop = false + } else if resultCursor == "0" { + break + } + + // 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() + assert.False(t, reflect.DeepEqual(secondResult, resCollection)) + for i := 0; i < len(secondResult); i += 2 { + secondResultAllKeys = append(secondResultAllKeys, secondResult[i]) + secondResultAllValues = append(secondResultAllValues, secondResult[i+1]) + } + + // 0 is returned for the cursor of the last iteration. + if resultCursor == "0" { + break + } + } + numberKeysList, numberValuesList := convertMapKeysAndValuesToResultList(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()) + 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()) + 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()) + 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()) + 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") { + containsElementsWithNumKeyword = true + break + } + } + assert.False(t, containsElementsWithNumKeyword) + } + + // Check if Non-hash key throws an error. + setResult, _ := client.Set(key2, "test") + assert.Equal(t, setResult.Value(), "OK") + _, _, err = client.HScan(key2, initialCursor) + assert.NotEmpty(t, err) + + // Check if Non-hash key throws an error when HSCAN called with options. + opts = options.NewHashScanOptionsBuilder().SetMatch("test").SetCount(int64(1)) + _, _, err = client.HScanWithOptions(key2, initialCursor, opts) + assert.NotEmpty(t, err) + + // Check if a negative cursor value throws an error. + opts = options.NewHashScanOptionsBuilder().SetCount(int64(-1)) + _, _, err = client.HScanWithOptions(key1, initialCursor, opts) + assert.NotEmpty(t, err) + }) +} + func (suite *GlideTestSuite) TestLPushLPop_WithExistingKey() { suite.runWithDefaultClients(func(client api.BaseClient) { list := []string{"value4", "value3", "value2", "value1"} @@ -2235,7 +2405,7 @@ func (suite *GlideTestSuite) TestSScan() { assert.Equal(t, len(charMembers), len(resCollection)) assert.True(t, isSubset(resCollection, charMembersResult)) - opts := api.NewBaseScanOptionsBuilder().SetMatch("a") + opts := options.NewBaseScanOptionsBuilder().SetMatch("a") resCursor, resCollection, err = client.SScanWithOptions(key1, initialCursor, opts) assert.NoError(t, err) assert.Equal(t, initialCursor, resCursor.Value()) @@ -2263,21 +2433,21 @@ func (suite *GlideTestSuite) TestSScan() { assert.True(t, isSubset(charMembersResult, resultCollection)) // test match pattern - opts = api.NewBaseScanOptionsBuilder().SetMatch("*") + opts = options.NewBaseScanOptionsBuilder().SetMatch("*") resCursor, resCollection, err = client.SScanWithOptions(key1, initialCursor, opts) assert.NoError(t, err) assert.NotEqual(t, initialCursor, resCursor.Value()) assert.GreaterOrEqual(t, len(resCollection), defaultCount) // test count - opts = api.NewBaseScanOptionsBuilder().SetCount(20) + opts = options.NewBaseScanOptionsBuilder().SetCount(20) resCursor, resCollection, err = client.SScanWithOptions(key1, initialCursor, opts) assert.NoError(t, err) assert.NotEqual(t, initialCursor, resCursor.Value()) assert.GreaterOrEqual(t, len(resCollection), 20) // test count with match, returns a non-empty array - opts = api.NewBaseScanOptionsBuilder().SetMatch("1*").SetCount(20) + 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()) @@ -4138,6 +4308,50 @@ func (suite *GlideTestSuite) TestZincrBy() { }) } +func (suite *GlideTestSuite) TestBZPopMin() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := "{zset}-1-" + uuid.NewString() + key2 := "{zset}-2-" + uuid.NewString() + key3 := "{zset}-2-" + uuid.NewString() + + // Add elements to key1 + zaddResult1, err := client.ZAdd(key1, map[string]float64{"a": 1.0, "b": 1.5}) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), zaddResult1.Value()) + + // Add elements to key2 + zaddResult2, err := client.ZAdd(key2, map[string]float64{"c": 2.0}) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(1), zaddResult2.Value()) + + // Pop minimum element from key1 and key2 + bzpopminResult1, err := client.BZPopMin([]string{key1, key2}, float64(.5)) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), api.KeyWithMemberAndScore{Key: key1, Member: "a", Score: 1.0}, bzpopminResult1.Value()) + + // Attempt to pop from non-existent key3 + bzpopminResult2, err := client.BZPopMin([]string{key3}, float64(1)) + assert.Nil(suite.T(), err) + assert.True(suite.T(), bzpopminResult2.IsNil()) + + // Pop minimum element from key2 + bzpopminResult3, err := client.BZPopMin([]string{key3, key2}, float64(.5)) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), api.KeyWithMemberAndScore{Key: key2, Member: "c", Score: 2.0}, bzpopminResult3.Value()) + + // Set key3 to a non-sorted set value + setResult, err := client.Set(key3, "value") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "OK", setResult.Value()) + + // Attempt to pop from key3 which is not a sorted set + _, err = client.BZPopMin([]string{key3}, float64(.5)) + if assert.Error(suite.T(), err) { + assert.IsType(suite.T(), &api.RequestError{}, err) + } + }) +} + func (suite *GlideTestSuite) TestZPopMin() { suite.runWithDefaultClients(func(client api.BaseClient) { key1 := uuid.New().String() diff --git a/go/integTest/test_utils.go b/go/integTest/test_utils.go index 10f2fb3be1..144d019dfc 100644 --- a/go/integTest/test_utils.go +++ b/go/integTest/test_utils.go @@ -17,3 +17,13 @@ func isSubset(sliceA []api.Result[string], sliceB []api.Result[string]) bool { } 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) + for key, value := range m { + keys = append(keys, api.CreateStringResult(key)) + values = append(values, api.CreateStringResult(value)) + } + return keys, values +}