From 34f036956c94f0bb1eb306024f5f803155e44302 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Mon, 20 Jan 2025 18:15:34 -0800 Subject: [PATCH] Go: `ZRANDMEMBER`. Signed-off-by: Yury-Fridlyand --- go/api/base_client.go | 82 ++++++++++++++++++++++++++++ go/api/options/constants.go | 11 ++-- go/api/response_handlers.go | 21 +++++++ go/api/response_types.go | 6 ++ go/api/sorted_set_commands.go | 6 ++ go/integTest/shared_commands_test.go | 62 +++++++++++++++++++++ 6 files changed, 183 insertions(+), 5 deletions(-) diff --git a/go/api/base_client.go b/go/api/base_client.go index c0a79dfdac..739a8fd447 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -2731,6 +2731,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 logarithmic access frequency counter of a Valkey object stored at key. // // Parameters: diff --git a/go/api/options/constants.go b/go/api/options/constants.go index d2d4b594db..35c2efe661 100644 --- a/go/api/options/constants.go +++ b/go/api/options/constants.go @@ -3,9 +3,10 @@ package options const ( - CountKeyword string = "COUNT" // Valkey API keyword used to extract specific number of matching indices from a list. - 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. - NoScores string = "NOSCORES" // Valkey API keyword for the no scores option for zscan command. + CountKeyword string = "COUNT" // Valkey API keyword used to extract specific number of matching indices from a list. + 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. ) diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index dd7fbe2712..14fde36219 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -484,6 +484,27 @@ func handleKeyWithMemberAndScoreResponse(response *C.struct_CommandResponse) (Re return CreateKeyWithMemberAndScoreResult(KeyWithMemberAndScore{key, member, score}), 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 84de6aed7f..14921f6e07 100644 --- a/go/api/response_types.go +++ b/go/api/response_types.go @@ -23,6 +23,12 @@ type KeyWithMemberAndScore struct { Score float64 } +// 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 +} + // Response type of [XAutoClaim] command. type XAutoClaimResponse struct { NextEntry string diff --git a/go/api/sorted_set_commands.go b/go/api/sorted_set_commands.go index 62d06091bd..0128929f7d 100644 --- a/go/api/sorted_set_commands.go +++ b/go/api/sorted_set_commands.go @@ -391,4 +391,10 @@ type SortedSetCommands interface { ZRemRangeByRank(key string, start int64, stop int64) (int64, error) 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) } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index cba42181b0..47bbf1a02f 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -6221,6 +6221,68 @@ func (suite *GlideTestSuite) TestZRemRangeByScore() { }) } +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(), &api.RequestError{}, err) + _, err = client.ZRandMemberWithCount(key2, 2) + assert.IsType(suite.T(), &api.RequestError{}, err) + _, err = client.ZRandMemberWithCountWithScores(key2, 2) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + func (suite *GlideTestSuite) TestObjectIdleTime() { suite.runWithDefaultClients(func(client api.BaseClient) { defaultClient := suite.defaultClient()