diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c7585a2f8..2603b0db20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes + * Java, Node, Python: Add transaction commands for JSON module ([#2862](https://github.com/valkey-io/valkey-glide/pull/2862)) * Go: Add HINCRBY command ([#2847](https://github.com/valkey-io/valkey-glide/pull/2847)) * Go: Add HINCRBYFLOAT command ([#2846](https://github.com/valkey-io/valkey-glide/pull/2846)) @@ -13,6 +14,7 @@ * Go: Add `ZPopMin` and `ZPopMax` ([#2850](https://github.com/valkey-io/valkey-glide/pull/2850)) * Java: Add binary version of `ZRANK WITHSCORE` ([#2896](https://github.com/valkey-io/valkey-glide/pull/2896)) * Go: Add `ZCARD` ([#2838](https://github.com/valkey-io/valkey-glide/pull/2838)) +* Go: Add `BZPopMin` ([#2849](https://github.com/valkey-io/valkey-glide/pull/2849)) #### Breaking Changes diff --git a/go/api/base_client.go b/go/api/base_client.go index 99cab3608d..81f965d537 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -1441,3 +1441,12 @@ func (client *baseClient) ZCard(key string) (Result[int64], error) { return handleLongResponse(result) } + +func (client *baseClient) BZPopMin(keys []string, timeoutSecs float64) (Result[KeyWithMemberAndScore], error) { + result, err := client.executeCommand(C.BZPopMin, append(keys, utils.FloatToString(timeoutSecs))) + if err != nil { + return CreateNilKeyWithMemberAndScoreResult(), err + } + + return handleKeyWithMemberAndScoreResponse(result) +} diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index dd4c2d1f24..4a5056c0c6 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -397,6 +397,30 @@ func handleStringSetResponse(response *C.struct_CommandResponse) (map[Result[str return slice, nil } +func handleKeyWithMemberAndScoreResponse(response *C.struct_CommandResponse) (Result[KeyWithMemberAndScore], error) { + defer C.free_command_response(response) + + if response == nil || response.response_type == uint32(C.Null) { + return CreateNilKeyWithMemberAndScoreResult(), nil + } + + typeErr := checkResponseType(response, C.Array, true) + if typeErr != nil { + return CreateNilKeyWithMemberAndScoreResult(), typeErr + } + + slice, err := parseArray(response) + if err != nil { + return CreateNilKeyWithMemberAndScoreResult(), err + } + + arr := slice.([]interface{}) + key := arr[0].(string) + member := arr[1].(string) + score := arr[2].(float64) + return CreateKeyWithMemberAndScoreResult(KeyWithMemberAndScore{key, member, score}), nil +} + func handleScanResponse( response *C.struct_CommandResponse, ) (Result[string], []Result[string], error) { diff --git a/go/api/response_types.go b/go/api/response_types.go index 3146032b04..6172c4ff2b 100644 --- a/go/api/response_types.go +++ b/go/api/response_types.go @@ -7,6 +7,14 @@ type Result[T any] struct { isNil bool } +// KeyWithMemberAndScore is used by BZPOPMIN/BZPOPMAX, which return an object consisting of the key of the sorted set that was +// popped, the popped member, and its score. +type KeyWithMemberAndScore struct { + Key string + Member string + Score float64 +} + func (result Result[T]) IsNil() bool { return result.isNil } @@ -47,6 +55,14 @@ func CreateNilBoolResult() Result[bool] { return Result[bool]{val: false, isNil: true} } +func CreateKeyWithMemberAndScoreResult(kmsVal KeyWithMemberAndScore) Result[KeyWithMemberAndScore] { + return Result[KeyWithMemberAndScore]{val: kmsVal, isNil: false} +} + +func CreateNilKeyWithMemberAndScoreResult() Result[KeyWithMemberAndScore] { + return Result[KeyWithMemberAndScore]{val: KeyWithMemberAndScore{"", "", 0.0}, isNil: true} +} + // Enum to distinguish value types stored in `ClusterValue` type ValueType int diff --git a/go/api/sorted_set_commands.go b/go/api/sorted_set_commands.go index 4159acabe1..4b63b70091 100644 --- a/go/api/sorted_set_commands.go +++ b/go/api/sorted_set_commands.go @@ -232,4 +232,34 @@ type SortedSetCommands interface { // // [valkey.io]: https://valkey.io/commands/zcard/ ZCard(key string) (Result[int64], error) + + // Blocks the connection until it removes and returns a member with the lowest score from the + // first non-empty sorted set, with the given `keys` being checked in the order they + // are provided. + // `BZPOPMIN` is the blocking variant of `ZPOPMIN`. + // + // Note: + // - When in cluster mode, all `keys` must map to the same hash slot. + // - `BZPOPMIN` is a client blocking command, see [Blocking Commands] for more details and best practices. + // + // See [valkey.io] for more details. + // + // Parameters: + // keys - The keys of the sorted sets. + // timeout - The number of seconds to wait for a blocking operation to complete. A value of + // `0` will block indefinitely. + // + // Return value: + // A `KeyWithMemberAndScore` struct containing the key where the member was popped out, the member + // itself, and the member score. If no member could be popped and the `timeout` expired, returns `nil`. + // + // example + // zaddResult1, err := client.ZAdd(key1, map[string]float64{"a": 1.0, "b": 1.5}) + // zaddResult2, err := client.ZAdd(key2, map[string]float64{"c": 2.0}) + // result, err := client.BZPopMin([]string{key1, key2}, float64(.5)) + // fmt.Println(res.Value()) // Output: {key: key1 member:a, score:1} + // + // [valkey.io]: https://valkey.io/commands/bzpopmin/ + // [blocking commands]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands + BZPopMin(keys []string, timeoutSecs float64) (Result[KeyWithMemberAndScore], error) } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index eefd38e9b0..9abe3714cb 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -4138,6 +4138,50 @@ func (suite *GlideTestSuite) TestZincrBy() { }) } +func (suite *GlideTestSuite) TestBZPopMin() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := "{zset}-1-" + uuid.NewString() + key2 := "{zset}-2-" + uuid.NewString() + key3 := "{zset}-2-" + uuid.NewString() + + // Add elements to key1 + zaddResult1, err := client.ZAdd(key1, map[string]float64{"a": 1.0, "b": 1.5}) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), zaddResult1.Value()) + + // Add elements to key2 + zaddResult2, err := client.ZAdd(key2, map[string]float64{"c": 2.0}) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(1), zaddResult2.Value()) + + // Pop minimum element from key1 and key2 + bzpopminResult1, err := client.BZPopMin([]string{key1, key2}, float64(.5)) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), api.KeyWithMemberAndScore{Key: key1, Member: "a", Score: 1.0}, bzpopminResult1.Value()) + + // Attempt to pop from non-existent key3 + bzpopminResult2, err := client.BZPopMin([]string{key3}, float64(1)) + assert.Nil(suite.T(), err) + assert.True(suite.T(), bzpopminResult2.IsNil()) + + // Pop minimum element from key2 + bzpopminResult3, err := client.BZPopMin([]string{key3, key2}, float64(.5)) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), api.KeyWithMemberAndScore{Key: key2, Member: "c", Score: 2.0}, bzpopminResult3.Value()) + + // Set key3 to a non-sorted set value + setResult, err := client.Set(key3, "value") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "OK", setResult.Value()) + + // Attempt to pop from key3 which is not a sorted set + _, err = client.BZPopMin([]string{key3}, float64(.5)) + if assert.Error(suite.T(), err) { + assert.IsType(suite.T(), &api.RequestError{}, err) + } + }) +} + func (suite *GlideTestSuite) TestZPopMin() { suite.runWithDefaultClients(func(client api.BaseClient) { key1 := uuid.New().String()