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