diff --git a/go/api/base_client.go b/go/api/base_client.go index 03808d748d..c94f30f608 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -23,6 +23,7 @@ import ( type BaseClient interface { StringCommands HashCommands + ListCommands // Close terminates the client by closing all associated resources. Close() @@ -135,7 +136,10 @@ func toCStrings(args []string) ([]C.uintptr_t, []C.ulong) { stringLengths := make([]C.ulong, len(args)) for i, str := range args { bytes := utils.StringToBytes(str) - ptr := uintptr(unsafe.Pointer(&bytes[0])) + var ptr uintptr + if len(str) > 0 { + ptr = uintptr(unsafe.Pointer(&bytes[0])) + } cStrings[i] = C.uintptr_t(ptr) stringLengths[i] = C.size_t(len(str)) } @@ -428,3 +432,83 @@ func (client *baseClient) HStrLen(key string, field string) (Result[int64], erro return handleLongResponse(result) } + +func (client *baseClient) LPush(key string, elements []string) (Result[int64], error) { + result, err := client.executeCommand(C.LPush, append([]string{key}, elements...)) + if err != nil { + return CreateNilInt64Result(), err + } + + return handleLongResponse(result) +} + +func (client *baseClient) LPop(key string) (Result[string], error) { + result, err := client.executeCommand(C.LPop, []string{key}) + if err != nil { + return CreateNilStringResult(), err + } + + return handleStringOrNullResponse(result) +} + +func (client *baseClient) LPopCount(key string, count int64) ([]Result[string], error) { + result, err := client.executeCommand(C.LPop, []string{key, utils.IntToString(count)}) + if err != nil { + return nil, err + } + + return handleStringArrayOrNullResponse(result) +} + +func (client *baseClient) LPos(key string, element string) (Result[int64], error) { + result, err := client.executeCommand(C.LPos, []string{key, element}) + if err != nil { + return CreateNilInt64Result(), err + } + + return handleLongOrNullResponse(result) +} + +func (client *baseClient) LPosWithOptions(key string, element string, options *LPosOptions) (Result[int64], error) { + result, err := client.executeCommand(C.LPos, append([]string{key, element}, options.toArgs()...)) + if err != nil { + return CreateNilInt64Result(), err + } + + return handleLongOrNullResponse(result) +} + +func (client *baseClient) LPosCount(key string, element string, count int64) ([]Result[int64], error) { + result, err := client.executeCommand(C.LPos, []string{key, element, CountKeyword, utils.IntToString(count)}) + if err != nil { + return nil, err + } + + return handleLongArrayResponse(result) +} + +func (client *baseClient) LPosCountWithOptions( + key string, + element string, + count int64, + options *LPosOptions, +) ([]Result[int64], error) { + result, err := client.executeCommand( + C.LPos, + append([]string{key, element, CountKeyword, utils.IntToString(count)}, options.toArgs()...), + ) + if err != nil { + return nil, err + } + + return handleLongArrayResponse(result) +} + +func (client *baseClient) RPush(key string, elements []string) (Result[int64], error) { + result, err := client.executeCommand(C.RPush, append([]string{key}, elements...)) + if err != nil { + return CreateNilInt64Result(), err + } + + return handleLongResponse(result) +} diff --git a/go/api/command_options.go b/go/api/command_options.go index e996032ce4..18e752dbe9 100644 --- a/go/api/command_options.go +++ b/go/api/command_options.go @@ -2,7 +2,11 @@ package api -import "strconv" +import ( + "strconv" + + "github.com/valkey-io/valkey-glide/go/glide/utils" +) // SetOptions represents optional arguments for the [api.StringCommands.SetWithOptions] command. // @@ -147,3 +151,55 @@ const ( UnixMilliseconds ExpiryType = "PXAT" // expire the value after the Unix time specified by [api.Expiry.Count], in milliseconds Persist ExpiryType = "PERSIST" // Remove the expiry associated with the key ) + +// LPosOptions represents optional arguments for the [api.ListCommands.LPosWithOptions] and +// [api.ListCommands.LPosCountWithOptions] commands. +// +// See [valkey.io] +// +// [valkey.io]: https://valkey.io/commands/lpos/ +type LPosOptions struct { + // Represents if the rank option is set. + IsRankSet bool + // The rank of the match to return. + Rank int64 + // Represents if the max length parameter is set. + IsMaxLenSet bool + // The maximum number of comparisons to make between the element and the items in the list. + MaxLen int64 +} + +func NewLPosOptionsBuilder() *LPosOptions { + return &LPosOptions{} +} + +func (lposOptions *LPosOptions) SetRank(rank int64) *LPosOptions { + lposOptions.IsRankSet = true + lposOptions.Rank = rank + return lposOptions +} + +func (lposOptions *LPosOptions) SetMaxLen(maxLen int64) *LPosOptions { + lposOptions.IsMaxLenSet = true + lposOptions.MaxLen = maxLen + return lposOptions +} + +func (opts *LPosOptions) toArgs() []string { + args := []string{} + if opts.IsRankSet { + args = append(args, RankKeyword, utils.IntToString(opts.Rank)) + } + + if opts.IsMaxLenSet { + args = append(args, MaxLenKeyword, utils.IntToString(opts.MaxLen)) + } + + return args +} + +const ( + CountKeyword string = "COUNT" // Valkey API keyword used to extract specific number of matching indices from a list. + RankKeyword string = "RANK" // Valkey API keyword use to determine the rank of the match to return. + MaxLenKeyword string = "MAXLEN" // Valkey API keyword used to determine the maximum number of list items to compare. +) diff --git a/go/api/list_commands.go b/go/api/list_commands.go new file mode 100644 index 0000000000..6993e56498 --- /dev/null +++ b/go/api/list_commands.go @@ -0,0 +1,195 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +// Supports commands and transactions for the "List Commands" group for standalone and cluster clients. +// +// See [valkey.io] for details. +// +// [valkey.io]: https://valkey.io/commands/?group=list +type ListCommands interface { + // Inserts all the specified values at the head of the list stored at key. elements are inserted one after the other to the + // head of the list, from the leftmost element to the rightmost element. If key does not exist, it is created as an empty + // list before performing the push operation. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the list. + // elements - The elements to insert at the head of the list stored at key. + // + // Return value: + // A api.Result[int64] containing the length of the list after the push operation. + // + // For example: + // result, err := client.LPush("my_list", []string{"value1", "value2"}) + // result.Value(): 2 + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/lpush/ + LPush(key string, elements []string) (Result[int64], error) + + // Removes and returns the first elements of the list stored at key. The command pops a single element from the beginning + // of the list. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the list. + // + // Return value: + // The Result[string] containing the value of the first element. + // If key does not exist, [api.CreateNilStringResult()] will be returned. + // + // For example: + // 1. result, err := client.LPush("my_list", []string{"value1", "value2"}) + // value, err := client.LPop("my_list") + // value.Value(): "value2" + // result.IsNil(): false + // 2. result, err := client.LPop("non_existent") + // result.Value(): "" + // result.IsNil(); true + // + // [valkey.io]: https://valkey.io/commands/lpop/ + LPop(key string) (Result[string], error) + + // Removes and returns up to count elements of the list stored at key, depending on the list's length. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the list. + // count - The count of the elements to pop from the list. + // + // Return value: + // An array of the popped elements as Result[string] will be returned depending on the list's length + // If key does not exist, nil will be returned. + // + // For example: + // 1. result, err := client.LPopCount("my_list", 2) + // result: []api.Result[string]{api.CreateStringResult("value1"), api.CreateStringResult("value2")} + // 2. result, err := client.LPopCount("non_existent") + // result: nil + // + // [valkey.io]: https://valkey.io/commands/lpop/ + LPopCount(key string, count int64) ([]Result[string], error) + + // Returns the index of the first occurrence of element inside the list specified by key. If no match is found, + // [api.CreateNilInt64Result()] is returned. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The name of the list. + // element - The value to search for within the list. + // + // Return value: + // The Result[int64] containing the index of the first occurrence of element, or [api.CreateNilInt64Result()] if element is + // not in the list. + // + // For example: + // result, err := client.RPush("my_list", []string{"a", "b", "c", "d", "e", "e"}) + // position, err := client.LPos("my_list", "e") + // position.Value(): 4 + // position.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/lpos/ + LPos(key string, element string) (Result[int64], error) + + // Returns the index of an occurrence of element within a list based on the given options. If no match is found, + // [api.CreateNilInt64Result()] is returned. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The name of the list. + // element - The value to search for within the list. + // options - The LPos options. + // + // Return value: + // The Result[int64] containing the index of element, or [api.CreateNilInt64Result()] if element is not in the list. + // + // For example: + // 1. result, err := client.RPush("my_list", []string{"a", "b", "c", "d", "e", "e"}) + // result, err := client.LPosWithOptions("my_list", "e", api.NewLPosOptionsBuilder().SetRank(2)) + // result.Value(): 5 (Returns the second occurrence of the element "e") + // 2. result, err := client.RPush("my_list", []string{"a", "b", "c", "d", "e", "e"}) + // result, err := client.LPosWithOptions("my_list", "e", api.NewLPosOptionsBuilder().SetRank(1).SetMaxLen(1000)) + // result.Value(): 4 + // + // [valkey.io]: https://valkey.io/commands/lpos/ + LPosWithOptions(key string, element string, options *LPosOptions) (Result[int64], error) + + // Returns an array of indices of matching elements within a list. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The name of the list. + // element - The value to search for within the list. + // count - The number of matches wanted. + // + // Return value: + // An array that holds the indices of the matching elements within the list. + // + // For example: + // result, err := client.RPush("my_list", []string{"a", "b", "c", "d", "e", "e", "e"}) + // result, err := client.LPosCount("my_list", "e", int64(3)) + // result: []api.Result[int64]{api.CreateInt64Result(4), api.CreateInt64Result(5), api.CreateInt64Result(6)} + // + // + // [valkey.io]: https://valkey.io/commands/lpos/ + LPosCount(key string, element string, count int64) ([]Result[int64], error) + + // Returns an array of indices of matching elements within a list based on the given options. If no match is found, an + // empty array is returned. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The name of the list. + // element - The value to search for within the list. + // count - The number of matches wanted. + // options - The LPos options. + // + // Return value: + // An array that holds the indices of the matching elements within the list. + // + // For example: + // 1. result, err := client.RPush("my_list", []string{"a", "b", "c", "d", "e", "e", "e"}) + // result, err := client.LPosWithOptions("my_list", "e", int64(1), api.NewLPosOptionsBuilder().SetRank(2)) + // result: []api.Result[int64]{api.CreateInt64Result(5)} + // 2. result, err := client.RPush("my_list", []string{"a", "b", "c", "d", "e", "e", "e"}) + // result, err := client.LPosWithOptions( + // "my_list", + // "e", + // int64(3), + // api.NewLPosOptionsBuilder().SetRank(2).SetMaxLen(1000), + // ) + // result: []api.Result[int64]{api.CreateInt64Result(5), api.CreateInt64Result(6)} + // + // + // [valkey.io]: https://valkey.io/commands/lpos/ + LPosCountWithOptions(key string, element string, count int64, options *LPosOptions) ([]Result[int64], error) + + // Inserts all the specified values at the tail of the list stored at key. + // elements are inserted one after the other to the tail of the list, from the leftmost element to the rightmost element. + // If key does not exist, it is created as an empty list before performing the push operation. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the list. + // elements - The elements to insert at the tail of the list stored at key. + // + // Return value: + // The Result[int64] containing the length of the list after the push operation. + // + // For example: + // result, err := client.RPush("my_list", []string{"a", "b", "c", "d", "e", "e", "e"}) + // result.Value(): 7 + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/rpush/ + RPush(key string, elements []string) (Result[int64], error) +} diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index 0959191ef5..554bfcec43 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -79,7 +79,30 @@ func handleStringArrayResponse(response *C.struct_CommandResponse) ([]Result[str return nil, typeErr } - var slice []Result[string] + slice := make([]Result[string], 0, response.array_value_len) + for _, v := range unsafe.Slice(response.array_value, response.array_value_len) { + res, err := convertCharArrayToString(&v, true) + if err != nil { + return nil, err + } + slice = append(slice, res) + } + return slice, nil +} + +func handleStringArrayOrNullResponse(response *C.struct_CommandResponse) ([]Result[string], error) { + defer C.free_command_response(response) + + typeErr := checkResponseType(response, C.Array, true) + if typeErr != nil { + return nil, typeErr + } + + if response.response_type == C.Null { + return nil, nil + } + + slice := make([]Result[string], 0, response.array_value_len) for _, v := range unsafe.Slice(response.array_value, response.array_value_len) { res, err := convertCharArrayToString(&v, true) if err != nil { @@ -101,6 +124,40 @@ func handleLongResponse(response *C.struct_CommandResponse) (Result[int64], erro return CreateInt64Result(int64(response.int_value)), nil } +func handleLongOrNullResponse(response *C.struct_CommandResponse) (Result[int64], error) { + defer C.free_command_response(response) + + typeErr := checkResponseType(response, C.Int, true) + if typeErr != nil { + return CreateNilInt64Result(), typeErr + } + + if response.response_type == C.Null { + return CreateNilInt64Result(), nil + } + + return CreateInt64Result(int64(response.int_value)), nil +} + +func handleLongArrayResponse(response *C.struct_CommandResponse) ([]Result[int64], error) { + defer C.free_command_response(response) + + typeErr := checkResponseType(response, C.Array, false) + if typeErr != nil { + return nil, typeErr + } + + slice := make([]Result[int64], 0, response.array_value_len) + for _, v := range unsafe.Slice(response.array_value, response.array_value_len) { + err := checkResponseType(&v, C.Int, false) + if err != nil { + return nil, err + } + slice = append(slice, CreateInt64Result(int64(v.int_value))) + } + return slice, nil +} + func handleDoubleResponse(response *C.struct_CommandResponse) (Result[float64], error) { defer C.free_command_response(response) diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index d3725d91a5..4315cda5d7 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -906,7 +906,7 @@ func (suite *GlideTestSuite) TestHVals_WithNotExistingKey() { res, err := client.HVals(key) assert.Nil(suite.T(), err) - assert.Nil(suite.T(), res) + assert.Equal(suite.T(), []api.Result[string]{}, res) }) } @@ -974,7 +974,7 @@ func (suite *GlideTestSuite) TestHKeys_WithNotExistingKey() { res, err := client.HKeys(key) assert.Nil(suite.T(), err) - assert.Nil(suite.T(), res) + assert.Equal(suite.T(), []api.Result[string]{}, res) }) } @@ -1017,3 +1017,215 @@ func (suite *GlideTestSuite) TestHStrLen_WithNotExistingField() { assert.Equal(suite.T(), int64(0), res2.Value()) }) } + +func (suite *GlideTestSuite) TestLPushLPop_WithExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + list := []string{"value4", "value3", "value2", "value1"} + key := uuid.NewString() + + res1, err := client.LPush(key, list) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(4), res1.Value()) + + res2, err := client.LPop(key) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "value1", res2.Value()) + + resultList := []api.Result[string]{api.CreateStringResult("value2"), api.CreateStringResult("value3")} + res3, err := client.LPopCount(key, 2) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), resultList, res3) + }) +} + +func (suite *GlideTestSuite) TestLPop_nonExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + + res1, err := client.LPop(key) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), api.CreateNilStringResult(), res1) + + res2, err := client.LPopCount(key, 2) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), ([]api.Result[string])(nil), res2) + }) +} + +func (suite *GlideTestSuite) TestLPushLPop_typeError() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + suite.verifyOK(client.Set(key, "value")) + + res1, err := client.LPush(key, []string{"value1"}) + assert.Equal(suite.T(), api.CreateNilInt64Result(), res1) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + res2, err := client.LPopCount(key, 2) + assert.Equal(suite.T(), ([]api.Result[string])(nil), res2) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestLPos_withAndWithoutOptions() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + res1, err := client.RPush(key, []string{"a", "a", "b", "c", "a", "b"}) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(6), res1.Value()) + + res2, err := client.LPos(key, "a") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(0), res2.Value()) + + res3, err := client.LPosWithOptions(key, "b", api.NewLPosOptionsBuilder().SetRank(2)) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(5), res3.Value()) + + // element doesn't exist + res4, err := client.LPos(key, "e") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), api.CreateNilInt64Result(), res4) + + // reverse traversal + res5, err := client.LPosWithOptions(key, "b", api.NewLPosOptionsBuilder().SetRank(-2)) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res5.Value()) + + // unlimited comparisons + res6, err := client.LPosWithOptions( + key, + "a", + api.NewLPosOptionsBuilder().SetRank(1).SetMaxLen(0), + ) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(0), res6.Value()) + + // limited comparisons + res7, err := client.LPosWithOptions( + key, + "c", + api.NewLPosOptionsBuilder().SetRank(1).SetMaxLen(2), + ) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), api.CreateNilInt64Result(), res7) + + // invalid rank value + res8, err := client.LPosWithOptions(key, "a", api.NewLPosOptionsBuilder().SetRank(0)) + assert.Equal(suite.T(), api.CreateNilInt64Result(), res8) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + // invalid maxlen value + res9, err := client.LPosWithOptions(key, "a", api.NewLPosOptionsBuilder().SetMaxLen(-1)) + assert.Equal(suite.T(), api.CreateNilInt64Result(), res9) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + // non-existent key + res10, err := client.LPos("non_existent_key", "a") + assert.Equal(suite.T(), api.CreateNilInt64Result(), res10) + assert.Nil(suite.T(), err) + + // wrong key data type + keyString := uuid.NewString() + suite.verifyOK(client.Set(keyString, "value")) + res11, err := client.LPos(keyString, "a") + assert.Equal(suite.T(), api.CreateNilInt64Result(), res11) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestLPosCount() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + + res1, err := client.RPush(key, []string{"a", "a", "b", "c", "a", "b"}) + assert.Equal(suite.T(), int64(6), res1.Value()) + assert.Nil(suite.T(), err) + + res2, err := client.LPosCount(key, "a", int64(2)) + assert.Equal(suite.T(), []api.Result[int64]{api.CreateInt64Result(0), api.CreateInt64Result(1)}, res2) + assert.Nil(suite.T(), err) + + res3, err := client.LPosCount(key, "a", int64(0)) + assert.Equal( + suite.T(), + []api.Result[int64]{api.CreateInt64Result(0), api.CreateInt64Result(1), api.CreateInt64Result(4)}, + res3, + ) + assert.Nil(suite.T(), err) + + // invalid count value + res4, err := client.LPosCount(key, "a", int64(-1)) + assert.Equal(suite.T(), ([]api.Result[int64])(nil), res4) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + // non-existent key + res5, err := client.LPosCount("non_existent_key", "a", int64(1)) + assert.Equal(suite.T(), []api.Result[int64]{}, res5) + assert.Nil(suite.T(), err) + + // wrong key data type + keyString := uuid.NewString() + suite.verifyOK(client.Set(keyString, "value")) + res6, err := client.LPosCount(keyString, "a", int64(1)) + assert.Equal(suite.T(), ([]api.Result[int64])(nil), res6) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestLPosCount_withOptions() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + + res1, err := client.RPush(key, []string{"a", "a", "b", "c", "a", "b"}) + assert.Equal(suite.T(), int64(6), res1.Value()) + assert.Nil(suite.T(), err) + + res2, err := client.LPosCountWithOptions(key, "a", int64(0), api.NewLPosOptionsBuilder().SetRank(1)) + assert.Equal( + suite.T(), + []api.Result[int64]{api.CreateInt64Result(0), api.CreateInt64Result(1), api.CreateInt64Result(4)}, + res2, + ) + assert.Nil(suite.T(), err) + + res3, err := client.LPosCountWithOptions(key, "a", int64(0), api.NewLPosOptionsBuilder().SetRank(2)) + assert.Equal(suite.T(), []api.Result[int64]{api.CreateInt64Result(1), api.CreateInt64Result(4)}, res3) + assert.Nil(suite.T(), err) + + // reverse traversal + res4, err := client.LPosCountWithOptions(key, "a", int64(0), api.NewLPosOptionsBuilder().SetRank(-1)) + assert.Equal( + suite.T(), + []api.Result[int64]{api.CreateInt64Result(4), api.CreateInt64Result(1), api.CreateInt64Result(0)}, + res4, + ) + assert.Nil(suite.T(), err) + }) +} + +func (suite *GlideTestSuite) TestRPush() { + suite.runWithDefaultClients(func(client api.BaseClient) { + list := []string{"value1", "value2", "value3", "value4"} + key := uuid.NewString() + + res1, err := client.RPush(key, list) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(4), res1.Value()) + + key2 := uuid.NewString() + suite.verifyOK(client.Set(key2, "value")) + + res2, err := client.LPush(key2, []string{"value1"}) + assert.Equal(suite.T(), api.CreateNilInt64Result(), res2) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +}