diff --git a/go/api/base_client.go b/go/api/base_client.go index fd22e348a5..0b7d9ef8e4 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -22,6 +22,7 @@ import ( // BaseClient defines an interface for methods common to both [GlideClient] and [GlideClusterClient]. type BaseClient interface { StringCommands + HashCommands // Close terminates the client by closing all associated resources. Close() @@ -146,7 +147,8 @@ func (client *baseClient) Set(key string, value string) (string, error) { if err != nil { return "", err } - return handleStringResponse(result), nil + + return handleStringResponse(result) } func (client *baseClient) SetWithOptions(key string, value string, options *SetOptions) (string, error) { @@ -154,7 +156,8 @@ func (client *baseClient) SetWithOptions(key string, value string, options *SetO if err != nil { return "", err } - return handleStringOrNullResponse(result), nil + + return handleStringOrNullResponse(result) } func (client *baseClient) Get(key string) (string, error) { @@ -162,7 +165,8 @@ func (client *baseClient) Get(key string) (string, error) { if err != nil { return "", err } - return handleStringOrNullResponse(result), nil + + return handleStringOrNullResponse(result) } func (client *baseClient) MSet(keyValueMap map[string]string) (string, error) { @@ -170,7 +174,8 @@ func (client *baseClient) MSet(keyValueMap map[string]string) (string, error) { if err != nil { return "", err } - return handleStringResponse(result), nil + + return handleStringResponse(result) } func (client *baseClient) MSetNX(keyValueMap map[string]string) (bool, error) { @@ -178,7 +183,8 @@ func (client *baseClient) MSetNX(keyValueMap map[string]string) (bool, error) { if err != nil { return false, err } - return handleBooleanResponse(result), nil + + return handleBooleanResponse(result) } func (client *baseClient) MGet(keys []string) ([]string, error) { @@ -186,7 +192,8 @@ func (client *baseClient) MGet(keys []string) ([]string, error) { if err != nil { return nil, err } - return handleStringArrayResponse(result), nil + + return handleStringArrayResponse(result) } func (client *baseClient) Incr(key string) (int64, error) { @@ -194,7 +201,8 @@ func (client *baseClient) Incr(key string) (int64, error) { if err != nil { return 0, err } - return handleLongResponse(result), nil + + return handleLongResponse(result) } func (client *baseClient) IncrBy(key string, amount int64) (int64, error) { @@ -202,7 +210,8 @@ func (client *baseClient) IncrBy(key string, amount int64) (int64, error) { if err != nil { return 0, err } - return handleLongResponse(result), nil + + return handleLongResponse(result) } func (client *baseClient) IncrByFloat(key string, amount float64) (float64, error) { @@ -213,7 +222,8 @@ func (client *baseClient) IncrByFloat(key string, amount float64) (float64, erro if err != nil { return 0, err } - return handleDoubleResponse(result), nil + + return handleDoubleResponse(result) } func (client *baseClient) Decr(key string) (int64, error) { @@ -221,7 +231,8 @@ func (client *baseClient) Decr(key string) (int64, error) { if err != nil { return 0, err } - return handleLongResponse(result), nil + + return handleLongResponse(result) } func (client *baseClient) DecrBy(key string, amount int64) (int64, error) { @@ -229,7 +240,8 @@ func (client *baseClient) DecrBy(key string, amount int64) (int64, error) { if err != nil { return 0, err } - return handleLongResponse(result), nil + + return handleLongResponse(result) } func (client *baseClient) Strlen(key string) (int64, error) { @@ -237,7 +249,8 @@ func (client *baseClient) Strlen(key string) (int64, error) { if err != nil { return 0, err } - return handleLongResponse(result), nil + + return handleLongResponse(result) } func (client *baseClient) SetRange(key string, offset int, value string) (int64, error) { @@ -245,7 +258,8 @@ func (client *baseClient) SetRange(key string, offset int, value string) (int64, if err != nil { return 0, err } - return handleLongResponse(result), nil + + return handleLongResponse(result) } func (client *baseClient) GetRange(key string, start int, end int) (string, error) { @@ -253,7 +267,8 @@ func (client *baseClient) GetRange(key string, start int, end int) (string, erro if err != nil { return "", err } - return handleStringResponse(result), nil + + return handleStringResponse(result) } func (client *baseClient) Append(key string, value string) (int64, error) { @@ -261,7 +276,8 @@ func (client *baseClient) Append(key string, value string) (int64, error) { if err != nil { return 0, err } - return handleLongResponse(result), nil + + return handleLongResponse(result) } func (client *baseClient) LCS(key1 string, key2 string) (string, error) { @@ -269,7 +285,8 @@ func (client *baseClient) LCS(key1 string, key2 string) (string, error) { if err != nil { return "", err } - return handleStringResponse(result), nil + + return handleStringResponse(result) } func (client *baseClient) GetDel(key string) (string, error) { @@ -282,5 +299,104 @@ func (client *baseClient) GetDel(key string) (string, error) { return "", err } - return handleStringOrNullResponse(result), nil + return handleStringOrNullResponse(result) +} + +func (client *baseClient) HGet(key string, field string) (string, error) { + result, err := client.executeCommand(C.HGet, []string{key, field}) + if err != nil { + return "", err + } + + return handleStringOrNullResponse(result) +} + +func (client *baseClient) HGetAll(key string) (map[string]string, error) { + result, err := client.executeCommand(C.HGetAll, []string{key}) + if err != nil { + return nil, err + } + + return handleStringToStringMapResponse(result) +} + +func (client *baseClient) HMGet(key string, fields []string) ([]string, error) { + result, err := client.executeCommand(C.HMGet, append([]string{key}, fields...)) + if err != nil { + return nil, err + } + + return handleStringArrayResponse(result) +} + +func (client *baseClient) HSet(key string, values map[string]string) (int64, error) { + result, err := client.executeCommand(C.HSet, utils.ConvertMapToKeyValueStringArray(key, values)) + if err != nil { + return 0, err + } + + return handleLongResponse(result) +} + +func (client *baseClient) HSetNX(key string, field string, value string) (bool, error) { + result, err := client.executeCommand(C.HSetNX, []string{key, field, value}) + if err != nil { + return false, err + } + + return handleBooleanResponse(result) +} + +func (client *baseClient) HDel(key string, fields []string) (int64, error) { + result, err := client.executeCommand(C.HDel, append([]string{key}, fields...)) + if err != nil { + return 0, err + } + + return handleLongResponse(result) +} + +func (client *baseClient) HLen(key string) (int64, error) { + result, err := client.executeCommand(C.HLen, []string{key}) + if err != nil { + return 0, err + } + + return handleLongResponse(result) +} + +func (client *baseClient) HVals(key string) ([]string, error) { + result, err := client.executeCommand(C.HVals, []string{key}) + if err != nil { + return nil, err + } + + return handleStringArrayResponse(result) +} + +func (client *baseClient) HExists(key string, field string) (bool, error) { + result, err := client.executeCommand(C.HExists, []string{key, field}) + if err != nil { + return false, err + } + + return handleBooleanResponse(result) +} + +func (client *baseClient) HKeys(key string) ([]string, error) { + result, err := client.executeCommand(C.HKeys, []string{key}) + if err != nil { + return nil, err + } + + return handleStringArrayResponse(result) +} + +func (client *baseClient) HStrLen(key string, field string) (int64, error) { + result, err := client.executeCommand(C.HStrlen, []string{key, field}) + if err != nil { + return 0, err + } + + return handleLongResponse(result) } diff --git a/go/api/commands.go b/go/api/commands.go index 652b896e42..56fbde418a 100644 --- a/go/api/commands.go +++ b/go/api/commands.go @@ -372,3 +372,228 @@ type StringCommands interface { //[valkey.io]: https://valkey.io/commands/getdel/ GetDel(key string) (string, error) } + +// HashCommands supports commands and transactions for the "Hash Commands" group for standalone and cluster +// clients. +// +// See [valkey.io] for details. +// +// [valkey.io]: https://valkey.io/commands/?group=hash +type HashCommands interface { + // HGet returns the value associated with field in the hash stored at key. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // field - The field in the hash stored at key to retrieve from the database. + // + // Return value: + // The value associated with field, or an empty string when field is not present in the hash or key does not exist. + // + // For example: + // Assume we have the following hash: + // my_hash := map[string]string{"field1": "value", "field2": "another_value"} + // payload, err := client.HGet("my_hash", "field1") + // // payload equals "value" + // payload, err = client.HGet("my_hash", "nonexistent_field") + // // payload equals "" + // + // [valkey.io]: https://valkey.io/commands/hget/ + HGet(key string, field string) (string, error) + + // HGetAll returns all fields and values of the hash stored at key. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // + // Return value: + // A map of all fields and their values in the hash, or an empty map when key does not exist. + // + // For example: + // fieldValueMap, err := client.HGetAll("my_hash") + // // fieldValueMap equals map[string]string{"field1": "value1", "field2": "value2"} + // + // [valkey.io]: https://valkey.io/commands/hgetall/ + HGetAll(key string) (map[string]string, error) + + // HMGet returns the values associated with the specified fields in the hash stored at key. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // fields - The fields in the hash stored at key to retrieve from the database. + // + // Return value: + // An array of values associated with the given fields, in the same order as they are requested. + // For every field that does not exist in the hash, a null value is returned. + // If key does not exist, returns an empty string array. + // + // For example: + // values, err := client.HMGet("my_hash", []string{"field1", "field2"}) + // // values equals []string{"value1", "value2"} + // + // [valkey.io]: https://valkey.io/commands/hmget/ + HMGet(key string, fields []string) ([]string, error) + + // HSet sets the specified fields to their respective values in the hash stored at key. + // This command overwrites the values of specified fields that exist in the hash. + // If key doesn't exist, a new key holding a hash is created. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // values - A map of field-value pairs to set in the hash. + // + // Return value: + // The number of fields that were added or updated. + // + // For example: + // num, err := client.HSet("my_hash", map[string]string{"field": "value", "field2": "value2"}) + // // num equals 2 + // + // [valkey.io]: https://valkey.io/commands/hset/ + HSet(key string, values map[string]string) (int64, error) + + // HSetNX sets field in the hash stored at key to value, only if field does not yet exist. + // If key does not exist, a new key holding a hash is created. + // If field already exists, this operation has no effect. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // field - The field to set. + // value - The value to set. + // + // Return value: + // true if field is a new field in the hash and value was set. + // false if field already exists in the hash and no operation was performed. + // + // For example: + // payload1, err := client.HSetNX("myHash", "field", "value") + // // payload1 equals true + // payload2, err := client.HSetNX("myHash", "field", "newValue") + // // payload2 equals false + // + // [valkey.io]: https://valkey.io/commands/hsetnx/ + HSetNX(key string, field string, value string) (bool, error) + + // HDel removes the specified fields from the hash stored at key. + // Specified fields that do not exist within this hash are ignored. + // If key does not exist, it is treated as an empty hash and this command returns 0. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // fields - The fields to remove from the hash stored at key. + // + // Return value: + // The number of fields that were removed from the hash, not including specified but non-existing fields. + // + // For example: + // num, err := client.HDel("my_hash", []string{"field_1", "field_2"}) + // // num equals 2 + // + // [valkey.io]: https://valkey.io/commands/hdel/ + HDel(key string, fields []string) (int64, error) + + // HLen returns the number of fields contained in the hash stored at key. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // + // Return value: + // The number of fields in the hash, or 0 when key does not exist. + // If key holds a value that is not a hash, an error is returned. + // + // For example: + // num1, err := client.HLen("myHash") + // // num1 equals 3 + // num2, err := client.HLen("nonExistingKey") + // // num2 equals 0 + // + // [valkey.io]: https://valkey.io/commands/hlen/ + HLen(key string) (int64, error) + + // HVals returns all values in the hash stored at key. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // + // Return value: + // A slice of strings containing all the values in the hash, or an empty slice when key does not exist. + // + // For example: + // values, err := client.HVals("myHash") + // // values equals []string{"value1", "value2", "value3"} + // + // [valkey.io]: https://valkey.io/commands/hvals/ + HVals(key string) ([]string, error) + + // HExists returns if field is an existing field in the hash stored at key. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // field - The field to check in the hash stored at key. + // + // Return value: + // true if the hash contains the specified field. + // false if the hash does not contain the field, or if the key does not exist. + // + // For example: + // exists, err := client.HExists("my_hash", "field1") + // // exists equals true + // exists, err = client.HExists("my_hash", "non_existent_field") + // // exists equals false + // + // [valkey.io]: https://valkey.io/commands/hexists/ + HExists(key string, field string) (bool, error) + + // HKeys returns all field names in the hash stored at key. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // + // Return value: + // A slice of strings containing all the field names in the hash, or an empty slice when key does not exist. + // + // For example: + // names, err := client.HKeys("my_hash") + // // names equals []string{"field_1", "field_2"} + // + // [valkey.io]: https://valkey.io/commands/hkeys/ + HKeys(key string) ([]string, error) + + // HStrLen returns the string length of the value associated with field in the hash stored at key. + // If the key or the field do not exist, 0 is returned. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // field - The field to get the string length of its value. + // + // Return value: + // The length of the string value associated with field, or 0 when field or key do not exist. + // + // For example: + // strlen, err := client.HStrLen("my_hash", "my_field") + // // strlen equals 10 + // + // [valkey.io]: https://valkey.io/commands/hstrlen/ + HStrLen(key string, field string) (int64, error) +} diff --git a/go/api/glide_client.go b/go/api/glide_client.go index 2c4618ab58..a62573284b 100644 --- a/go/api/glide_client.go +++ b/go/api/glide_client.go @@ -40,7 +40,8 @@ func (client *GlideClient) CustomCommand(args []string) (interface{}, error) { if err != nil { return nil, err } - return handleStringOrNullResponse(res), nil + + return handleStringOrNullResponse(res) } // Sets configuration parameters to the specified values. @@ -66,7 +67,7 @@ func (client *GlideClient) ConfigSet(parameters map[string]string) (string, erro if err != nil { return "", err } - return handleStringResponse(result), nil + return handleStringResponse(result) } // Gets the values of configuration parameters. @@ -93,5 +94,5 @@ func (client *GlideClient) ConfigGet(args []string) (map[string]string, error) { if err != nil { return nil, err } - return handleStringToStringMapResponse(res), nil + return handleStringToStringMapResponse(res) } diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index ac79df9a32..87e5fb5424 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -7,6 +7,7 @@ package api import "C" import ( + "errors" "unsafe" ) @@ -19,43 +20,70 @@ func convertCharArrayToString(arr *C.char, length C.long) string { return string(byteSlice) } -func handleStringResponse(response *C.struct_CommandResponse) string { +func handleStringResponse(response *C.struct_CommandResponse) (string, error) { + if response == nil { + return "", errors.New("handleStringArrayResponse: command response is nil") + } + defer C.free_command_response(response) - return convertCharArrayToString(response.string_value, response.string_value_len) + return convertCharArrayToString(response.string_value, response.string_value_len), nil } -func handleStringOrNullResponse(response *C.struct_CommandResponse) string { +func handleStringOrNullResponse(response *C.struct_CommandResponse) (string, error) { if response == nil { - return "" + return "", nil } + return handleStringResponse(response) } -func handleStringArrayResponse(response *C.struct_CommandResponse) []string { +// handleBooleanResponse converts a C struct_CommandResponse's bool_value to a Go bool. +func handleBooleanResponse(response *C.struct_CommandResponse) (bool, error) { + if response == nil { + return false, errors.New("handleBooleanResponse: command response is nil") + } + + defer C.free_command_response(response) + return bool(response.bool_value), nil +} + +func handleStringArrayResponse(response *C.struct_CommandResponse) ([]string, error) { + if response == nil { + return nil, errors.New("handleStringArrayResponse: command response is nil") + } + defer C.free_command_response(response) var slice []string for _, v := range unsafe.Slice(response.array_value, response.array_value_len) { slice = append(slice, convertCharArrayToString(v.string_value, v.string_value_len)) } - return slice -} -func handleLongResponse(response *C.struct_CommandResponse) int64 { - defer C.free_command_response(response) - return int64(response.int_value) + return slice, nil } -func handleDoubleResponse(response *C.struct_CommandResponse) float64 { +func handleLongResponse(response *C.struct_CommandResponse) (int64, error) { + if response == nil { + return 0, errors.New("handleLongResponse: command response is nil") + } + defer C.free_command_response(response) - return float64(response.float_value) + return int64(response.int_value), nil } -func handleBooleanResponse(response *C.struct_CommandResponse) bool { +func handleDoubleResponse(response *C.struct_CommandResponse) (float64, error) { + if response == nil { + return 0, errors.New("handleStringArrayResponse: command response is nil") + } + defer C.free_command_response(response) - return bool(response.bool_value) + return float64(response.float_value), nil } -func handleStringToStringMapResponse(response *C.struct_CommandResponse) map[string]string { +func handleStringToStringMapResponse(response *C.struct_CommandResponse) (map[string]string, error) { + if response == nil { + return nil, errors.New("handleStringMapResponse: command response is nil") + } + defer C.free_command_response(response) m := make(map[string]string, response.array_value_len) for _, v := range unsafe.Slice(response.array_value, response.array_value_len) { @@ -63,5 +91,6 @@ func handleStringToStringMapResponse(response *C.struct_CommandResponse) map[str value := convertCharArrayToString(v.map_value.string_value, v.map_value.string_value_len) m[key] = value } - return m + + return m, nil } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index c7c6b3d69f..410e3a86d4 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -521,3 +521,409 @@ func (suite *GlideTestSuite) TestGetDel_EmptyKey() { assert.Equal(suite.T(), "key is required", err.Error()) }) } + +func (suite *GlideTestSuite) TestHSet_WithExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + fields := map[string]string{"field1": "value1", "field2": "value2"} + key := uuid.New().String() + + res1, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res1) + + res2, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(0), res2) + }) +} + +func (suite *GlideTestSuite) TestHSet_byteString() { + suite.runWithDefaultClients(func(client api.BaseClient) { + fields := map[string]string{ + string([]byte{0xFF, 0x00, 0xAA}): string([]byte{0xDE, 0xAD, 0xBE, 0xEF}), + string([]byte{0x01, 0x02, 0x03, 0xFE}): string([]byte{0xCA, 0xFE, 0xBA, 0xBE}), + } + key := string([]byte{0x01, 0x02, 0x03, 0xFE}) + + res1, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res1) + + res2, err := client.HGetAll(key) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), fields, res2) + }) +} + +func (suite *GlideTestSuite) TestHSet_WithAddNewField() { + suite.runWithDefaultClients(func(client api.BaseClient) { + fields := map[string]string{"field1": "value1", "field2": "value2"} + key := uuid.New().String() + + res1, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res1) + + res2, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(0), res2) + + fields["field3"] = "value3" + res3, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(1), res3) + }) +} + +func (suite *GlideTestSuite) TestHGet_WithExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + fields := map[string]string{"field1": "value1", "field2": "value2"} + key := uuid.NewString() + + res1, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res1) + + res2, err := client.HGet(key, "field1") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "value1", res2) + }) +} + +func (suite *GlideTestSuite) TestHGet_WithNotExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + + res1, err := client.HGet(key, "field1") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "", res1) + }) +} + +func (suite *GlideTestSuite) TestHGet_WithNotExistingField() { + suite.runWithDefaultClients(func(client api.BaseClient) { + fields := map[string]string{"field1": "value1", "field2": "value2"} + key := uuid.NewString() + + res1, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res1) + + res2, err := client.HGet(key, "foo") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "", res2) + }) +} + +func (suite *GlideTestSuite) TestHGetAll_WithExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + fields := map[string]string{"field1": "value1", "field2": "value2"} + key := uuid.NewString() + + res1, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res1) + + res2, err := client.HGetAll(key) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), fields, res2) + }) +} + +func (suite *GlideTestSuite) TestHGetAll_WithNotExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + + res, err := client.HGetAll(key) + assert.Nil(suite.T(), err) + assert.Empty(suite.T(), res) + }) +} + +func (suite *GlideTestSuite) TestHMGet() { + suite.runWithDefaultClients(func(client api.BaseClient) { + fields := map[string]string{"field1": "value1", "field2": "value2"} + key := uuid.NewString() + + res1, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res1) + + res2, err := client.HMGet(key, []string{"field1", "field2", "field3"}) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), []string{"value1", "value2", ""}, res2) + }) +} + +func (suite *GlideTestSuite) TestHMGet_WithNotExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + + res, err := client.HMGet(key, []string{"field1", "field2", "field3"}) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), []string{"", "", ""}, res) + }) +} + +func (suite *GlideTestSuite) TestHMGet_WithNotExistingField() { + suite.runWithDefaultClients(func(client api.BaseClient) { + fields := map[string]string{"field1": "value1", "field2": "value2"} + key := uuid.NewString() + + res1, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res1) + + res2, err := client.HMGet(key, []string{"field3", "field4"}) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), []string{"", ""}, res2) + }) +} + +func (suite *GlideTestSuite) TestHSetNX_WithExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + fields := map[string]string{"field1": "value1", "field2": "value2"} + key := uuid.NewString() + + res1, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res1) + + res2, err := client.HSetNX(key, "field1", "value1") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), false, res2) + }) +} + +func (suite *GlideTestSuite) TestHSetNX_WithNotExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + + res1, err := client.HSetNX(key, "field1", "value1") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), true, res1) + + res2, err := client.HGetAll(key) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), map[string]string{"field1": "value1"}, res2) + }) +} + +func (suite *GlideTestSuite) TestHSetNX_WithExistingField() { + suite.runWithDefaultClients(func(client api.BaseClient) { + fields := map[string]string{"field1": "value1", "field2": "value2"} + key := uuid.NewString() + + res1, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res1) + + res2, err := client.HSetNX(key, "field1", "value1") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), false, res2) + }) +} + +func (suite *GlideTestSuite) TestHDel() { + suite.runWithDefaultClients(func(client api.BaseClient) { + fields := map[string]string{"field1": "value1", "field2": "value2"} + key := uuid.NewString() + + res1, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res1) + + res2, err := client.HDel(key, []string{"field1", "field2"}) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res2) + + res3, err := client.HGetAll(key) + assert.Nil(suite.T(), err) + assert.Empty(suite.T(), res3) + + res4, err := client.HDel(key, []string{"field1", "field2"}) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(0), res4) + }) +} + +func (suite *GlideTestSuite) TestHDel_WithNotExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + res, err := client.HDel(key, []string{"field1", "field2"}) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(0), res) + }) +} + +func (suite *GlideTestSuite) TestHDel_WithNotExistingField() { + suite.runWithDefaultClients(func(client api.BaseClient) { + fields := map[string]string{"field1": "value1", "field2": "value2"} + key := uuid.NewString() + + res1, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res1) + + res2, err := client.HDel(key, []string{"field3", "field4"}) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(0), res2) + }) +} + +func (suite *GlideTestSuite) TestHLen() { + suite.runWithDefaultClients(func(client api.BaseClient) { + fields := map[string]string{"field1": "value1", "field2": "value2"} + key := uuid.NewString() + + res1, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res1) + + res2, err := client.HLen(key) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res2) + }) +} + +func (suite *GlideTestSuite) TestHLen_WithNotExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + res, err := client.HLen(key) + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(0), res) + }) +} + +func (suite *GlideTestSuite) TestHVals_WithExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + fields := map[string]string{"field1": "value1", "field2": "value2"} + key := uuid.NewString() + + res1, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res1) + + res2, err := client.HVals(key) + assert.Nil(suite.T(), err) + assert.Contains(suite.T(), res2, "value1") + assert.Contains(suite.T(), res2, "value2") + }) +} + +func (suite *GlideTestSuite) TestHVals_WithNotExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + + res, err := client.HVals(key) + assert.Nil(suite.T(), err) + assert.Nil(suite.T(), res) + }) +} + +func (suite *GlideTestSuite) TestHExists_WithExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + fields := map[string]string{"field1": "value1", "field2": "value2"} + key := uuid.NewString() + + res1, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res1) + + res2, err := client.HExists(key, "field1") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), true, res2) + }) +} + +func (suite *GlideTestSuite) TestHExists_WithNotExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + + res, err := client.HExists(key, "field1") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), false, res) + }) +} + +func (suite *GlideTestSuite) TestHExists_WithNotExistingField() { + suite.runWithDefaultClients(func(client api.BaseClient) { + fields := map[string]string{"field1": "value1", "field2": "value2"} + key := uuid.NewString() + + res1, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res1) + + res2, err := client.HExists(key, "field3") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), false, res2) + }) +} + +func (suite *GlideTestSuite) TestHKeys_WithExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + fields := map[string]string{"field1": "value1", "field2": "value2"} + key := uuid.NewString() + + res1, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res1) + + res2, err := client.HKeys(key) + assert.Nil(suite.T(), err) + assert.Contains(suite.T(), res2, "field1") + assert.Contains(suite.T(), res2, "field2") + }) +} + +func (suite *GlideTestSuite) TestHKeys_WithNotExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + + res, err := client.HKeys(key) + assert.Nil(suite.T(), err) + assert.Nil(suite.T(), res) + }) +} + +func (suite *GlideTestSuite) TestHStrLen_WithExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + fields := map[string]string{"field1": "value1", "field2": "value2"} + key := uuid.NewString() + + res1, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res1) + + res2, err := client.HStrLen(key, "field1") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(6), res2) + }) +} + +func (suite *GlideTestSuite) TestHStrLen_WithNotExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + + res, err := client.HStrLen(key, "field1") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(0), res) + }) +} + +func (suite *GlideTestSuite) TestHStrLen_WithNotExistingField() { + suite.runWithDefaultClients(func(client api.BaseClient) { + fields := map[string]string{"field1": "value1", "field2": "value2"} + key := uuid.NewString() + + res1, err := client.HSet(key, fields) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res1) + + res2, err := client.HStrLen(key, "field3") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(0), res2) + }) +} diff --git a/go/utils/transform_utils.go b/go/utils/transform_utils.go index 11470356dc..4427a091ee 100644 --- a/go/utils/transform_utils.go +++ b/go/utils/transform_utils.go @@ -22,6 +22,25 @@ func FloatToString(value float64) string { return strconv.FormatFloat(value, 'g', -1 /*precision*/, 64 /*bit*/) } +// ConvertMapToKeyValueStringArray converts a map of string keys and values to a slice of the initial key followed by the +// key-value pairs. +func ConvertMapToKeyValueStringArray(key string, args map[string]string) []string { + // Preallocate the slice with space for the initial key and twice the number of map entries (each entry has a key and a + // value). + values := make([]string, 1, 1+2*len(args)) + + // Set the first element of the slice to the provided key. + values[0] = key + + // Loop over each key-value pair in the map and append them to the slice. + for k, v := range args { + // Append the key and value directly to the slice. + values = append(values, k, v) + } + + return values +} + // Flattens the Map: { (key1, value1), (key2, value2), ..} to a slice { key1, value1, key2, value2, ..} func MapToString(parameter map[string]string) []string { flat := make([]string, 0, len(parameter)*2) diff --git a/go/utils/transform_utils_test.go b/go/utils/transform_utils_test.go new file mode 100644 index 0000000000..635fef7d56 --- /dev/null +++ b/go/utils/transform_utils_test.go @@ -0,0 +1,61 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package utils + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConvertMapToKeyValueStringArray(t *testing.T) { + // Define test cases + testCases := []struct { + name string + key string + args map[string]string + expected []string + }{ + { + name: "Test with single key-value pair", + key: "singleKey", + args: map[string]string{"key1": "value1"}, + expected: []string{"singleKey", "key1", "value1"}, + }, + { + name: "Test with multiple key-value pairs", + key: "multiKeys", + args: map[string]string{"key1": "value1", "key2": "value2", "key3": "value3"}, + expected: []string{"multiKeys", "key1", "value1", "key2", "value2", "key3", "value3"}, + }, + { + name: "Test with empty map", + key: "emptyKey", + args: map[string]string{}, + expected: []string{"emptyKey"}, + }, + } + + // Iterate through test cases. + for _, testCase := range testCases { + // Run each test case as a subtest. + t.Run(testCase.name, func(t *testing.T) { + // Call the function being tested. + actual := ConvertMapToKeyValueStringArray(testCase.key, testCase.args) + + // Check if the lengths of actual and expected slices match. + if len(actual) != len(testCase.expected) { + t.Errorf("Length mismatch. Expected %d, got %d", len(testCase.expected), len(actual)) + } + + // Check if the key is present in the actual result. + assert.Contains(t, actual, testCase.key, "The key should be present in the result") + + // Check if all key-value pairs from the input map are present in the actual result. + for k, v := range testCase.args { + assert.Contains(t, actual, k, "The key from the input map should be present in the result") + assert.Contains(t, actual, v, "The value from the input map should be present in the result") + } + }) + } +}