From 936b70aa3285e1a191bc7d1afd1c8cb817709d06 Mon Sep 17 00:00:00 2001 From: janhavigupta007 <46344506+janhavigupta007@users.noreply.github.com> Date: Fri, 11 Oct 2024 03:26:41 +0000 Subject: [PATCH] Implementing list commands (#2386) * Go: Implementing List commands Signed-off-by: Janhavi Gupta --- go/api/base_client.go | 82 ++++++++ go/api/command_options.go | 25 +++ go/api/list_commands.go | 194 +++++++++++++++++++ go/integTest/shared_commands_test.go | 271 ++++++++++++++++++++++++++- 4 files changed, 571 insertions(+), 1 deletion(-) diff --git a/go/api/base_client.go b/go/api/base_client.go index 5347f711f1..39584164ce 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -513,6 +513,88 @@ func (client *baseClient) RPush(key string, elements []string) (Result[int64], e return handleLongResponse(result) } +func (client *baseClient) LRange(key string, start int64, end int64) ([]Result[string], error) { + result, err := client.executeCommand(C.LRange, []string{key, utils.IntToString(start), utils.IntToString(end)}) + if err != nil { + return nil, err + } + + return handleStringArrayResponse(result) +} + +func (client *baseClient) LIndex(key string, index int64) (Result[string], error) { + result, err := client.executeCommand(C.LIndex, []string{key, utils.IntToString(index)}) + if err != nil { + return CreateNilStringResult(), err + } + + return handleStringOrNullResponse(result) +} + +func (client *baseClient) LTrim(key string, start int64, end int64) (Result[string], error) { + result, err := client.executeCommand(C.LTrim, []string{key, utils.IntToString(start), utils.IntToString(end)}) + if err != nil { + return CreateNilStringResult(), err + } + + return handleStringResponse(result) +} + +func (client *baseClient) LLen(key string) (Result[int64], error) { + result, err := client.executeCommand(C.LLen, []string{key}) + if err != nil { + return CreateNilInt64Result(), err + } + + return handleLongResponse(result) +} + +func (client *baseClient) LRem(key string, count int64, element string) (Result[int64], error) { + result, err := client.executeCommand(C.LRem, []string{key, utils.IntToString(count), element}) + if err != nil { + return CreateNilInt64Result(), err + } + + return handleLongResponse(result) +} + +func (client *baseClient) RPop(key string) (Result[string], error) { + result, err := client.executeCommand(C.RPop, []string{key}) + if err != nil { + return CreateNilStringResult(), err + } + + return handleStringOrNullResponse(result) +} + +func (client *baseClient) RPopCount(key string, count int64) ([]Result[string], error) { + result, err := client.executeCommand(C.RPop, []string{key, utils.IntToString(count)}) + if err != nil { + return nil, err + } + + return handleStringArrayOrNullResponse(result) +} + +func (client *baseClient) LInsert( + key string, + insertPosition InsertPosition, + pivot string, + element string, +) (Result[int64], error) { + insertPositionStr, err := insertPosition.toString() + if err != nil { + return CreateNilInt64Result(), err + } + + result, err := client.executeCommand(C.LInsert, []string{key, insertPositionStr, pivot, element}) + if err != nil { + return CreateNilInt64Result(), err + } + + return handleLongResponse(result) +} + func (client *baseClient) Ping() (string, error) { result, err := client.executeCommand(C.Ping, []string{}) if err != nil { diff --git a/go/api/command_options.go b/go/api/command_options.go index 18e752dbe9..7d37979926 100644 --- a/go/api/command_options.go +++ b/go/api/command_options.go @@ -203,3 +203,28 @@ const ( 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. ) + +// A InsertPosition defines where to insert new elements into a list. +// +// See [valkey.io] +// +// [valkey.io]: https://valkey.io/commands/linsert/ +type InsertPosition string + +const ( + // Insert new element before the pivot. + Before InsertPosition = "BEFORE" + // Insert new element after the pivot. + After InsertPosition = "AFTER" +) + +func (insertPosition InsertPosition) toString() (string, error) { + switch insertPosition { + case Before: + return string(Before), nil + case After: + return string(After), nil + default: + return "", &RequestError{"Invalid insert position"} + } +} diff --git a/go/api/list_commands.go b/go/api/list_commands.go index 6993e56498..f6149f11be 100644 --- a/go/api/list_commands.go +++ b/go/api/list_commands.go @@ -192,4 +192,198 @@ type ListCommands interface { // // [valkey.io]: https://valkey.io/commands/rpush/ RPush(key string, elements []string) (Result[int64], error) + + // Returns the specified elements of the list stored at key. + // The offsets start and end are zero-based indexes, with 0 being the first element of the list, 1 being the next element + // and so on. These offsets can also be negative numbers indicating offsets starting at the end of the list, with -1 being + // the last element of the list, -2 being the penultimate, and so on. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the list. + // start - The starting point of the range. + // end - The end of the range. + // + // Return value: + // Array of elements as Result[string] in the specified range. + // If start exceeds the end of the list, or if start is greater than end, an empty array will be returned. + // If end exceeds the actual end of the list, the range will stop at the actual end of the list. + // If key does not exist an empty array will be returned. + // + // For example: + // 1. result, err := client.LRange("my_list", 0, 2) + // result: []api.Result[string]{api.CreateStringResult("value1"), api.CreateStringResult("value2"), + // api.CreateStringResult("value3")} + // 2. result, err := client.LRange("my_list", -2, -1) + // result: []api.Result[string]{api.CreateStringResult("value2"), api.CreateStringResult("value3")} + // 3. result, err := client.LRange("non_existent_key", 0, 2) + // result: []api.Result[string]{} + // + // [valkey.io]: https://valkey.io/commands/lrange/ + LRange(key string, start int64, end int64) ([]Result[string], error) + + // Returns the element at index from the list stored at key. + // The index is zero-based, so 0 means the first element, 1 the second element and so on. Negative indices can be used to + // designate elements starting at the tail of the list. Here, -1 means the last element, -2 means the penultimate and so + // forth. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the list. + // index - The index of the element in the list to retrieve. + // + // Return value: + // The Result[string] containing element at index in the list stored at key. + // If index is out of range or if key does not exist, [api.CreateNilStringResult()] is returned. + // + // For example: + // 1. result, err := client.LIndex("myList", 0) + // result.Value(): "value1" // Returns the first element in the list stored at 'myList'. + // result.IsNil(): false + // 2. result, err := client.LIndex("myList", -1) + // result.Value(): "value3" // Returns the last element in the list stored at 'myList'. + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/lindex/ + LIndex(key string, index int64) (Result[string], error) + + // Trims an existing list so that it will contain only the specified range of elements specified. + // The offsets start and end are zero-based indexes, with 0 being the first element of the list, 1 being the next element + // and so on. These offsets can also be negative numbers indicating offsets starting at the end of the list, with -1 being + // the last element of the list, -2 being the penultimate, and so on. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the list. + // start - The starting point of the range. + // end - The end of the range. + // + // Return value: + // The Result[string] containing always "OK". + // If start exceeds the end of the list, or if start is greater than end, the result will be an empty list (which causes + // key to be removed). + // If end exceeds the actual end of the list, it will be treated like the last element of the list. + // If key does not exist, OK will be returned without changes to the database. + // + // For example: + // result, err := client.LTrim("my_list", 0, 1) + // result.Value(): "OK" + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/ltrim/ + LTrim(key string, start int64, end int64) (Result[string], error) + + // Returns the length of the list stored at key. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the list. + // + // Return value: + // The Result[int64] containing the length of the list at key. + // If key does not exist, it is interpreted as an empty list and 0 is returned. + // + // For example: + // result, err := client.LLen("my_list") + // result.Value(): int64(3) // Indicates that there are 3 elements in the list. + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/llen/ + LLen(key string) (Result[int64], error) + + // Removes the first count occurrences of elements equal to element from the list stored at key. + // If count is positive: Removes elements equal to element moving from head to tail. + // If count is negative: Removes elements equal to element moving from tail to head. + // If count is 0 or count is greater than the occurrences of elements equal to element, it removes all elements equal to + // element. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the list. + // count - The count of the occurrences of elements equal to element to remove. + // element - The element to remove from the list. + // + // Return value: + // The Result[int64] containing the number of the removed elements. + // If key does not exist, 0 is returned. + // + // For example: + // result, err := client.LRem("my_list", 2, "value") + // result.Value(): int64(2) + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/lrem/ + LRem(key string, count int64, element string) (Result[int64], error) + + // Removes and returns the last elements of the list stored at key. + // The command pops a single element from the end 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 last element. + // If key does not exist, [api.CreateNilStringResult()] will be returned. + // + // For example: + // 1. result, err := client.RPop("my_list") + // result.Value(): "value1" + // result.IsNil(): false + // 2. result, err := client.RPop("non_exiting_key") + // result.Value(): "" + // result.IsNil(): true + // + // [valkey.io]: https://valkey.io/commands/rpop/ + RPop(key string) (Result[string], error) + + // Removes and returns up to count elements from 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 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.RPopCount("my_list", 2) + // result: []api.Result[string]{api.CreateStringResult("value1"), api.CreateStringResult("value2")} + // 2. result, err := client.RPop("non_exiting_key") + // result: nil + // + // [valkey.io]: https://valkey.io/commands/rpop/ + RPopCount(key string, count int64) ([]Result[string], error) + + // Inserts element in the list at key either before or after the pivot. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the list. + // insertPosition - The relative position to insert into - either api.Before or api.After the pivot. + // pivot - An element of the list. + // element - The new element to insert. + // + // Return value: + // The Result[int64] containing the list length after a successful insert operation. + // If the key doesn't exist returns -1. + // If the pivot wasn't found, returns 0. + // + // For example: + // "my_list": {"Hello", "Wprld"} + // result, err := client.LInsert("my_list", api.Before, "World", "There") + // result.Value(): 3 + // + // [valkey.io]: https://valkey.io/commands/linsert/ + LInsert(key string, insertPosition InsertPosition, pivot string, element string) (Result[int64], error) } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index b5b46c8fd0..a25c9cbe54 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -1240,9 +1240,278 @@ func (suite *GlideTestSuite) TestRPush() { key2 := uuid.NewString() suite.verifyOK(client.Set(key2, "value")) - res2, err := client.LPush(key2, []string{"value1"}) + res2, err := client.RPush(key2, []string{"value1"}) assert.Equal(suite.T(), api.CreateNilInt64Result(), res2) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) }) } + +func (suite *GlideTestSuite) TestLRange() { + 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()) + + resultList := []api.Result[string]{ + api.CreateStringResult("value1"), + api.CreateStringResult("value2"), + api.CreateStringResult("value3"), + api.CreateStringResult("value4"), + } + res2, err := client.LRange(key, int64(0), int64(-1)) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), resultList, res2) + + res3, err := client.LRange("non_existing_key", int64(0), int64(-1)) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), []api.Result[string]{}, res3) + + key2 := uuid.NewString() + suite.verifyOK(client.Set(key2, "value")) + + res4, err := client.LRange(key2, int64(0), int64(1)) + assert.Equal(suite.T(), ([]api.Result[string])(nil), res4) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestLIndex() { + 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.LIndex(key, int64(0)) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "value1", res2.Value()) + assert.Equal(suite.T(), false, res2.IsNil()) + + res3, err := client.LIndex(key, int64(-1)) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "value4", res3.Value()) + assert.Equal(suite.T(), false, res3.IsNil()) + + res4, err := client.LIndex("non_existing_key", int64(0)) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), api.CreateNilStringResult(), res4) + + key2 := uuid.NewString() + suite.verifyOK(client.Set(key2, "value")) + + res5, err := client.LIndex(key2, int64(0)) + assert.Equal(suite.T(), api.CreateNilStringResult(), res5) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestLTrim() { + 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()) + + suite.verifyOK(client.LTrim(key, int64(0), int64(1))) + + res2, err := client.LRange(key, int64(0), int64(-1)) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), []api.Result[string]{api.CreateStringResult("value1"), api.CreateStringResult("value2")}, res2) + + suite.verifyOK(client.LTrim(key, int64(4), int64(2))) + + res3, err := client.LRange(key, int64(0), int64(-1)) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), []api.Result[string]{}, res3) + + key2 := uuid.NewString() + suite.verifyOK(client.Set(key2, "value")) + + res4, err := client.LIndex(key2, int64(0)) + assert.Equal(suite.T(), api.CreateNilStringResult(), res4) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestLLen() { + 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.LLen(key) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(4), res2.Value()) + assert.Equal(suite.T(), false, res2.IsNil()) + + res3, err := client.LLen("non_existing_key") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(0), res3.Value()) + assert.Equal(suite.T(), false, res3.IsNil()) + + key2 := uuid.NewString() + suite.verifyOK(client.Set(key2, "value")) + + res4, err := client.LLen(key2) + assert.Equal(suite.T(), api.CreateNilInt64Result(), res4) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestLRem() { + suite.runWithDefaultClients(func(client api.BaseClient) { + list := []string{"value1", "value2", "value1", "value1", "value2"} + key := uuid.NewString() + + res1, err := client.LPush(key, list) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(5), res1.Value()) + + res2, err := client.LRem(key, 2, "value1") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res2.Value()) + assert.Equal(suite.T(), false, res2.IsNil()) + res3, err := client.LRange(key, int64(0), int64(-1)) + assert.Nil(suite.T(), err) + assert.Equal( + suite.T(), + []api.Result[string]{ + api.CreateStringResult("value2"), + api.CreateStringResult("value2"), + api.CreateStringResult("value1"), + }, + res3, + ) + + res4, err := client.LRem(key, -1, "value2") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(1), res4.Value()) + assert.Equal(suite.T(), false, res4.IsNil()) + res5, err := client.LRange(key, int64(0), int64(-1)) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), []api.Result[string]{api.CreateStringResult("value2"), api.CreateStringResult("value1")}, res5) + + res6, err := client.LRem(key, 0, "value2") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(1), res6.Value()) + assert.Equal(suite.T(), false, res6.IsNil()) + res7, err := client.LRange(key, int64(0), int64(-1)) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), []api.Result[string]{api.CreateStringResult("value1")}, res7) + + res8, err := client.LRem("non_existing_key", 0, "value") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(0), res8.Value()) + assert.Equal(suite.T(), false, res8.IsNil()) + }) +} + +func (suite *GlideTestSuite) TestRPopAndRPopCount() { + 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()) + + res2, err := client.RPop(key) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "value4", res2.Value()) + assert.Equal(suite.T(), false, res2.IsNil()) + + res3, err := client.RPopCount(key, int64(2)) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), []api.Result[string]{api.CreateStringResult("value3"), api.CreateStringResult("value2")}, res3) + + res4, err := client.RPop("non_existing_key") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), api.CreateNilStringResult(), res4) + + res5, err := client.RPopCount("non_existing_key", int64(2)) + assert.Equal(suite.T(), ([]api.Result[string])(nil), res5) + assert.Nil(suite.T(), err) + + key2 := uuid.NewString() + suite.verifyOK(client.Set(key2, "value")) + + res6, err := client.RPop(key2) + assert.Equal(suite.T(), api.CreateNilStringResult(), res6) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + res7, err := client.RPopCount(key2, int64(2)) + assert.Equal(suite.T(), ([]api.Result[string])(nil), res7) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestLInsert() { + 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()) + + res2, err := client.LInsert(key, api.Before, "value2", "value1.5") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(5), res2.Value()) + assert.Equal(suite.T(), false, res2.IsNil()) + + res3, err := client.LInsert(key, api.After, "value3", "value3.5") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(6), res3.Value()) + assert.Equal(suite.T(), false, res3.IsNil()) + + res4, err := client.LRange(key, int64(0), int64(-1)) + assert.Nil(suite.T(), err) + assert.Equal( + suite.T(), + []api.Result[string]{ + api.CreateStringResult("value1"), + api.CreateStringResult("value1.5"), + api.CreateStringResult("value2"), + api.CreateStringResult("value3"), + api.CreateStringResult("value3.5"), + api.CreateStringResult("value4"), + }, + res4, + ) + + res5, err := client.LInsert("non_existing_key", api.Before, "pivot", "elem") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(0), res5.Value()) + assert.Equal(suite.T(), false, res5.IsNil()) + + res6, err := client.LInsert(key, api.Before, "value5", "value6") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(-1), res6.Value()) + assert.Equal(suite.T(), false, res6.IsNil()) + + key2 := uuid.NewString() + suite.verifyOK(client.Set(key2, "value")) + + res7, err := client.LInsert(key2, api.Before, "value5", "value6") + assert.Equal(suite.T(), api.CreateNilInt64Result(), res7) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +}