From 21c110bc11878e3895b1842150e2b8eda1d778ec Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 17 Jan 2025 16:38:30 -0800 Subject: [PATCH] Go: `HRANDFIELD`. Signed-off-by: Yury-Fridlyand --- go/api/base_client.go | 106 +++++++++++++++++++++++++++ go/api/hash_commands.go | 6 ++ go/api/response_handlers.go | 27 +++++++ go/integTest/shared_commands_test.go | 71 ++++++++++++++++++ 4 files changed, 210 insertions(+) diff --git a/go/api/base_client.go b/go/api/base_client.go index 9c08a9fc64..2ad70f029d 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -509,6 +509,112 @@ func (client *baseClient) HScanWithOptions( return handleScanResponse(result) } +// Returns a random field name from the hash value stored at `key`. +// +// Since: +// +// Valkey 6.2.0 and above. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the hash. +// +// Return value: +// +// A random field name from the hash stored at `key`, or `nil` when +// the key does not exist. +// +// Example: +// +// field, err := client.HRandField("my_hash") +// +// [valkey.io]: https://valkey.io/commands/hexists/ +func (client *baseClient) HRandField(key string) (Result[string], error) { + result, err := client.executeCommand(C.HRandField, []string{key}) + if err != nil { + return CreateNilStringResult(), err + } + return handleStringOrNilResponse(result) +} + +// Retrieves up to `count` random field names from the hash value stored at `key`. +// +// Since: +// +// Valkey 6.2.0 and above. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the hash. +// 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 random field names from the hash stored at `key`, +// or an empty array when the key does not exist. +// +// Example: +// +// fields, err := client.HRandFieldWithCount("my_hash", -5) +// +// [valkey.io]: https://valkey.io/commands/hexists/ +func (client *baseClient) HRandFieldWithCount(key string, count int64) ([]string, error) { + result, err := client.executeCommand(C.HRandField, []string{key, utils.IntToString(count)}) + if err != nil { + return nil, err + } + // TODO remove that after merging with https://github.com/valkey-io/valkey-glide/pull/2965 + data, err := handleStringArrayResponse(result) + var res []string + for _, val := range data { + res = append(res, val.Value()) + } + return res, err +} + +// Retrieves up to `count` random field names along with their values from the hash +// value stored at `key`. +// +// Since: +// +// Valkey 6.2.0 and above. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the hash. +// count - The number of field names to return. +// If `count` is positive, returns unique elements. If negative, allows for duplicates. +// +// Return value: +// +// A 2D `array` of `[field, value]` arrays, where `field` is a random +// field name from the hash and `value` is the associated value of the field name. +// If the hash does not exist or is empty, the response will be an empty array. +// +// Example: +// +// fieldsAndValues, err := client.HRandFieldWithCountWithValues("my_hash", -5) +// for _, pair := range fieldsAndValues { +// field := pair[0] +// value := pair[1] +// } +// +// [valkey.io]: https://valkey.io/commands/hexists/ +func (client *baseClient) HRandFieldWithCountWithValues(key string, count int64) ([][]string, error) { + result, err := client.executeCommand(C.HRandField, []string{key, utils.IntToString(count), "WITHVALUES"}) + if err != nil { + return nil, err + } + return handle2DStringArrayResponse(result) +} + func (client *baseClient) LPush(key string, elements []string) (int64, error) { result, err := client.executeCommand(C.LPush, append([]string{key}, elements...)) if err != nil { diff --git a/go/api/hash_commands.go b/go/api/hash_commands.go index c5fb068a2a..0d1e4514f8 100644 --- a/go/api/hash_commands.go +++ b/go/api/hash_commands.go @@ -343,4 +343,10 @@ type HashCommands interface { // // [valkey.io]: https://valkey.io/commands/hscan/ HScanWithOptions(key string, cursor string, options *options.HashScanOptions) (Result[string], []Result[string], error) + + HRandField(key string) (Result[string], error) + + HRandFieldWithCount(key string, count int64) ([]string, error) + + HRandFieldWithCountWithValues(key string, count int64) ([][]string, error) } diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index d8b3a734e2..1356b52d51 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -191,6 +191,33 @@ func handleStringArrayResponse(response *C.struct_CommandResponse) ([]Result[str return convertStringArray(response) } +func handle2DStringArrayResponse(response *C.struct_CommandResponse) ([][]string, error) { + defer C.free_command_response(response) + typeErr := checkResponseType(response, C.Array, false) + if typeErr != nil { + return nil, typeErr + } + array, err := parseArray(response) + if err != nil { + return nil, err + } + converted, err := arrayConverter[[]string]{ + arrayConverter[string]{ + nil, + false, + }, + false, + }.convert(array) + if err != nil { + return nil, err + } + res, ok := converted.([][]string) + if !ok { + return nil, &RequestError{fmt.Sprintf("unexpected type: %T", converted)} + } + return res, nil +} + func handleStringArrayOrNullResponse(response *C.struct_CommandResponse) ([]Result[string], error) { defer C.free_command_response(response) diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 507df5e959..ce184a3f92 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -1282,6 +1282,77 @@ func (suite *GlideTestSuite) TestHScan() { }) } +func (suite *GlideTestSuite) TestHRandField() { + suite.SkipIfServerVersionLowerThanBy("6.2.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + + // key does not exist + res, err := client.HRandField(key) + assert.NoError(suite.T(), err) + assert.True(suite.T(), res.IsNil()) + resc, err := client.HRandFieldWithCount(key, 5) + assert.NoError(suite.T(), err) + assert.Empty(suite.T(), resc) + rescv, err := client.HRandFieldWithCountWithValues(key, 5) + assert.NoError(suite.T(), err) + assert.Empty(suite.T(), rescv) + + data := map[string]string{"f1": "v1", "f2": "v2", "f3": "v3"} + hset, err := client.HSet(key, data) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(3), hset) + + fields := make([]string, 0, len(data)) + for k := range data { + fields = append(fields, k) + } + res, err = client.HRandField(key) + assert.NoError(suite.T(), err) + assert.Contains(suite.T(), fields, res.Value()) + + // With Count - positive count + resc, err = client.HRandFieldWithCount(key, 5) + assert.NoError(suite.T(), err) + assert.ElementsMatch(suite.T(), fields, resc) + + // With Count - negative count + resc, err = client.HRandFieldWithCount(key, -5) + assert.NoError(suite.T(), err) + assert.Len(suite.T(), resc, 5) + for _, field := range resc { + assert.Contains(suite.T(), fields, field) + } + + // With values - positive count + rescv, err = client.HRandFieldWithCountWithValues(key, 5) + assert.NoError(suite.T(), err) + resvMap := make(map[string]string) + for _, pair := range rescv { + resvMap[pair[0]] = pair[1] + } + assert.Equal(suite.T(), data, resvMap) + + // With values - negative count + rescv, err = client.HRandFieldWithCountWithValues(key, -5) + assert.NoError(suite.T(), err) + assert.Len(suite.T(), resc, 5) + for _, pair := range rescv { + assert.Contains(suite.T(), fields, pair[0]) + } + + // key exists but holds non hash type value + key = uuid.NewString() + suite.verifyOK(client.Set(key, "HRandField")) + _, err = client.HRandField(key) + assert.IsType(suite.T(), &api.RequestError{}, err) + _, err = client.HRandFieldWithCount(key, 42) + assert.IsType(suite.T(), &api.RequestError{}, err) + _, err = client.HRandFieldWithCountWithValues(key, 42) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + func (suite *GlideTestSuite) TestLPushLPop_WithExistingKey() { suite.runWithDefaultClients(func(client api.BaseClient) { list := []string{"value4", "value3", "value2", "value1"}