Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Go: ZRANDMEMBER. #2983

Merged
merged 5 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 84 additions & 2 deletions go/api/base_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
//
Expand Down
1 change: 1 addition & 0 deletions go/api/options/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the WITHSCORE one even an option? We tested that with some commands passing WITHSCORE in the place where the doc asks for WITHSCORES would work, but is any command explicitly asking for WITHSCORE? if not we should probably remove
cc @prateek-kumar-improving

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably we could keep it there with lowercase (aka private)

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`.
)
Expand Down
21 changes: 21 additions & 0 deletions go/api/response_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions go/api/response_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions go/api/sorted_set_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
62 changes: 62 additions & 0 deletions go/integTest/shared_commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading