From 3c44d5a23691c3b6d8cc6913e9b05035375f8937 Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:09:18 -0800 Subject: [PATCH 01/17] Go: Add command SSCAN and SMOVE (#2789) * Go: Add command SSCAN and SMOVE Signed-off-by: TJ Zhang --- CHANGELOG.md | 1 + go/api/base_client.go | 33 ++++ go/api/command_options.go | 48 ++++++ go/api/response_handlers.go | 38 +++++ go/api/set_commands.go | 96 +++++++++++ go/integTest/shared_commands_test.go | 238 +++++++++++++++++++++++++++ go/integTest/test_utils.go | 19 +++ 7 files changed, 473 insertions(+) create mode 100644 go/integTest/test_utils.go diff --git a/CHANGELOG.md b/CHANGELOG.md index b2c349d60f..e465f65ce9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Java: Bump protobuf (protoc) version ([#2796](https://github.com/valkey-io/valkey-glide/pull/2796), [#2800](https://github.com/valkey-io/valkey-glide/pull/2800)) * Go: Add `SInterStore` ([#2779](https://github.com/valkey-io/valkey-glide/issues/2779)) * Node: Remove native package references for MacOs x64 architecture ([#2799](https://github.com/valkey-io/valkey-glide/issues/2799)) +* Go: Add `SScan` and `SMove` ([#2789](https://github.com/valkey-io/valkey-glide/issues/2789)) #### Breaking Changes diff --git a/go/api/base_client.go b/go/api/base_client.go index 5441cbcc77..0bb250fd07 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -662,6 +662,39 @@ 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) { + result, err := client.executeCommand(C.SScan, []string{key, cursor}) + if err != nil { + return CreateNilStringResult(), nil, err + } + return handleScanResponse(result) +} + +func (client *baseClient) SScanWithOptions( + key string, + cursor string, + options *BaseScanOptions, +) (Result[string], []Result[string], error) { + optionArgs, err := options.toArgs() + if err != nil { + return CreateNilStringResult(), nil, err + } + + result, err := client.executeCommand(C.SScan, append([]string{key, cursor}, optionArgs...)) + if err != nil { + return CreateNilStringResult(), nil, err + } + return handleScanResponse(result) +} + +func (client *baseClient) SMove(source string, destination string, member string) (Result[bool], error) { + result, err := client.executeCommand(C.SMove, []string{source, destination, member}) + if err != nil { + return CreateNilBoolResult(), err + } + return handleBooleanResponse(result) +} + func (client *baseClient) LRange(key string, start int64, end int64) ([]Result[string], error) { result, err := client.executeCommand(C.LRange, []string{key, utils.IntToString(start), utils.IntToString(end)}) if err != nil { diff --git a/go/api/command_options.go b/go/api/command_options.go index c63d01a7a3..bbfaf982a0 100644 --- a/go/api/command_options.go +++ b/go/api/command_options.go @@ -230,6 +230,7 @@ const ( CountKeyword string = "COUNT" // Valkey API keyword used to extract specific number of matching indices from a list. RankKeyword string = "RANK" // Valkey API keyword use to determine the rank of the match to return. MaxLenKeyword string = "MAXLEN" // Valkey API keyword used to determine the maximum number of list items to compare. + MatchKeyword string = "MATCH" // Valkey API keyword used to indicate the match filter. ) // A InsertPosition defines where to insert new elements into a list. @@ -277,3 +278,50 @@ 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 { + /** + * 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. + */ + match string + /** + * `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. + */ + count int64 +} + +func NewBaseScanOptionsBuilder() *BaseScanOptions { + return &BaseScanOptions{} +} + +func (scanOptions *BaseScanOptions) SetMatch(m string) *BaseScanOptions { + scanOptions.match = m + return scanOptions +} + +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/response_handlers.go b/go/api/response_handlers.go index 8a0f2dd0ef..0a0e15204c 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -372,3 +372,41 @@ func handleStringSetResponse(response *C.struct_CommandResponse) (map[Result[str return slice, nil } + +func handleScanResponse( + response *C.struct_CommandResponse, +) (Result[string], []Result[string], error) { + defer C.free_command_response(response) + + typeErr := checkResponseType(response, C.Array, false) + if typeErr != nil { + return CreateNilStringResult(), nil, typeErr + } + + slice, err := parseArray(response) + if err != nil { + return CreateNilStringResult(), nil, err + } + + if arr, ok := slice.([]interface{}); ok { + resCollection, err := convertToResultStringArray(arr[1].([]interface{})) + if err != nil { + return CreateNilStringResult(), nil, err + } + return CreateStringResult(arr[0].(string)), resCollection, nil + } + + return CreateNilStringResult(), nil, err +} + +func convertToResultStringArray(input []interface{}) ([]Result[string], error) { + result := make([]Result[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) + } + return result, nil +} diff --git a/go/api/set_commands.go b/go/api/set_commands.go index e75730cf6d..a87500a8c0 100644 --- a/go/api/set_commands.go +++ b/go/api/set_commands.go @@ -381,4 +381,100 @@ 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) + + // 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 - [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 := api.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 *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. + // + // Note: When in cluster mode, `source` and `destination` must map to the same hash slot. + // + // See [valkey.io] for details. + // + // Parameters: + // source - The key of the set to remove the element from. + // destination - The key of the set to add the element to. + // member - The set element to move. + // + // Return value: + // `true` on success, or `false` if the `source` set does not exist or the element is not a member of the source set. + // + // Example: + // moved := SMove("set1", "set2", "element") + // fmt.Println(moved.Value()) // Output: true + // + // [valkey.io]: https://valkey.io/commands/smove/ + SMove(source string, destination string, member string) (Result[bool], error) } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 406e0a1ac6..63f0e39ec7 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -5,6 +5,7 @@ package integTest import ( "math" "reflect" + "strconv" "time" "github.com/google/uuid" @@ -1989,6 +1990,243 @@ func (suite *GlideTestSuite) TestSUnion() { }) } +func (suite *GlideTestSuite) TestSMove() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := "{key}-1-" + uuid.NewString() + key2 := "{key}-2-" + uuid.NewString() + key3 := "{key}-3-" + uuid.NewString() + stringKey := "{key}-4-" + uuid.NewString() + nonExistingKey := "{key}-5-" + uuid.NewString() + memberArray1 := []string{"1", "2", "3"} + memberArray2 := []string{"2", "3"} + t := suite.T() + + res1, err := client.SAdd(key1, memberArray1) + assert.NoError(t, err) + assert.Equal(t, int64(3), res1.Value()) + + res2, err := client.SAdd(key2, memberArray2) + assert.NoError(t, err) + assert.Equal(t, int64(2), res2.Value()) + + // move an element + res3, err := client.SMove(key1, key2, "1") + assert.NoError(t, err) + assert.True(t, res3.Value()) + + res4, err := client.SMembers(key1) + assert.NoError(t, err) + expectedSet := map[api.Result[string]]struct{}{ + api.CreateStringResult("2"): {}, + api.CreateStringResult("3"): {}, + } + assert.True(t, reflect.DeepEqual(expectedSet, res4)) + + res5, err := client.SMembers(key2) + assert.NoError(t, err) + expectedSet = map[api.Result[string]]struct{}{ + api.CreateStringResult("1"): {}, + api.CreateStringResult("2"): {}, + api.CreateStringResult("3"): {}, + } + assert.True(t, reflect.DeepEqual(expectedSet, res5)) + + // moved element already exists in the destination set + res6, err := client.SMove(key2, key1, "2") + assert.NoError(t, err) + assert.True(t, res6.Value()) + + res7, err := client.SMembers(key1) + assert.NoError(t, err) + expectedSet = map[api.Result[string]]struct{}{ + api.CreateStringResult("2"): {}, + api.CreateStringResult("3"): {}, + } + assert.True(t, reflect.DeepEqual(expectedSet, res7)) + + res8, err := client.SMembers(key2) + assert.NoError(t, err) + expectedSet = map[api.Result[string]]struct{}{ + api.CreateStringResult("1"): {}, + api.CreateStringResult("3"): {}, + } + assert.True(t, reflect.DeepEqual(expectedSet, res8)) + + // attempt to move from a non-existing key + res9, err := client.SMove(nonExistingKey, key1, "4") + assert.NoError(t, err) + assert.False(t, res9.Value()) + + res10, err := client.SMembers(key1) + assert.NoError(t, err) + expectedSet = map[api.Result[string]]struct{}{ + api.CreateStringResult("2"): {}, + api.CreateStringResult("3"): {}, + } + assert.True(t, reflect.DeepEqual(expectedSet, res10)) + + // move to a new set + res11, err := client.SMove(key1, key3, "2") + assert.NoError(t, err) + assert.True(t, res11.Value()) + + res12, err := client.SMembers(key1) + assert.NoError(t, err) + assert.Len(t, res12, 1) + assert.Contains(t, res12, api.CreateStringResult("3")) + + res13, err := client.SMembers(key3) + assert.NoError(t, err) + assert.Len(t, res13, 1) + assert.Contains(t, res13, api.CreateStringResult("2")) + + // attempt to move a missing element + res14, err := client.SMove(key1, key3, "42") + assert.NoError(t, err) + assert.False(t, res14.Value()) + + res12, err = client.SMembers(key1) + assert.NoError(t, err) + assert.Len(t, res12, 1) + assert.Contains(t, res12, api.CreateStringResult("3")) + + res13, err = client.SMembers(key3) + assert.NoError(t, err) + assert.Len(t, res13, 1) + assert.Contains(t, res13, api.CreateStringResult("2")) + + // moving missing element to missing key + res15, err := client.SMove(key1, nonExistingKey, "42") + assert.NoError(t, err) + assert.False(t, res15.Value()) + + res12, err = client.SMembers(key1) + assert.NoError(t, err) + assert.Len(t, res12, 1) + assert.Contains(t, res12, api.CreateStringResult("3")) + + // key exists but is not contain a set + _, err = client.Set(stringKey, "value") + assert.NoError(t, err) + + _, err = client.SMove(stringKey, key1, "_") + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestSScan() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := "{key}-1-" + uuid.NewString() + key2 := "{key}-2-" + uuid.NewString() + initialCursor := "0" + defaultCount := 10 + // use large dataset to force an iterative cursor. + numMembers := make([]string, 50000) + numMembersResult := make([]api.Result[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)) + } + + // empty set + resCursor, resCollection, err := client.SScan(key1, initialCursor) + assert.NoError(t, err) + assert.Equal(t, initialCursor, resCursor.Value()) + 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.Empty(t, resCollection) + } else { + _, _, err = client.SScan(key1, "-1") + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + } + + // result contains the whole set + res, err := client.SAdd(key1, charMembers) + assert.NoError(t, err) + assert.Equal(t, int64(len(charMembers)), res.Value()) + resCursor, resCollection, err = client.SScan(key1, initialCursor) + assert.NoError(t, err) + assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, len(charMembers), len(resCollection)) + assert.True(t, isSubset(resCollection, charMembersResult)) + + opts := api.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")})) + + // result contains a subset of the key + res, err = client.SAdd(key1, numMembers) + assert.NoError(t, err) + assert.Equal(t, int64(50000), res.Value()) + resCursor, resCollection, err = client.SScan(key1, "0") + assert.NoError(t, err) + resultCollection := resCollection + + // 0 is returned for the cursor of the last iteration + for resCursor.Value() != "0" { + nextCursor, nextCol, err := client.SScan(key1, resCursor.Value()) + assert.NoError(t, err) + assert.NotEqual(t, nextCursor, resCursor) + assert.False(t, isSubset(resultCollection, nextCol)) + resultCollection = append(resultCollection, nextCol...) + resCursor = nextCursor + } + assert.NotEmpty(t, resultCollection) + assert.True(t, isSubset(numMembersResult, resultCollection)) + assert.True(t, isSubset(charMembersResult, resultCollection)) + + // test match pattern + opts = api.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) + 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) + resCursor, resCollection, err = client.SScanWithOptions(key1, initialCursor, opts) + assert.NoError(t, err) + assert.NotEqual(t, initialCursor, resCursor.Value()) + assert.GreaterOrEqual(t, len(resCollection), 0) + + // exceptions + // non-set key + _, err = client.Set(key2, "test") + assert.NoError(t, err) + + _, _, err = client.SScan(key2, initialCursor) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + func (suite *GlideTestSuite) TestLRange() { suite.runWithDefaultClients(func(client api.BaseClient) { list := []string{"value4", "value3", "value2", "value1"} diff --git a/go/integTest/test_utils.go b/go/integTest/test_utils.go new file mode 100644 index 0000000000..10f2fb3be1 --- /dev/null +++ b/go/integTest/test_utils.go @@ -0,0 +1,19 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +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{}) + for _, v := range sliceB { + setB[v.Value()] = struct{}{} + } + for _, v := range sliceA { + if _, found := setB[v.Value()]; !found { + return false + } + } + return true +} From a8040a5b6fe234dc7c8cf88c4d4baab6a3e94182 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Wed, 18 Dec 2024 12:12:48 -0800 Subject: [PATCH 02/17] Go: Add custom command to cluster client (#2803) * Add custom command to cluster client Signed-off-by: Yury-Fridlyand --- go/Makefile | 4 +- go/api/glide_client.go | 17 ++++- go/api/glide_cluster_client.go | 42 ++++++++++++ go/api/response_handlers.go | 12 ++-- go/api/response_types.go | 84 ++++++++++++++++++++++++ go/integTest/cluster_commands_test.go | 29 ++++++++ go/integTest/standalone_commands_test.go | 4 +- 7 files changed, 181 insertions(+), 11 deletions(-) create mode 100644 go/integTest/cluster_commands_test.go diff --git a/go/Makefile b/go/Makefile index 3d496e3881..41d0b13d7e 100644 --- a/go/Makefile +++ b/go/Makefile @@ -38,7 +38,8 @@ clean: go clean rm -f lib.h rm -f benchmarks/benchmarks - + rm -rf protobuf + rm -rf target build-glide-client: cargo build --release @@ -49,6 +50,7 @@ build-glide-client-debug: cbindgen --config cbindgen.toml --crate glide-rs --output lib.h generate-protobuf: + rm -rf protobuf mkdir -p protobuf protoc --proto_path=../glide-core/src/protobuf \ --go_opt=Mconnection_request.proto=github.com/valkey-io/valkey-glide/go/protobuf \ diff --git a/go/api/glide_client.go b/go/api/glide_client.go index 2f8e8e5235..04e43266f2 100644 --- a/go/api/glide_client.go +++ b/go/api/glide_client.go @@ -26,13 +26,26 @@ func NewGlideClient(config *GlideClientConfiguration) (*GlideClient, error) { // the command name and subcommands, should be added as a separate value in args. The returning value depends on the executed // command. // +// See [Valkey GLIDE Wiki] for details on the restrictions and limitations of the custom command API. +// // This function should only be used for single-response commands. Commands that don't return complete response and awaits // (such as SUBSCRIBE), or that return potentially more than a single response (such as XREAD), or that change the client's // behavior (such as entering pub/sub mode on RESP2 connections) shouldn't be called using this function. // -// For example, to return a list of all pub/sub clients: +// Parameters: +// +// args - Arguments for the custom command including the command name. +// +// Return value: +// +// The returned value for the custom command. +// +// For example: +// +// result, err := client.CustomCommand([]string{"ping"}) +// result.(string): "PONG" // -// client.CustomCommand([]string{"CLIENT", "LIST","TYPE", "PUBSUB"}) +// [Valkey GLIDE Wiki]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command func (client *GlideClient) CustomCommand(args []string) (interface{}, error) { res, err := client.executeCommand(C.CustomCommand, args) if err != nil { diff --git a/go/api/glide_cluster_client.go b/go/api/glide_cluster_client.go index 7ab186d7c2..ec7c034818 100644 --- a/go/api/glide_cluster_client.go +++ b/go/api/glide_cluster_client.go @@ -2,6 +2,10 @@ package api +// #cgo LDFLAGS: -L../target/release -lglide_rs +// #include "../lib.h" +import "C" + // GlideClusterClient is a client used for connection in cluster mode. type GlideClusterClient struct { *baseClient @@ -16,3 +20,41 @@ func NewGlideClusterClient(config *GlideClusterClientConfiguration) (*GlideClust return &GlideClusterClient{client}, nil } + +// CustomCommand executes a single command, specified by args, without checking inputs. Every part of the command, including +// the command name and subcommands, should be added as a separate value in args. The returning value depends on the executed +// command. +// +// The command will be routed automatically based on the passed command's default request policy. +// +// See [Valkey GLIDE Wiki] for details on the restrictions and limitations of the custom command API. +// +// This function should only be used for single-response commands. Commands that don't return complete response and awaits +// (such as SUBSCRIBE), or that return potentially more than a single response (such as XREAD), or that change the client's +// behavior (such as entering pub/sub mode on RESP2 connections) shouldn't be called using this function. +// +// Parameters: +// +// args - Arguments for the custom command including the command name. +// +// Return value: +// +// The returned value for the custom command. +// +// For example: +// +// result, err := client.CustomCommand([]string{"ping"}) +// result.Value().(string): "PONG" +// +// [Valkey GLIDE Wiki]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command +func (client *GlideClusterClient) CustomCommand(args []string) (ClusterValue[interface{}], error) { + res, err := client.executeCommand(C.CustomCommand, args) + if err != nil { + return CreateEmptyClusterValue(), err + } + data, err := handleInterfaceResponse(res) + if err != nil { + return CreateEmptyClusterValue(), err + } + return CreateClusterValue(data), nil +} diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index 0a0e15204c..618a95bfc2 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -123,9 +123,9 @@ func parseMap(response *C.struct_CommandResponse) (interface{}, error) { return nil, nil } - value_map := make(map[interface{}]interface{}, response.array_value_len) + value_map := make(map[string]interface{}, response.array_value_len) for _, v := range unsafe.Slice(response.array_value, response.array_value_len) { - res_key, err := parseInterface(v.map_key) + res_key, err := parseString(v.map_key) if err != nil { return nil, err } @@ -133,7 +133,7 @@ func parseMap(response *C.struct_CommandResponse) (interface{}, error) { if err != nil { return nil, err } - value_map[res_key] = res_val + value_map[res_key.(string)] = res_val } return value_map, nil } @@ -143,13 +143,13 @@ func parseSet(response *C.struct_CommandResponse) (interface{}, error) { return nil, nil } - slice := make(map[interface{}]struct{}, response.sets_value_len) + slice := make(map[string]struct{}, response.sets_value_len) for _, v := range unsafe.Slice(response.sets_value, response.sets_value_len) { - res, err := parseInterface(&v) + res, err := parseString(&v) if err != nil { return nil, err } - slice[res] = struct{}{} + slice[res.(string)] = struct{}{} } return slice, nil diff --git a/go/api/response_types.go b/go/api/response_types.go index 2a480adf57..3146032b04 100644 --- a/go/api/response_types.go +++ b/go/api/response_types.go @@ -46,3 +46,87 @@ func CreateBoolResult(boolVal bool) Result[bool] { func CreateNilBoolResult() Result[bool] { return Result[bool]{val: false, isNil: true} } + +// Enum to distinguish value types stored in `ClusterValue` +type ValueType int + +const ( + SingleValue ValueType = 1 + MultiValue ValueType = 2 +) + +// Enum-like structure which stores either a single-node response or multi-node response. +// Multi-node response stored in a map, where keys are hostnames or ":" strings. +// +// For example: +// +// // Command failed: +// value, err := clusterClient.CustomCommand(args) +// value.IsEmpty(): true +// err != nil: true +// +// // Command returns response from multiple nodes: +// value, _ := clusterClient.info() +// node, nodeResponse := range value.Value().(map[string]interface{}) { +// response := nodeResponse.(string) +// // `node` stores cluster node IP/hostname, `response` stores the command output from that node +// } +// +// // Command returns a response from single node: +// value, _ := clusterClient.infoWithRoute(Random{}) +// response := value.Value().(string) +// // `response` stores the command output from a cluster node +type ClusterValue[T any] struct { + valueType ValueType + value Result[T] +} + +func (value ClusterValue[T]) Value() T { + return value.value.Value() +} + +func (value ClusterValue[T]) ValueType() ValueType { + return value.valueType +} + +func (value ClusterValue[T]) IsSingleValue() bool { + return value.valueType == SingleValue +} + +func (value ClusterValue[T]) IsMultiValue() bool { + return value.valueType == MultiValue +} + +func (value ClusterValue[T]) IsEmpty() bool { + return value.value.IsNil() +} + +func CreateClusterValue[T any](data T) ClusterValue[T] { + switch any(data).(type) { + case map[string]interface{}: + return CreateClusterMultiValue(data) + default: + return CreateClusterSingleValue(data) + } +} + +func CreateClusterSingleValue[T any](data T) ClusterValue[T] { + return ClusterValue[T]{ + valueType: SingleValue, + value: Result[T]{val: data, isNil: false}, + } +} + +func CreateClusterMultiValue[T any](data T) ClusterValue[T] { + return ClusterValue[T]{ + valueType: MultiValue, + value: Result[T]{val: data, isNil: false}, + } +} + +func CreateEmptyClusterValue() ClusterValue[interface{}] { + var empty interface{} + return ClusterValue[interface{}]{ + value: Result[interface{}]{val: empty, isNil: true}, + } +} diff --git a/go/integTest/cluster_commands_test.go b/go/integTest/cluster_commands_test.go new file mode 100644 index 0000000000..142f0cf273 --- /dev/null +++ b/go/integTest/cluster_commands_test.go @@ -0,0 +1,29 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package integTest + +import ( + "strings" + + "github.com/stretchr/testify/assert" +) + +func (suite *GlideTestSuite) TestClusterCustomCommandInfo() { + client := suite.defaultClusterClient() + result, err := client.CustomCommand([]string{"INFO"}) + + assert.Nil(suite.T(), err) + // INFO is routed to all primary nodes by default + for _, value := range result.Value().(map[string]interface{}) { + assert.True(suite.T(), strings.Contains(value.(string), "# Stats")) + } +} + +func (suite *GlideTestSuite) TestClusterCustomCommandEcho() { + client := suite.defaultClusterClient() + result, err := client.CustomCommand([]string{"ECHO", "GO GLIDE GO"}) + + assert.Nil(suite.T(), err) + // ECHO is routed to a single random node + assert.Equal(suite.T(), "GO GLIDE GO", result.Value().(string)) +} diff --git a/go/integTest/standalone_commands_test.go b/go/integTest/standalone_commands_test.go index 80cbb63581..e886645c13 100644 --- a/go/integTest/standalone_commands_test.go +++ b/go/integTest/standalone_commands_test.go @@ -128,7 +128,7 @@ func (suite *GlideTestSuite) TestCustomCommandConfigGet_MapResponse() { result2, err := client.CustomCommand([]string{"CONFIG", "GET", "timeout", "maxmemory"}) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), map[interface{}]interface{}{"timeout": "1000", "maxmemory": "1073741824"}, result2) + assert.Equal(suite.T(), map[string]interface{}{"timeout": "1000", "maxmemory": "1073741824"}, result2) } func (suite *GlideTestSuite) TestCustomCommandConfigSMembers_SetResponse() { @@ -142,7 +142,7 @@ func (suite *GlideTestSuite) TestCustomCommandConfigSMembers_SetResponse() { result2, err := client.CustomCommand([]string{"SMEMBERS", key}) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), map[interface{}]struct{}{"member1": {}, "member2": {}, "member3": {}}, result2) + assert.Equal(suite.T(), map[string]struct{}{"member1": {}, "member2": {}, "member3": {}}, result2) } func (suite *GlideTestSuite) TestCustomCommand_invalidCommand() { From b4231b0b9e929c48cc760fbb76b8b1150b58b4d8 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 19 Dec 2024 12:36:50 -0800 Subject: [PATCH 03/17] go: modules CI (#2801) * Modules CI Signed-off-by: Yury-Fridlyand --- .github/workflows/go.yml | 51 ++++--- go/Cargo.toml | 2 +- go/DEVELOPER.md | 23 ++-- go/Makefile | 22 ++- go/integTest/connection_test.go | 8 +- go/integTest/glide_test_suite_test.go | 163 +++++++++++++++++------ go/integTest/json_module_test.go | 20 +++ go/integTest/standalone_commands_test.go | 4 +- go/integTest/vss_module_test.go | 20 +++ 9 files changed, 240 insertions(+), 73 deletions(-) create mode 100644 go/integTest/json_module_test.go create mode 100644 go/integTest/vss_module_test.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 1d17640188..044c0ac369 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -99,21 +99,11 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} engine-version: ${{ matrix.engine.version }} - - name: Install tools for Go ${{ matrix.go }} - working-directory: ./go - run: make install-tools-go${{ matrix.go }} - - - name: Set LD_LIBRARY_PATH - if: ${{ matrix.os == 'ubuntu-latest' }} - run: echo "LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$GITHUB_WORKSPACE/go/target/release/deps/" >> $GITHUB_ENV - - - name: Build client - working-directory: ./go - run: make build - - - name: Run tests - working-directory: ./go - run: make test + - name: Install & build & test + working-directory: go + run: | + LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$GITHUB_WORKSPACE/go/target/release/deps/ + make install-tools-go${{ matrix.go }} build unit-test integ-test - uses: ./.github/workflows/test-benchmark with: @@ -216,7 +206,7 @@ jobs: working-directory: go run: | LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$GITHUB_WORKSPACE/go/target/release/deps/ - make install-tools-go${{ matrix.go }} build test + make install-tools-go${{ matrix.go }} build unit-test integ-test - name: Upload test reports if: always() @@ -227,3 +217,32 @@ jobs: path: | utils/clusters/** benchmarks/results/** + + test-modules: + if: (github.repository_owner == 'valkey-io' && github.event_name == 'workflow_dispatch') || github.event.pull_request.head.repo.owner.login == 'valkey-io' + environment: AWS_ACTIONS + name: Modules Tests + runs-on: [self-hosted, linux, ARM64] + timeout-minutes: 15 + steps: + - name: Setup self-hosted runner access + run: sudo chown -R $USER:$USER /home/ubuntu/actions-runner/_work/valkey-glide + + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.20.0" + cache-dependency-path: go/go.sum + + - name: Build and test + working-directory: ./go + run: | + make install-tools-go1.20.0 + LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$GITHUB_WORKSPACE/go/target/release/deps/ + make build + make modules-test cluster-endpoints=${{ secrets.MEMDB_MODULES_ENDPOINT }} tls=true + + # TODO: + # Upload test reports diff --git a/go/Cargo.toml b/go/Cargo.toml index 48556820fd..90f86983e7 100644 --- a/go/Cargo.toml +++ b/go/Cargo.toml @@ -9,7 +9,7 @@ authors = ["Valkey GLIDE Maintainers"] crate-type = ["cdylib"] [dependencies] -redis = { path = "../glide-core/redis-rs/redis", features = ["aio", "tokio-comp", "tls", "tokio-native-tls-comp", "tls-rustls-insecure"] } +redis = { path = "../glide-core/redis-rs/redis", features = ["aio", "tokio-comp", "connection-manager", "tokio-rustls-comp"] } glide-core = { path = "../glide-core", features = ["socket-layer"] } tokio = { version = "^1", features = ["rt", "macros", "rt-multi-thread", "time"] } protobuf = { version = "3.3.0", features = [] } diff --git a/go/DEVELOPER.md b/go/DEVELOPER.md index 35879e9ed3..5619b7f7b2 100644 --- a/go/DEVELOPER.md +++ b/go/DEVELOPER.md @@ -140,30 +140,35 @@ Before starting this step, make sure you've installed all software requirements. ### Test -To run tests, use the following command: +To run tests, use the benefit on makefile. To run unit tests, run the following command: ```bash -go test -race ./... +make unit-test ``` -For more detailed test output, add the `-v` flag: +To run integration tests, run: ```bash -go test -race ./... -v +make integ-test ``` -To execute a specific test, include `-run `. For example: +To run modules tests, run: ```bash -go test -race ./... -run TestConnectionRequestProtobufGeneration_allFieldsSet -v +make modules-test ``` -### Submodules +To execute a specific test, include `test-filter=`. For example: -After pulling new changes, ensure that you update the submodules by running the following command: +```bash +make unit-test test-filter=TestConnectionRequestProtobufGeneration_allFieldsSet +``` + +Integration and modules tests accept `standalone-endpoints`, `cluster-endpoints` and `tls` parameters to run tests on existing servers. +By default, those test suite start standalone and cluster servers without TLS and stop them at the end. ```bash -git submodule update --init --recursive +make integ-test standalone-endpoints=localhost:6379 cluster-endpoints=localhost:7000 tls=true ``` ### Generate protobuf files diff --git a/go/Makefile b/go/Makefile index 41d0b13d7e..c4d4b5aeb4 100644 --- a/go/Makefile +++ b/go/Makefile @@ -74,13 +74,31 @@ format: gofumpt -w . golines -w --shorten-comments -m 127 . -test: +# unit tests - skip complete IT suite (including MT) +unit-test: 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 ./... + go test -v -race ./... -skip TestGlideTestSuite $(if $(test-filter), -run $(test-filter)) + +# integration tests - run subtask with skipping modules tests +integ-test: export TEST_FILTER = -skip TestGlideTestSuite/TestModule $(if $(test-filter), -run $(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: __it + +__it: + 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 ./integTest/... \ + $(TEST_FILTER) \ + $(if $(filter true, $(tls)), --tls,) \ + $(if $(standalone-endpoints), --standalone-endpoints=$(standalone-endpoints)) \ + $(if $(cluster-endpoints), --cluster-endpoints=$(cluster-endpoints)) # Note: this task is no longer run by CI because: # - build failures that occur while running the task can be hidden by the task; CI still reports success in these scenarios. # - there is not a good way to both generate a test report and log the test outcomes to GH actions. +# TODO: fix this and include -run/-skip flags test-and-report: mkdir -p reports go test -v -race ./... -json | go-test-report -o reports/test-report.html diff --git a/go/integTest/connection_test.go b/go/integTest/connection_test.go index 910772769b..22c6b809e8 100644 --- a/go/integTest/connection_test.go +++ b/go/integTest/connection_test.go @@ -9,7 +9,7 @@ import ( func (suite *GlideTestSuite) TestStandaloneConnect() { config := api.NewGlideClientConfiguration(). - WithAddress(&api.NodeAddress{Port: suite.standalonePorts[0]}) + WithAddress(&suite.standaloneHosts[0]) client, err := api.NewGlideClient(config) assert.Nil(suite.T(), err) @@ -20,8 +20,8 @@ func (suite *GlideTestSuite) TestStandaloneConnect() { func (suite *GlideTestSuite) TestClusterConnect() { config := api.NewGlideClusterClientConfiguration() - for _, port := range suite.clusterPorts { - config.WithAddress(&api.NodeAddress{Port: port}) + for _, host := range suite.clusterHosts { + config.WithAddress(&host) } client, err := api.NewGlideClusterClient(config) @@ -34,7 +34,7 @@ func (suite *GlideTestSuite) TestClusterConnect() { func (suite *GlideTestSuite) TestClusterConnect_singlePort() { config := api.NewGlideClusterClientConfiguration(). - WithAddress(&api.NodeAddress{Port: suite.clusterPorts[0]}) + WithAddress(&suite.clusterHosts[0]) client, err := api.NewGlideClusterClient(config) diff --git a/go/integTest/glide_test_suite_test.go b/go/integTest/glide_test_suite_test.go index fd4e0d92d5..520037dad0 100644 --- a/go/integTest/glide_test_suite_test.go +++ b/go/integTest/glide_test_suite_test.go @@ -4,6 +4,7 @@ package integTest import ( "errors" + "flag" "fmt" "log" "os" @@ -19,13 +20,20 @@ import ( type GlideTestSuite struct { suite.Suite - standalonePorts []int - clusterPorts []int + standaloneHosts []api.NodeAddress + clusterHosts []api.NodeAddress + tls bool serverVersion string clients []*api.GlideClient clusterClients []*api.GlideClusterClient } +var ( + tls = flag.Bool("tls", false, "one") + clusterHosts = flag.String("cluster-endpoints", "", "two") + standaloneHosts = flag.String("standalone-endpoints", "", "three") +) + func (suite *GlideTestSuite) SetupSuite() { // Stop cluster in case previous test run was interrupted or crashed and didn't stop. // If an error occurs, we ignore it in case the servers actually were stopped before running this. @@ -37,49 +45,71 @@ func (suite *GlideTestSuite) SetupSuite() { log.Fatal(err) } - // Start standalone instance - clusterManagerOutput := runClusterManager(suite, []string{"start", "-r", "0"}, false) - - suite.standalonePorts = extractPorts(suite, clusterManagerOutput) - suite.T().Logf("Standalone ports = %s", fmt.Sprint(suite.standalonePorts)) - - // Start cluster - clusterManagerOutput = runClusterManager(suite, []string{"start", "--cluster-mode"}, false) + cmd := []string{} + suite.tls = false + if *tls { + cmd = []string{"--tls"} + suite.tls = true + } + suite.T().Logf("TLS = %t", suite.tls) - suite.clusterPorts = extractPorts(suite, clusterManagerOutput) - suite.T().Logf("Cluster ports = %s", fmt.Sprint(suite.clusterPorts)) + // Note: code does not start standalone if cluster hosts are given and vice versa + startServer := true - // Get Redis version - byteOutput, err := exec.Command("redis-server", "-v").Output() - if err != nil { - suite.T().Fatal(err.Error()) + if *standaloneHosts != "" { + suite.standaloneHosts = parseHosts(suite, *standaloneHosts) + startServer = false + } + if *clusterHosts != "" { + suite.clusterHosts = parseHosts(suite, *clusterHosts) + startServer = false } + if startServer { + // Start standalone instance + clusterManagerOutput := runClusterManager(suite, append(cmd, "start", "-r", "3"), false) + suite.standaloneHosts = extractAddresses(suite, clusterManagerOutput) + + // Start cluster + clusterManagerOutput = runClusterManager(suite, append(cmd, "start", "--cluster-mode", "-r", "3"), false) + suite.clusterHosts = extractAddresses(suite, clusterManagerOutput) + } + + suite.T().Logf("Standalone hosts = %s", fmt.Sprint(suite.standaloneHosts)) + suite.T().Logf("Cluster hosts = %s", fmt.Sprint(suite.clusterHosts)) - suite.serverVersion = extractServerVersion(string(byteOutput)) + // Get server version + suite.serverVersion = getServerVersion(suite) suite.T().Logf("Detected server version = %s", suite.serverVersion) } -func extractPorts(suite *GlideTestSuite, output string) []int { - var ports []int +func parseHosts(suite *GlideTestSuite, addresses string) []api.NodeAddress { + var result []api.NodeAddress + + addressList := strings.Split(addresses, ",") + for _, address := range addressList { + parts := strings.Split(address, ":") + port, err := strconv.Atoi(parts[1]) + if err != nil { + suite.T().Fatalf("Failed to parse port from string %s: %s", parts[1], err.Error()) + } + + result = append(result, api.NodeAddress{Host: parts[0], Port: port}) + } + return result +} + +func extractAddresses(suite *GlideTestSuite, output string) []api.NodeAddress { for _, line := range strings.Split(output, "\n") { if !strings.HasPrefix(line, "CLUSTER_NODES=") { continue } addresses := strings.Split(line, "=")[1] - addressList := strings.Split(addresses, ",") - for _, address := range addressList { - portString := strings.Split(address, ":")[1] - port, err := strconv.Atoi(portString) - if err != nil { - suite.T().Fatalf("Failed to parse port from cluster_manager.py output: %s", err.Error()) - } - - ports = append(ports, port) - } + return parseHosts(suite, addresses) } - return ports + suite.T().Fatalf("Failed to parse port from cluster_manager.py output") + return []api.NodeAddress{} } func runClusterManager(suite *GlideTestSuite, args []string, ignoreExitCode bool) string { @@ -108,13 +138,66 @@ func runClusterManager(suite *GlideTestSuite, args []string, ignoreExitCode bool return string(output) } -func extractServerVersion(output string) string { - // Redis response: - // Redis server v=7.2.3 sha=00000000:0 malloc=jemalloc-5.3.0 bits=64 build=7504b1fedf883f2 - // Valkey response: - // Server v=7.2.5 sha=26388270:0 malloc=jemalloc-5.3.0 bits=64 build=ea40bb1576e402d6 - versionSection := strings.Split(output, "v=")[1] - return strings.Split(versionSection, " ")[0] +func getServerVersion(suite *GlideTestSuite) string { + var err error = nil + if len(suite.standaloneHosts) > 0 { + config := api.NewGlideClientConfiguration(). + WithAddress(&suite.standaloneHosts[0]). + WithUseTLS(suite.tls). + WithRequestTimeout(5000) + + client, err := api.NewGlideClient(config) + if err == nil && client != nil { + defer client.Close() + // TODO use info command + info, _ := client.CustomCommand([]string{"info", "server"}) + return extractServerVersion(suite, info.(string)) + } + } + if len(suite.clusterHosts) == 0 { + if err != nil { + suite.T().Fatalf("No cluster hosts configured, standalone failed with %s", err.Error()) + } + suite.T().Fatal("No server hosts configured") + } + + config := api.NewGlideClusterClientConfiguration(). + WithAddress(&suite.clusterHosts[0]). + WithUseTLS(suite.tls). + WithRequestTimeout(5000) + + client, err := api.NewGlideClusterClient(config) + if err == nil && client != nil { + defer client.Close() + // TODO use info command with route + info, _ := client.CustomCommand([]string{"info", "server"}) + for _, value := range info.Value().(map[string]interface{}) { + return extractServerVersion(suite, value.(string)) + } + } + suite.T().Fatalf("Can't connect to any server to get version: %s", err.Error()) + return "" +} + +func extractServerVersion(suite *GlideTestSuite, output string) string { + // output format: + // # Server + // redis_version:7.2.3 + // ... + // It can contain `redis_version` or `valkey_version` key or both. If both, `valkey_version` should be taken + for _, line := range strings.Split(output, "\r\n") { + if strings.Contains(line, "valkey_version") { + return strings.Split(line, ":")[1] + } + } + + for _, line := range strings.Split(output, "\r\n") { + if strings.Contains(line, "redis_version") { + return strings.Split(line, ":")[1] + } + } + suite.T().Fatalf("Can't read server version from INFO command output: %s", output) + return "" } func TestGlideTestSuite(t *testing.T) { @@ -146,7 +229,8 @@ func (suite *GlideTestSuite) getDefaultClients() []api.BaseClient { func (suite *GlideTestSuite) defaultClient() *api.GlideClient { config := api.NewGlideClientConfiguration(). - WithAddress(&api.NodeAddress{Port: suite.standalonePorts[0]}). + WithAddress(&suite.standaloneHosts[0]). + WithUseTLS(suite.tls). WithRequestTimeout(5000) return suite.client(config) } @@ -163,7 +247,8 @@ func (suite *GlideTestSuite) client(config *api.GlideClientConfiguration) *api.G func (suite *GlideTestSuite) defaultClusterClient() *api.GlideClusterClient { config := api.NewGlideClusterClientConfiguration(). - WithAddress(&api.NodeAddress{Port: suite.clusterPorts[0]}). + WithAddress(&suite.clusterHosts[0]). + WithUseTLS(suite.tls). WithRequestTimeout(5000) return suite.clusterClient(config) } diff --git a/go/integTest/json_module_test.go b/go/integTest/json_module_test.go new file mode 100644 index 0000000000..8f0f33efc4 --- /dev/null +++ b/go/integTest/json_module_test.go @@ -0,0 +1,20 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package integTest + +import ( + "strings" + + "github.com/stretchr/testify/assert" +) + +func (suite *GlideTestSuite) TestModuleVerifyJsonLoaded() { + client := suite.defaultClusterClient() + // TODO use INFO command + result, err := client.CustomCommand([]string{"INFO", "MODULES"}) + + assert.Nil(suite.T(), err) + for _, value := range result.Value().(map[string]interface{}) { + assert.True(suite.T(), strings.Contains(value.(string), "# json_core_metrics")) + } +} diff --git a/go/integTest/standalone_commands_test.go b/go/integTest/standalone_commands_test.go index e886645c13..318c2d18ed 100644 --- a/go/integTest/standalone_commands_test.go +++ b/go/integTest/standalone_commands_test.go @@ -33,7 +33,7 @@ func (suite *GlideTestSuite) TestCustomCommandPing_StringResponse() { func (suite *GlideTestSuite) TestCustomCommandClientInfo() { clientName := "TEST_CLIENT_NAME" config := api.NewGlideClientConfiguration(). - WithAddress(&api.NodeAddress{Port: suite.standalonePorts[0]}). + WithAddress(&suite.standaloneHosts[0]). WithClientName(clientName) client := suite.client(config) @@ -92,7 +92,7 @@ func (suite *GlideTestSuite) TestCustomCommandIncrByFloat_FloatResponse() { func (suite *GlideTestSuite) TestCustomCommandMGet_ArrayResponse() { clientName := "TEST_CLIENT_NAME" config := api.NewGlideClientConfiguration(). - WithAddress(&api.NodeAddress{Port: suite.standalonePorts[0]}). + WithAddress(&suite.standaloneHosts[0]). WithClientName(clientName) client := suite.client(config) diff --git a/go/integTest/vss_module_test.go b/go/integTest/vss_module_test.go new file mode 100644 index 0000000000..1ebad87e13 --- /dev/null +++ b/go/integTest/vss_module_test.go @@ -0,0 +1,20 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package integTest + +import ( + "strings" + + "github.com/stretchr/testify/assert" +) + +func (suite *GlideTestSuite) TestModuleVerifyVssLoaded() { + client := suite.defaultClusterClient() + // TODO use INFO command + result, err := client.CustomCommand([]string{"INFO", "MODULES"}) + + assert.Nil(suite.T(), err) + for _, value := range result.Value().(map[string]interface{}) { + assert.True(suite.T(), strings.Contains(value.(string), "# search_index_stats")) + } +} From 24a2dd025c2cc14ca3fffc1cc771096cb59d4160 Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Thu, 19 Dec 2024 20:13:40 -0800 Subject: [PATCH 04/17] Go: Add command ZAdd (#2813) * Go: Add command ZAdd Signed-off-by: TJ Zhang --- CHANGELOG.md | 1 + go/api/base_client.go | 90 ++++++++++++++++++++++- go/api/options/zadd_options.go | 106 +++++++++++++++++++++++++++ go/api/sorted_set_commands.go | 91 +++++++++++++++++++++++ go/go.mod | 2 +- go/integTest/shared_commands_test.go | 92 +++++++++++++++++++++++ go/utils/transform_utils.go | 19 +++++ 7 files changed, 398 insertions(+), 3 deletions(-) create mode 100644 go/api/options/zadd_options.go create mode 100644 go/api/sorted_set_commands.go diff --git a/CHANGELOG.md b/CHANGELOG.md index e465f65ce9..6c7c5ed20e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Go: Add `SInterStore` ([#2779](https://github.com/valkey-io/valkey-glide/issues/2779)) * Node: Remove native package references for MacOs x64 architecture ([#2799](https://github.com/valkey-io/valkey-glide/issues/2799)) * Go: Add `SScan` and `SMove` ([#2789](https://github.com/valkey-io/valkey-glide/issues/2789)) +* Go: Add `ZADD` ([#2813](https://github.com/valkey-io/valkey-glide/issues/2813)) #### Breaking Changes diff --git a/go/api/base_client.go b/go/api/base_client.go index 0bb250fd07..0e4aea3995 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -15,6 +15,7 @@ import ( "strconv" "unsafe" + "github.com/valkey-io/valkey-glide/go/glide/api/options" "github.com/valkey-io/valkey-glide/go/glide/protobuf" "github.com/valkey-io/valkey-glide/go/glide/utils" "google.golang.org/protobuf/proto" @@ -26,6 +27,7 @@ type BaseClient interface { HashCommands ListCommands SetCommands + SortedSetCommands ConnectionManagementCommands GenericBaseCommands // Close terminates the client by closing all associated resources. @@ -46,6 +48,7 @@ func successCallback(channelPtr unsafe.Pointer, cResponse *C.struct_CommandRespo resultChannel <- payload{value: response, error: nil} } +// //export failureCallback func failureCallback(channelPtr unsafe.Pointer, cErrorMessage *C.char, cErrorType C.RequestErrorType) { resultChannel := *(*chan payload)(channelPtr) @@ -102,7 +105,10 @@ func (client *baseClient) Close() { client.coreClient = nil } -func (client *baseClient) executeCommand(requestType C.RequestType, args []string) (*C.struct_CommandResponse, error) { +func (client *baseClient) executeCommand( + requestType C.RequestType, + args []string, +) (*C.struct_CommandResponse, error) { if client.coreClient == nil { return nil, &ClosingError{"ExecuteCommand failed. The client is closed."} } @@ -769,7 +775,10 @@ func (client *baseClient) LInsert( return CreateNilInt64Result(), err } - result, err := client.executeCommand(C.LInsert, []string{key, insertPositionStr, pivot, element}) + result, err := client.executeCommand( + C.LInsert, + []string{key, insertPositionStr, pivot, element}, + ) if err != nil { return CreateNilInt64Result(), err } @@ -1204,3 +1213,80 @@ func (client *baseClient) Renamenx(key string, newKey string) (Result[bool], err } return handleBooleanResponse(result) } + +func (client *baseClient) ZAdd( + key string, + membersScoreMap map[string]float64, +) (Result[int64], error) { + result, err := client.executeCommand( + C.ZAdd, + append([]string{key}, utils.ConvertMapToValueKeyStringArray(membersScoreMap)...), + ) + if err != nil { + return CreateNilInt64Result(), err + } + + return handleLongResponse(result) +} + +func (client *baseClient) ZAddWithOptions( + key string, + membersScoreMap map[string]float64, + opts *options.ZAddOptions, +) (Result[int64], error) { + optionArgs, err := opts.ToArgs() + if err != nil { + return CreateNilInt64Result(), err + } + commandArgs := append([]string{key}, optionArgs...) + result, err := client.executeCommand( + C.ZAdd, + append(commandArgs, utils.ConvertMapToValueKeyStringArray(membersScoreMap)...), + ) + if err != nil { + return CreateNilInt64Result(), err + } + + return handleLongResponse(result) +} + +func (client *baseClient) zAddIncrBase(key string, opts *options.ZAddOptions) (Result[float64], error) { + optionArgs, err := opts.ToArgs() + if err != nil { + return CreateNilFloat64Result(), err + } + + result, err := client.executeCommand(C.ZAdd, append([]string{key}, optionArgs...)) + if err != nil { + return CreateNilFloat64Result(), err + } + + return handleDoubleResponse(result) +} + +func (client *baseClient) ZAddIncr( + key string, + member string, + increment float64, +) (Result[float64], error) { + options, err := options.NewZAddOptionsBuilder().SetIncr(true, increment, member) + if err != nil { + return CreateNilFloat64Result(), err + } + + return client.zAddIncrBase(key, options) +} + +func (client *baseClient) ZAddIncrWithOptions( + key string, + member string, + increment float64, + opts *options.ZAddOptions, +) (Result[float64], error) { + incrOpts, err := opts.SetIncr(true, increment, member) + if err != nil { + return CreateNilFloat64Result(), err + } + + return client.zAddIncrBase(key, incrOpts) +} diff --git a/go/api/options/zadd_options.go b/go/api/options/zadd_options.go new file mode 100644 index 0000000000..7926b346cc --- /dev/null +++ b/go/api/options/zadd_options.go @@ -0,0 +1,106 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +import ( + "errors" + + "github.com/valkey-io/valkey-glide/go/glide/utils" +) + +// Optional arguments to `ZAdd` in [SortedSetCommands] +type ZAddOptions struct { + conditionalChange ConditionalChange + updateOptions UpdateOptions + changed bool + incr bool + increment float64 + member string +} + +func NewZAddOptionsBuilder() *ZAddOptions { + return &ZAddOptions{} +} + +// `conditionalChangeā€œ defines conditions for updating or adding elements with {@link SortedSetBaseCommands#zadd} +// command. +func (options *ZAddOptions) SetConditionalChange(c ConditionalChange) *ZAddOptions { + options.conditionalChange = c + return options +} + +// `updateOptions` specifies conditions for updating scores with zadd command. +func (options *ZAddOptions) SetUpdateOptions(u UpdateOptions) *ZAddOptions { + options.updateOptions = u + return options +} + +// `Changed` changes the return value from the number of new elements added to the total number of elements changed. +func (options *ZAddOptions) SetChanged(ch bool) (*ZAddOptions, error) { + if options.incr { + return nil, errors.New("changed cannot be set when incr is true") + } + options.changed = ch + return options, nil +} + +// `INCR` sets the increment value to use when incr is true. +func (options *ZAddOptions) SetIncr(incr bool, increment float64, member string) (*ZAddOptions, error) { + if options.changed { + return nil, errors.New("incr cannot be set when changed is true") + } + options.incr = incr + options.increment = increment + options.member = member + return options, nil +} + +// `ToArgs` converts the options to a list of arguments. +func (opts *ZAddOptions) ToArgs() ([]string, error) { + args := []string{} + var err error + + if opts.conditionalChange == OnlyIfExists || opts.conditionalChange == OnlyIfDoesNotExist { + args = append(args, string(opts.conditionalChange)) + } + + if opts.updateOptions == ScoreGreaterThanCurrent || opts.updateOptions == ScoreLessThanCurrent { + args = append(args, string(opts.updateOptions)) + } + + if opts.changed { + args = append(args, ChangedKeyword) + } + + if opts.incr { + args = append(args, IncrKeyword, utils.FloatToString(opts.increment), opts.member) + } + + return args, err +} + +// A ConditionalSet defines whether a new value should be set or not. +type ConditionalChange string + +const ( + // Only update elements that already exist. Don't add new elements. Equivalent to "XX" in the Valkey API. + OnlyIfExists ConditionalChange = "XX" + // Only add new elements. Don't update already existing elements. Equivalent to "NX" in the Valkey API. + OnlyIfDoesNotExist ConditionalChange = "NX" +) + +type UpdateOptions string + +const ( + // Only update existing elements if the new score is less than the current score. Equivalent to + // "LT" in the Valkey API. + ScoreLessThanCurrent UpdateOptions = "LT" + // Only update existing elements if the new score is greater than the current score. Equivalent + // to "GT" in the Valkey API. + ScoreGreaterThanCurrent UpdateOptions = "GT" +) + +const ( + ChangedKeyword string = "CH" // Valkey API keyword used to return total number of elements changed + IncrKeyword string = "INCR" // Valkey API keyword to make zadd act like ZINCRBY. +) diff --git a/go/api/sorted_set_commands.go b/go/api/sorted_set_commands.go new file mode 100644 index 0000000000..8eecf70120 --- /dev/null +++ b/go/api/sorted_set_commands.go @@ -0,0 +1,91 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +import ( + "github.com/valkey-io/valkey-glide/go/glide/api/options" +) + +// SortedSetCommands supports commands and transactions for the "Sorted Set Commands" group for standalone and cluster clients. +// +// See [valkey.io] for details. +// +// [valkey.io]: https://valkey.io/commands/#sorted-set +type SortedSetCommands interface { + // Adds one or more members to a sorted set, or updates their scores. Creates the key if it doesn't exist. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the set. + // membersScoreMap - A map of members to their scores. + // + // Return value: + // Result[int64] - The number of members added to the set. + // + // Example: + // res, err := client.ZAdd(key, map[string]float64{"one": 1.0, "two": 2.0, "three": 3.0}) + // fmt.Println(res.Value()) // Output: 3 + // + // [valkey.io]: https://valkey.io/commands/zadd/ + ZAdd(key string, membersScoreMap map[string]float64) (Result[int64], error) + + // Adds one or more members to a sorted set, or updates their scores. Creates the key if it doesn't exist. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the set. + // membersScoreMap - A map of members to their scores. + // opts - The options for the command. See [ZAddOptions] for details. + // + // Return value: + // Result[int64] - The number of members added to the set. If CHANGED is set, the number of members that were updated. + // + // Example: + // res, err := client.ZAddWithOptions(key, map[string]float64{"one": 1.0, "two": 2.0, "three": 3.0}, + // options.NewZAddOptionsBuilder().SetChanged(true).Build()) + // fmt.Println(res.Value()) // Output: 3 + // + // [valkey.io]: https://valkey.io/commands/zadd/ + ZAddWithOptions(key string, membersScoreMap map[string]float64, opts *options.ZAddOptions) (Result[int64], error) + + // Adds one or more members to a sorted set, or updates their scores. Creates the key if it doesn't exist. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the set. + // member - The member to add to. + // increment - The increment to add to the member's score. + // + // Return value: + // Result[float64] - The new score of the member. + // + // Example: + // res, err := client.ZAddIncr(key, "one", 1.0) + // fmt.Println(res.Value()) // Output: 1.0 + // + // [valkey.io]: https://valkey.io/commands/zadd/ + ZAddIncr(key string, member string, increment float64) (Result[float64], error) + + // Adds one or more members to a sorted set, or updates their scores. Creates the key if it doesn't exist. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the set. + // member - The member to add to. + // increment - The increment to add to the member's score. + // opts - The options for the command. See [ZAddOptions] for details. + // + // Return value: + // Result[float64] - The new score of the member. + // + // Example: + // res, err := client.ZAddIncrWithOptions(key, "one", 1.0, options.NewZAddOptionsBuilder().SetChanged(true)) + // fmt.Println(res.Value()) // Output: 1.0 + // + // [valkey.io]: https://valkey.io/commands/zadd/ + ZAddIncrWithOptions(key string, member string, increment float64, opts *options.ZAddOptions) (Result[float64], error) +} diff --git a/go/go.mod b/go/go.mod index cbca0b10fa..1cd188a865 100644 --- a/go/go.mod +++ b/go/go.mod @@ -3,6 +3,7 @@ module github.com/valkey-io/valkey-glide/go/glide go 1.20 require ( + github.com/google/uuid v1.6.0 github.com/stretchr/testify v1.8.4 google.golang.org/protobuf v1.33.0 ) @@ -10,7 +11,6 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/go-cmp v0.6.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 63f0e39ec7..972dfd8593 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -11,6 +11,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/valkey-io/valkey-glide/go/glide/api" + "github.com/valkey-io/valkey-glide/go/glide/api/options" ) const ( @@ -3781,3 +3782,94 @@ func (suite *GlideTestSuite) TestRenamenx() { assert.Equal(suite.T(), false, res2.Value()) }) } + +func (suite *GlideTestSuite) TestZAddAndZAddIncr() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + key2 := uuid.New().String() + key3 := uuid.New().String() + key4 := uuid.New().String() + membersScoreMap := map[string]float64{ + "one": 1.0, + "two": 2.0, + "three": 3.0, + } + t := suite.T() + + res, err := client.ZAdd(key, membersScoreMap) + assert.Nil(t, err) + assert.Equal(t, int64(3), res.Value()) + + resIncr, err := client.ZAddIncr(key, "one", float64(2)) + assert.Nil(t, err) + assert.Equal(t, float64(3), resIncr.Value()) + + // exceptions + // non-sortedset key + _, err = client.Set(key2, "test") + assert.NoError(t, err) + + _, err = client.ZAdd(key2, membersScoreMap) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + // wrong key type for zaddincr + _, err = client.ZAddIncr(key2, "one", float64(2)) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + // with NX & XX + onlyIfExistsOpts := options.NewZAddOptionsBuilder().SetConditionalChange(options.OnlyIfExists) + onlyIfDoesNotExistOpts := options.NewZAddOptionsBuilder().SetConditionalChange(options.OnlyIfDoesNotExist) + + res, err = client.ZAddWithOptions(key3, membersScoreMap, onlyIfExistsOpts) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(0), res.Value()) + + res, err = client.ZAddWithOptions(key3, membersScoreMap, onlyIfDoesNotExistOpts) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(3), res.Value()) + + resIncr, err = client.ZAddIncrWithOptions(key3, "one", 5, onlyIfDoesNotExistOpts) + assert.NotNil(suite.T(), err) + assert.True(suite.T(), resIncr.IsNil()) + + resIncr, err = client.ZAddIncrWithOptions(key3, "one", 5, onlyIfExistsOpts) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), float64(6), resIncr.Value()) + + // with GT or LT + membersScoreMap2 := map[string]float64{ + "one": -3.0, + "two": 2.0, + "three": 3.0, + } + + res, err = client.ZAdd(key4, membersScoreMap2) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(3), res.Value()) + + membersScoreMap2["one"] = 10.0 + + gtOpts := options.NewZAddOptionsBuilder().SetUpdateOptions(options.ScoreGreaterThanCurrent) + ltOpts := options.NewZAddOptionsBuilder().SetUpdateOptions(options.ScoreLessThanCurrent) + gtOptsChanged, _ := options.NewZAddOptionsBuilder().SetUpdateOptions(options.ScoreGreaterThanCurrent).SetChanged(true) + ltOptsChanged, _ := options.NewZAddOptionsBuilder().SetUpdateOptions(options.ScoreLessThanCurrent).SetChanged(true) + + res, err = client.ZAddWithOptions(key4, membersScoreMap2, gtOptsChanged) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(1), res.Value()) + + res, err = client.ZAddWithOptions(key4, membersScoreMap2, ltOptsChanged) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(0), res.Value()) + + resIncr, err = client.ZAddIncrWithOptions(key4, "one", -3, ltOpts) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), float64(7), resIncr.Value()) + + resIncr, err = client.ZAddIncrWithOptions(key4, "one", -3, gtOpts) + assert.NotNil(suite.T(), err) + assert.True(suite.T(), resIncr.IsNil()) + }) +} diff --git a/go/utils/transform_utils.go b/go/utils/transform_utils.go index cddc6a8e0e..f83541d168 100644 --- a/go/utils/transform_utils.go +++ b/go/utils/transform_utils.go @@ -50,6 +50,25 @@ func MapToString(parameter map[string]string) []string { return flat } +// Flattens a map[string, V] to a value-key string array +func ConvertMapToValueKeyStringArray[V any](args map[string]V) []string { + result := make([]string, 0, len(args)*2) + for key, value := range args { + // Convert the value to a string after type checking + switch v := any(value).(type) { + case string: + result = append(result, v) + case int64: + result = append(result, strconv.FormatInt(v, 10)) + case float64: + result = append(result, strconv.FormatFloat(v, 'f', -1, 64)) + } + // Append the key + result = append(result, key) + } + return result +} + // Concat concatenates multiple slices of strings into a single slice. func Concat(slices ...[]string) []string { size := 0 From e109c6bd506c476f10fd032cfdc9e3b3f056d25a Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Fri, 20 Dec 2024 02:08:20 -0800 Subject: [PATCH 05/17] GO: HIncrByFloat command Signed-off-by: Prateek Kumar --- go/api/base_client.go | 9 ++++++++ go/api/commands.go | 21 ++++++++++++++++++ go/integTest/shared_commands_test.go | 33 ++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/go/api/base_client.go b/go/api/base_client.go index 0bb250fd07..795360422b 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -436,6 +436,15 @@ func (client *baseClient) HStrLen(key string, field string) (Result[int64], erro return handleLongResponse(result) } +func (client *baseClient) HIncrByFloat(key string, field string, increment float64) (Result[float64], error) { + result, err := client.executeCommand(C.HIncrByFloat, []string{key, field, utils.FloatToString(increment)}) + if err != nil { + return CreateNilFloat64Result(), err + } + + return handleDoubleResponse(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 { diff --git a/go/api/commands.go b/go/api/commands.go index c99af91f29..8eff3fc668 100644 --- a/go/api/commands.go +++ b/go/api/commands.go @@ -704,6 +704,27 @@ type HashCommands interface { // // [valkey.io]: https://valkey.io/commands/hstrlen/ HStrLen(key string, field string) (Result[int64], error) + + // Increments the string representing a floating point number stored at `field`` in the hash stored at `key` by increment. + // By using a negative increment value, the value stored at field in the hash stored at `key` is decremented. + // If `field` or `key` does not exist, it is set to 0 before performing the operation. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // field - The field in the hash stored at `key` to increment its value. + // increment - The amount to increment. + // + // Return value: + // The Result[float64] value of `field` in the hash stored at `key` after the increment. + // For example: + // hsetResult, err := client.HSet("key", map[string]string{"field": "10"}) + // hincrByFloatResult, err := client.HIncrByFloat("key", "field", 1.5) + // // hincrByFloatResult.Value(): 11.5 + // + // [valkey.io]: https://valkey.io/commands/hincrbyfloat/ + HIncrByFloat(key string, field string, increment float64) (Result[float64], error) } // ConnectionManagementCommands defines an interface for connection management-related commands. diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 63f0e39ec7..243eb4bb97 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -1039,6 +1039,39 @@ func (suite *GlideTestSuite) TestHStrLen_WithNotExistingField() { }) } +func (suite *GlideTestSuite) TestHIncrByFloat_WithExistingField() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + field := uuid.NewString() + fieldValueMap := map[string]string{field: "10"} + + hsetResult, err := client.HSet(key, fieldValueMap) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(1), hsetResult.Value()) + + hincrByFloatResult, hincrByFloarErr := client.HIncrByFloat(key, field, 1.5) + assert.Nil(suite.T(), hincrByFloarErr) + assert.Equal(suite.T(), float64(11.5), hincrByFloatResult.Value()) + }) +} + +func (suite *GlideTestSuite) TestHIncrByFloat_WithNonExistingField() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + field := uuid.NewString() + field2 := uuid.NewString() + fieldValueMap := map[string]string{field2: "1"} + + hsetResult, err := client.HSet(key, fieldValueMap) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(1), hsetResult.Value()) + + hincrByFloatResult, hincrByFloarErr := client.HIncrByFloat(key, field, 1.5) + assert.Nil(suite.T(), hincrByFloarErr) + assert.Equal(suite.T(), float64(1.5), hincrByFloatResult.Value()) + }) +} + func (suite *GlideTestSuite) TestLPushLPop_WithExistingKey() { suite.runWithDefaultClients(func(client api.BaseClient) { list := []string{"value4", "value3", "value2", "value1"} From 8696ae4ab4efa59feb385ab9d9f064e4fc0e936f Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Fri, 20 Dec 2024 02:11:23 -0800 Subject: [PATCH 06/17] CHANGELOG.md file updated Signed-off-by: Prateek Kumar --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e465f65ce9..cb2f1db45e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ #### Changes - -* Go: Add SUNIONSTORE command ([#2805](https://github.com/valkey-io/valkey-glide/pull/2805) -* Go: Add SUNION ([#2787](https://github.com/valkey-io/valkey-glide/pull/2787) +* GO: Add HIncrByFloat command ([#2846](https://github.com/valkey-io/valkey-glide/pull/2846)) +* Go: Add SUNIONSTORE command ([#2805](https://github.com/valkey-io/valkey-glide/pull/2805)) +* Go: Add SUNION ([#2787](https://github.com/valkey-io/valkey-glide/pull/2787)) * Java: bump `netty` version ([#2795](https://github.com/valkey-io/valkey-glide/pull/2795)) * Java: Bump protobuf (protoc) version ([#2796](https://github.com/valkey-io/valkey-glide/pull/2796), [#2800](https://github.com/valkey-io/valkey-glide/pull/2800)) * Go: Add `SInterStore` ([#2779](https://github.com/valkey-io/valkey-glide/issues/2779)) From 5763eb15fc4374183211438a17cb53a439f23408 Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Fri, 20 Dec 2024 11:09:28 -0800 Subject: [PATCH 07/17] GO: Add hIncrBy command Signed-off-by: Prateek Kumar --- go/api/base_client.go | 9 ++++++++ go/api/commands.go | 22 +++++++++++++++++++ go/integTest/shared_commands_test.go | 33 ++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/go/api/base_client.go b/go/api/base_client.go index 0e4aea3995..091d3a2cb1 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -442,6 +442,15 @@ func (client *baseClient) HStrLen(key string, field string) (Result[int64], erro return handleLongResponse(result) } +func (client *baseClient) HIncrBy(key string, field string, increment int64) (Result[int64], error) { + result, err := client.executeCommand(C.HIncrBy, []string{key, field, utils.IntToString(increment)}) + if err != nil { + return CreateNilInt64Result(), err + } + + return handleLongResponse(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 { diff --git a/go/api/commands.go b/go/api/commands.go index c99af91f29..6f28299610 100644 --- a/go/api/commands.go +++ b/go/api/commands.go @@ -704,6 +704,28 @@ type HashCommands interface { // // [valkey.io]: https://valkey.io/commands/hstrlen/ HStrLen(key string, field string) (Result[int64], error) + + // Increments the number stored at `field` in the hash stored at `key` by increment. + // By using a negative increment value, the value stored at `field` in the hash stored at `key` is decremented. + // If `field` or `key` does not exist, it is set to 0 before performing the operation. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // field - The field in the hash stored at `key` to increment its value. + // increment - The amount to increment. + // + // Return value: + // The Result[int64] value of `field` in the hash stored at `key` after the increment. + // + // For example: + // hsetResult, err := client.HSet("key", map[string]string{"field": "10"}) + // hincrByResult, err := client.HIncrBy("key", "field", 1) + // // hincrByResult.Value(): 11 + // + // [valkey.io]: https://valkey.io/commands/hincrby/ + HIncrBy(key string, field string, increment int64) (Result[int64], error) } // ConnectionManagementCommands defines an interface for connection management-related commands. diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 972dfd8593..8c65edf39a 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -1040,6 +1040,39 @@ func (suite *GlideTestSuite) TestHStrLen_WithNotExistingField() { }) } +func (suite *GlideTestSuite) TestHIncrBy_WithExistingField() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + field := uuid.NewString() + fieldValueMap := map[string]string{field: "10"} + + hsetResult, err := client.HSet(key, fieldValueMap) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(1), hsetResult.Value()) + + hincrByResult, hincrByErr := client.HIncrBy(key, field, 1) + assert.Nil(suite.T(), hincrByErr) + assert.Equal(suite.T(), float64(11), hincrByResult.Value()) + }) +} + +func (suite *GlideTestSuite) TestHIncrBy_WithNonExistingField() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + field := uuid.NewString() + field2 := uuid.NewString() + fieldValueMap := map[string]string{field2: "1"} + + hsetResult, err := client.HSet(key, fieldValueMap) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(1), hsetResult.Value()) + + hincrByResult, hincrByErr := client.HIncrBy(key, field, 2) + assert.Nil(suite.T(), hincrByErr) + assert.Equal(suite.T(), float64(2), hincrByResult.Value()) + }) +} + func (suite *GlideTestSuite) TestLPushLPop_WithExistingKey() { suite.runWithDefaultClients(func(client api.BaseClient) { list := []string{"value4", "value3", "value2", "value1"} From 33c95a84113a4411341a18e6d0f234347f92cb8d Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Fri, 20 Dec 2024 11:15:42 -0800 Subject: [PATCH 08/17] Go: Update change log file for HINCRBY command Signed-off-by: Prateek Kumar --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c7c5ed20e..f19843a26f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ #### Changes - +* Go: Add HINCRBY command ([#2847](https://github.com/valkey-io/valkey-glide/pull/2847)) * Go: Add SUNIONSTORE command ([#2805](https://github.com/valkey-io/valkey-glide/pull/2805) * Go: Add SUNION ([#2787](https://github.com/valkey-io/valkey-glide/pull/2787) * Java: bump `netty` version ([#2795](https://github.com/valkey-io/valkey-glide/pull/2795)) From 8a0f652980388f6937993d162d0e689053d81167 Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Fri, 20 Dec 2024 11:22:31 -0800 Subject: [PATCH 09/17] Go: Add HINCRBY command - update tests Signed-off-by: Prateek Kumar --- go/integTest/shared_commands_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 8c65edf39a..34bca5a1b9 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -1052,7 +1052,7 @@ func (suite *GlideTestSuite) TestHIncrBy_WithExistingField() { hincrByResult, hincrByErr := client.HIncrBy(key, field, 1) assert.Nil(suite.T(), hincrByErr) - assert.Equal(suite.T(), float64(11), hincrByResult.Value()) + assert.Equal(suite.T(), int64(11), hincrByResult.Value()) }) } @@ -1069,7 +1069,7 @@ func (suite *GlideTestSuite) TestHIncrBy_WithNonExistingField() { hincrByResult, hincrByErr := client.HIncrBy(key, field, 2) assert.Nil(suite.T(), hincrByErr) - assert.Equal(suite.T(), float64(2), hincrByResult.Value()) + assert.Equal(suite.T(), int64(2), hincrByResult.Value()) }) } From 33cd0c72df3d2063b093cb3f799125bc5d1dbaf3 Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Fri, 20 Dec 2024 11:39:04 -0800 Subject: [PATCH 10/17] Go: Add HINCRBYFLOAT - update test cases Signed-off-by: Prateek Kumar --- go/api/commands.go | 1 + go/integTest/shared_commands_test.go | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/go/api/commands.go b/go/api/commands.go index 8eff3fc668..3a29b7a366 100644 --- a/go/api/commands.go +++ b/go/api/commands.go @@ -718,6 +718,7 @@ type HashCommands interface { // // Return value: // The Result[float64] value of `field` in the hash stored at `key` after the increment. + // // For example: // hsetResult, err := client.HSet("key", map[string]string{"field": "10"}) // hincrByFloatResult, err := client.HIncrByFloat("key", "field", 1.5) diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 233eeeed1b..197dcdd614 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -1050,8 +1050,8 @@ func (suite *GlideTestSuite) TestHIncrByFloat_WithExistingField() { assert.Nil(suite.T(), err) assert.Equal(suite.T(), int64(1), hsetResult.Value()) - hincrByFloatResult, hincrByFloarErr := client.HIncrByFloat(key, field, 1.5) - assert.Nil(suite.T(), hincrByFloarErr) + hincrByFloatResult, hincrByFloatErr := client.HIncrByFloat(key, field, 1.5) + assert.Nil(suite.T(), hincrByFloatErr) assert.Equal(suite.T(), float64(11.5), hincrByFloatResult.Value()) }) } @@ -1067,8 +1067,8 @@ func (suite *GlideTestSuite) TestHIncrByFloat_WithNonExistingField() { assert.Nil(suite.T(), err) assert.Equal(suite.T(), int64(1), hsetResult.Value()) - hincrByFloatResult, hincrByFloarErr := client.HIncrByFloat(key, field, 1.5) - assert.Nil(suite.T(), hincrByFloarErr) + hincrByFloatResult, hincrByFloatErr := client.HIncrByFloat(key, field, 1.5) + assert.Nil(suite.T(), hincrByFloatErr) assert.Equal(suite.T(), float64(1.5), hincrByFloatResult.Value()) }) } From b90ac0ba5eedeada5c5f0a62121b55d51ce9747b Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Fri, 20 Dec 2024 11:44:58 -0800 Subject: [PATCH 11/17] Go: HINCRBYFLOAT - update change log Signed-off-by: Prateek Kumar --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 427d299213..4a15974a40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ #### Changes -* GO: Add HIncrByFloat command ([#2846](https://github.com/valkey-io/valkey-glide/pull/2846)) +* GO: Add HINCRBYFLOAT command ([#2846](https://github.com/valkey-io/valkey-glide/pull/2846)) * Go: Add SUNIONSTORE command ([#2805](https://github.com/valkey-io/valkey-glide/pull/2805)) * Go: Add SUNION ([#2787](https://github.com/valkey-io/valkey-glide/pull/2787)) * Java: bump `netty` version ([#2795](https://github.com/valkey-io/valkey-glide/pull/2795)) From fa620ca24533e3c51d740bf8c8de70e440244139 Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Fri, 20 Dec 2024 11:46:25 -0800 Subject: [PATCH 12/17] Go: HINCRBYFLOAT - update change log Signed-off-by: Prateek Kumar --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a15974a40..67d0d5d788 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ #### Changes -* GO: Add HINCRBYFLOAT command ([#2846](https://github.com/valkey-io/valkey-glide/pull/2846)) +* Go: Add HINCRBYFLOAT command ([#2846](https://github.com/valkey-io/valkey-glide/pull/2846)) * Go: Add SUNIONSTORE command ([#2805](https://github.com/valkey-io/valkey-glide/pull/2805)) * Go: Add SUNION ([#2787](https://github.com/valkey-io/valkey-glide/pull/2787)) * Java: bump `netty` version ([#2795](https://github.com/valkey-io/valkey-glide/pull/2795)) From 8c643290569570ab41071197d0be834ba4890f80 Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Fri, 20 Dec 2024 13:06:12 -0800 Subject: [PATCH 13/17] Go: HINCRBYFLOAT - fix review comments Signed-off-by: Prateek Kumar --- go/api/commands.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go/api/commands.go b/go/api/commands.go index 3a29b7a366..c369fc4a72 100644 --- a/go/api/commands.go +++ b/go/api/commands.go @@ -705,7 +705,7 @@ type HashCommands interface { // [valkey.io]: https://valkey.io/commands/hstrlen/ HStrLen(key string, field string) (Result[int64], error) - // Increments the string representing a floating point number stored at `field`` in the hash stored at `key` by increment. + // Increments the string representing a floating point number stored at `field` in the hash stored at `key` by increment. // By using a negative increment value, the value stored at field in the hash stored at `key` is decremented. // If `field` or `key` does not exist, it is set to 0 before performing the operation. // @@ -719,7 +719,7 @@ type HashCommands interface { // Return value: // The Result[float64] value of `field` in the hash stored at `key` after the increment. // - // For example: + // Example: // hsetResult, err := client.HSet("key", map[string]string{"field": "10"}) // hincrByFloatResult, err := client.HIncrByFloat("key", "field", 1.5) // // hincrByFloatResult.Value(): 11.5 From 9919ed05b719762869e2ad4de499e819f86d8d54 Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Fri, 20 Dec 2024 13:16:13 -0800 Subject: [PATCH 14/17] Go: HINCRBY - update review comments Signed-off-by: Prateek Kumar --- go/api/commands.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/api/commands.go b/go/api/commands.go index 6f28299610..d2708e226c 100644 --- a/go/api/commands.go +++ b/go/api/commands.go @@ -719,7 +719,7 @@ type HashCommands interface { // Return value: // The Result[int64] value of `field` in the hash stored at `key` after the increment. // - // For example: + // Example: // hsetResult, err := client.HSet("key", map[string]string{"field": "10"}) // hincrByResult, err := client.HIncrBy("key", "field", 1) // // hincrByResult.Value(): 11 From e4db1e9fd105eb564c64c56952a7ff63d1a1d2f4 Mon Sep 17 00:00:00 2001 From: Joseph Brinkman Date: Fri, 20 Dec 2024 16:37:00 -0500 Subject: [PATCH 15/17] GO: Add ZIncrBy Command (#2830) * Go: Add ZIncrBy command Signed-off-by: jbrinkman * reformat document to resolve linting error Signed-off-by: jbrinkman * update test with additional type check Signed-off-by: jbrinkman * add example Signed-off-by: jbrinkman * fix merge error Signed-off-by: jbrinkman --------- Signed-off-by: jbrinkman Signed-off-by: Joseph Brinkman --- CHANGELOG.md | 1 + go/api/base_client.go | 10 ++++++++- go/api/sorted_set_commands.go | 22 ++++++++++++++++++++ go/integTest/shared_commands_test.go | 31 ++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c7c5ed20e..462f97bb7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Java: Bump protobuf (protoc) version ([#2796](https://github.com/valkey-io/valkey-glide/pull/2796), [#2800](https://github.com/valkey-io/valkey-glide/pull/2800)) * Go: Add `SInterStore` ([#2779](https://github.com/valkey-io/valkey-glide/issues/2779)) * Node: Remove native package references for MacOs x64 architecture ([#2799](https://github.com/valkey-io/valkey-glide/issues/2799)) +* Go: Add `ZIncrBy` command ([#2830](https://github.com/valkey-io/valkey-glide/pull/2830)) * Go: Add `SScan` and `SMove` ([#2789](https://github.com/valkey-io/valkey-glide/issues/2789)) * Go: Add `ZADD` ([#2813](https://github.com/valkey-io/valkey-glide/issues/2813)) diff --git a/go/api/base_client.go b/go/api/base_client.go index 0e4aea3995..a05ffa2f30 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -48,7 +48,6 @@ func successCallback(channelPtr unsafe.Pointer, cResponse *C.struct_CommandRespo resultChannel <- payload{value: response, error: nil} } -// //export failureCallback func failureCallback(channelPtr unsafe.Pointer, cErrorMessage *C.char, cErrorType C.RequestErrorType) { resultChannel := *(*chan payload)(channelPtr) @@ -1290,3 +1289,12 @@ func (client *baseClient) ZAddIncrWithOptions( return client.zAddIncrBase(key, incrOpts) } + +func (client *baseClient) ZIncrBy(key string, increment float64, member string) (Result[float64], error) { + result, err := client.executeCommand(C.ZIncrBy, []string{key, utils.FloatToString(increment), member}) + if err != nil { + return CreateNilFloat64Result(), err + } + + return handleDoubleResponse(result) +} diff --git a/go/api/sorted_set_commands.go b/go/api/sorted_set_commands.go index 8eecf70120..02a9697e6d 100644 --- a/go/api/sorted_set_commands.go +++ b/go/api/sorted_set_commands.go @@ -88,4 +88,26 @@ type SortedSetCommands interface { // // [valkey.io]: https://valkey.io/commands/zadd/ ZAddIncrWithOptions(key string, member string, increment float64, opts *options.ZAddOptions) (Result[float64], error) + + // Increments the score of member in the sorted set stored at key by increment. + // If member does not exist in the sorted set, it is added with increment as its score. + // If key does not exist, a new sorted set with the specified member as its sole member + // is created. + // + // see [valkey.io] for details. + // + // Parameters: + // key - The key of the sorted set. + // increment - The score increment. + // member - A member of the sorted set. + // + // Return value: + // The new score of member. + // + // Example: + // res, err := client.ZIncrBy("myzset", 2.0, "one") + // fmt.Println(res.Value()) // Output: 2.0 + // + // [valkey.io]: https://valkey.io/commands/zincrby/ + ZIncrBy(key string, increment float64, member string) (Result[float64], error) } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 972dfd8593..35db759367 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -3873,3 +3873,34 @@ func (suite *GlideTestSuite) TestZAddAndZAddIncr() { assert.True(suite.T(), resIncr.IsNil()) }) } + +func (suite *GlideTestSuite) TestZincrBy() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := uuid.New().String() + key2 := uuid.New().String() + + // key does not exist + res1, err := client.ZIncrBy(key1, 2.5, "value1") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), 2.5, res1.Value()) + + // key exists, but value doesn't + res2, err := client.ZIncrBy(key1, -3.3, "value2") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), -3.3, res2.Value()) + + // updating existing value in existing key + res3, err := client.ZIncrBy(key1, 1.0, "value1") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), 3.5, res3.Value()) + + // Key exists, but it is not a sorted set + res4, err := client.SAdd(key2, []string{"one", "two"}) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res4.Value()) + + _, err = client.ZIncrBy(key2, 0.5, "_") + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} From 91e2f253642c68aa15ec0cda7c30a88fec189849 Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Fri, 20 Dec 2024 13:52:07 -0800 Subject: [PATCH 16/17] Go: HINCRBY - fix formatting Signed-off-by: Prateek Kumar --- go/api/commands.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/go/api/commands.go b/go/api/commands.go index 890642e7e4..4db24e3ef2 100644 --- a/go/api/commands.go +++ b/go/api/commands.go @@ -727,18 +727,18 @@ type HashCommands interface { // [valkey.io]: https://valkey.io/commands/hincrby/ HIncrBy(key string, field string, increment int64) (Result[int64], error) - // Increments the string representing a floating point number stored at `field` in the hash stored at `key` by increment. + // Increments the string representing a floating point number stored at `field` in the hash stored at `key` by increment. // By using a negative increment value, the value stored at `field` in the hash stored at `key` is decremented. - // If `field` or `key` does not exist, it is set to 0 before performing the operation. - // - // See [valkey.io] for details. + // If `field` or `key` does not exist, it is set to 0 before performing the operation. + // + // See [valkey.io] for details. // // Parameters: // key - The key of the hash. // field - The field in the hash stored at `key` to increment its value. // increment - The amount to increment. // - // Return value: + // Return value: // The Result[float64] value of `field` in the hash stored at `key` after the increment. // // Example: From e61b67c86d3df74866131931c2a00a2ca5bab5e2 Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Fri, 20 Dec 2024 15:22:37 -0800 Subject: [PATCH 17/17] Go: HINCRBY - review comments fixed Signed-off-by: Prateek Kumar --- go/api/commands.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go/api/commands.go b/go/api/commands.go index 4db24e3ef2..8f62892024 100644 --- a/go/api/commands.go +++ b/go/api/commands.go @@ -720,7 +720,7 @@ type HashCommands interface { // The Result[int64] value of `field` in the hash stored at `key` after the increment. // // Example: - // hsetResult, err := client.HSet("key", map[string]string{"field": "10"}) + // _, err := client.HSet("key", map[string]string{"field": "10"}) // hincrByResult, err := client.HIncrBy("key", "field", 1) // // hincrByResult.Value(): 11 // @@ -742,7 +742,7 @@ type HashCommands interface { // The Result[float64] value of `field` in the hash stored at `key` after the increment. // // Example: - // hsetResult, err := client.HSet("key", map[string]string{"field": "10"}) + // _, err := client.HSet("key", map[string]string{"field": "10"}) // hincrByFloatResult, err := client.HIncrByFloat("key", "field", 1.5) // // hincrByFloatResult.Value(): 11.5 //