From 324f9e4a749032f432170c69f60365523a912350 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 31 Jan 2025 10:48:32 -0800 Subject: [PATCH] Go: `ZRANDMEMBER`. (#2983) * Go: `ZRANDMEMBER`. Signed-off-by: Yury-Fridlyand --- go/api/base_client.go | 86 +++++++++++++++++++++++++++- go/api/options/constants.go | 1 + go/api/response_handlers.go | 21 +++++++ go/api/response_types.go | 1 + go/api/sorted_set_commands.go | 6 ++ go/integTest/shared_commands_test.go | 62 ++++++++++++++++++++ 6 files changed, 175 insertions(+), 2 deletions(-) diff --git a/go/api/base_client.go b/go/api/base_client.go index 69070f08f6..99772946d6 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -5877,6 +5877,88 @@ func (client *baseClient) ZRemRangeByScore(key string, rangeQuery options.RangeB return handleIntResponse(result) } +// Returns a random member from the sorted set stored at `key`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the sorted set. +// +// Return value: +// +// A string representing a random member from the sorted set. +// If the sorted set does not exist or is empty, the response will be `nil`. +// +// Example: +// +// member, err := client.ZRandMember("key1") +// +// [valkey.io]: https://valkey.io/commands/zrandmember/ +func (client *baseClient) ZRandMember(key string) (Result[string], error) { + result, err := client.executeCommand(C.ZRandMember, []string{key}) + if err != nil { + return CreateNilStringResult(), err + } + return handleStringOrNilResponse(result) +} + +// Returns a random member from the sorted set stored at `key`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the sorted set. +// count - The number of field names to return. +// If `count` is positive, returns unique elements. If negative, allows for duplicates. +// +// Return value: +// +// An array of members from the sorted set. +// If the sorted set does not exist or is empty, the response will be an empty array. +// +// Example: +// +// members, err := client.ZRandMemberWithCount("key1", -5) +// +// [valkey.io]: https://valkey.io/commands/zrandmember/ +func (client *baseClient) ZRandMemberWithCount(key string, count int64) ([]string, error) { + result, err := client.executeCommand(C.ZRandMember, []string{key, utils.IntToString(count)}) + if err != nil { + return nil, err + } + return handleStringArrayResponse(result) +} + +// Returns a random member from the sorted set stored at `key`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the sorted set. +// count - The number of field names to return. +// If `count` is positive, returns unique elements. If negative, allows for duplicates. +// +// Return value: +// +// An array of `MemberAndScore` objects, which store member names and their respective scores. +// If the sorted set does not exist or is empty, the response will be an empty array. +// +// Example: +// +// membersAndScores, err := client.ZRandMemberWithCountWithScores("key1", 5) +// +// [valkey.io]: https://valkey.io/commands/zrandmember/ +func (client *baseClient) ZRandMemberWithCountWithScores(key string, count int64) ([]MemberAndScore, error) { + result, err := client.executeCommand(C.ZRandMember, []string{key, utils.IntToString(count), options.WithScores}) + if err != nil { + return nil, err + } + return handleMemberAndScoreArrayResponse(result) +} + // Returns the scores associated with the specified `members` in the sorted set stored at `key`. // // Since: @@ -5890,8 +5972,8 @@ func (client *baseClient) ZRemRangeByScore(key string, rangeQuery options.RangeB // // Return value: // -// An array of scores corresponding to `members`. -// If a member does not exist in the sorted set, the corresponding value in the list will be `nil`. +// An array of scores corresponding to `members`. +// If a member does not exist in the sorted set, the corresponding value in the list will be `nil`. // // Example: // diff --git a/go/api/options/constants.go b/go/api/options/constants.go index 45913c5b5c..e728968633 100644 --- a/go/api/options/constants.go +++ b/go/api/options/constants.go @@ -7,6 +7,7 @@ const ( MatchKeyword string = "MATCH" // Valkey API keyword used to indicate the match filter. NoValue string = "NOVALUE" // Valkey API keyword for the no value option for hcsan command. WithScore string = "WITHSCORE" // Valkey API keyword for the with score option for zrank and zrevrank commands. + WithScores string = "WITHSCORES" // Valkey API keyword for ZRandMember command to return scores along with members. NoScores string = "NOSCORES" // Valkey API keyword for the no scores option for zscan command. WithValues string = "WITHVALUES" // Valkey API keyword to query hash values along their names in `HRANDFIELD`. ) diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index 3a3f3fe52b..8bdd51f7c2 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -633,6 +633,27 @@ func handleKeyWithArrayOfMembersAndScoresResponse( return CreateKeyWithArrayOfMembersAndScoresResult(KeyWithArrayOfMembersAndScores{key, memberAndScoreArray}), nil } +func handleMemberAndScoreArrayResponse(response *C.struct_CommandResponse) ([]MemberAndScore, error) { + defer C.free_command_response(response) + + typeErr := checkResponseType(response, C.Array, false) + if typeErr != nil { + return nil, typeErr + } + + slice, err := parseArray(response) + if err != nil { + return nil, err + } + + var result []MemberAndScore + for _, arr := range slice.([]interface{}) { + pair := arr.([]interface{}) + result = append(result, MemberAndScore{pair[0].(string), pair[1].(float64)}) + } + return result, nil +} + func handleScanResponse(response *C.struct_CommandResponse) (string, []string, error) { defer C.free_command_response(response) diff --git a/go/api/response_types.go b/go/api/response_types.go index aee9e2a3d4..db548c402f 100644 --- a/go/api/response_types.go +++ b/go/api/response_types.go @@ -29,6 +29,7 @@ type KeyWithArrayOfMembersAndScores struct { MembersAndScores []MemberAndScore } +// MemberAndScore is used by ZRANDMEMBER, which return an object consisting of the sorted set member, and its score. type MemberAndScore struct { Member string Score float64 diff --git a/go/api/sorted_set_commands.go b/go/api/sorted_set_commands.go index cbad4edb11..1331a09e04 100644 --- a/go/api/sorted_set_commands.go +++ b/go/api/sorted_set_commands.go @@ -71,5 +71,11 @@ type SortedSetCommands interface { ZRemRangeByScore(key string, rangeQuery options.RangeByScore) (int64, error) + ZRandMember(key string) (Result[string], error) + + ZRandMemberWithCount(key string, count int64) ([]string, error) + + ZRandMemberWithCountWithScores(key string, count int64) ([]MemberAndScore, error) + ZMScore(key string, members []string) ([]Result[float64], error) } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 4c7e3c5b74..71fbcb8c93 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -6496,6 +6496,68 @@ func (suite *GlideTestSuite) TestZMScore() { }) } +func (suite *GlideTestSuite) TestZRandMember() { + suite.runWithDefaultClients(func(client api.BaseClient) { + t := suite.T() + key1 := uuid.NewString() + key2 := uuid.NewString() + members := []string{"one", "two"} + + zadd, err := client.ZAdd(key1, map[string]float64{"one": 1.0, "two": 2.0}) + assert.NoError(t, err) + assert.Equal(t, int64(2), zadd) + + randomMember, err := client.ZRandMember(key1) + assert.NoError(t, err) + assert.Contains(t, members, randomMember.Value()) + + // unique values are expected as count is positive + randomMembers, err := client.ZRandMemberWithCount(key1, 4) + assert.NoError(t, err) + assert.ElementsMatch(t, members, randomMembers) + + membersAndScores, err := client.ZRandMemberWithCountWithScores(key1, 4) + expectedMembersAndScores := []api.MemberAndScore{{Member: "one", Score: 1}, {Member: "two", Score: 2}} + assert.NoError(t, err) + assert.ElementsMatch(t, expectedMembersAndScores, membersAndScores) + + // Duplicate values are expected as count is negative + randomMembers, err = client.ZRandMemberWithCount(key1, -4) + assert.NoError(t, err) + assert.Len(t, randomMembers, 4) + for _, member := range randomMembers { + assert.Contains(t, members, member) + } + + membersAndScores, err = client.ZRandMemberWithCountWithScores(key1, -4) + assert.NoError(t, err) + assert.Len(t, membersAndScores, 4) + for _, memberAndScore := range membersAndScores { + assert.Contains(t, expectedMembersAndScores, memberAndScore) + } + + // non existing key should return null or empty array + randomMember, err = client.ZRandMember(key2) + assert.NoError(t, err) + assert.True(t, randomMember.IsNil()) + randomMembers, err = client.ZRandMemberWithCount(key2, -4) + assert.NoError(t, err) + assert.Len(t, randomMembers, 0) + membersAndScores, err = client.ZRandMemberWithCountWithScores(key2, -4) + assert.NoError(t, err) + assert.Len(t, membersAndScores, 0) + + // Key exists, but is not a set + suite.verifyOK(client.Set(key2, "ZRandMember")) + _, err = client.ZRandMember(key2) + assert.IsType(suite.T(), &errors.RequestError{}, err) + _, err = client.ZRandMemberWithCount(key2, 2) + assert.IsType(suite.T(), &errors.RequestError{}, err) + _, err = client.ZRandMemberWithCountWithScores(key2, 2) + assert.IsType(suite.T(), &errors.RequestError{}, err) + }) +} + func (suite *GlideTestSuite) TestObjectIdleTime() { suite.runWithDefaultClients(func(client api.BaseClient) { defaultClient := suite.defaultClient()