diff --git a/docs/src/content/docs/commands/LLEN.md b/docs/src/content/docs/commands/LLEN.md index 0910ec718..261e30836 100644 --- a/docs/src/content/docs/commands/LLEN.md +++ b/docs/src/content/docs/commands/LLEN.md @@ -38,6 +38,11 @@ LLEN key - Error Message: `(error) WRONGTYPE Operation against a key holding the wrong kind of value` - Occurs if the key exists but is not associated with a list. +2. `Wrong number of arguments` + + - Error Message: `(error) ERR wrong number of arguments for 'llen' command` + - Occurs if command is executed without any arguments or with 2 or more arguments + ## Example Usage ### Basic Usage @@ -64,7 +69,7 @@ Getting the `LLEN` of a list `nonExistentList` which does not exist. (integer) 0 ``` -### Invalid usage +### Invalid Usage: Key is Not a List Trying to get the `LLEN` of a key `mystring` which is holding wrong data type `string`. diff --git a/docs/src/content/docs/commands/LPOP.md b/docs/src/content/docs/commands/LPOP.md index cdbaf3539..ad726a298 100644 --- a/docs/src/content/docs/commands/LPOP.md +++ b/docs/src/content/docs/commands/LPOP.md @@ -17,26 +17,32 @@ LPOP key | --------- | ------------------------------------------------------------------------------ | ------ | -------- | | `key` | The key of the list from which the first element will be removed and returned. | String | Yes | + + ## Return Value -| Condition | Return Value | -| ---------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Command is successful | `String` The value of the first element in the list, if the list exists and is not empty. | -| If the key does not exist or the list is empty | `nil` | -| Syntax or specified constraints are invalid | error | +| Condition | Return Value | +| ---------------------------- | ---------------------------------------------------- | +| Command is successful | `String` The value of the first element in the list. | +| If the key does not exist | `nil` | +| The key is of the wrong type | error | + + ## Behavior -- When the `LPOP` command is executed, DiceDB checks if the key exists and is associated with a list. If so, the first element of the list is removed and returned. -- If the key does not exist or the list is empty, the command returns `nil`. +- When the `LPOP` command is executed, DiceDB checks if the key exists and is associated with a list. +- If the list has elements, the first element is removed and returned. +- If the key does not exist, the command treats it as an empty list and returns `nil`. - If the key exists but is not associated with a list, a `WRONGTYPE` error is returned. -- If more than one argument is passed, an error is returned. +- If more than one key is passed, an error is returned. ## Errors -1. `Wrong type of key` +1. `Wrong type of value or key`: -When the `LPOP` command is executed for a key that exists but is not associated with a list, an error is returned. This error occurs if the key is associated with a type other than a list, such as a string or set. + - Error Message: `(error) WRONGTYPE Operation against a key holding the wrong kind of value` + - Occurs if the key exists but is not associated with a list. ```bash 127.0.0.1:7379> LPOP mystring @@ -45,17 +51,8 @@ When the `LPOP` command is executed for a key that exists but is not associated 2. `Wrong number of arguments` -If the `LPOP` command is executed with more than one key or no key, the following error is returned: - -```bash -127.0.0.1:7379> LPOP -(error) ERR wrong number of arguments for 'lpop' command -``` - -```bash -127.0.0.1:7379> LPOP mylist secondlist -(error) ERR wrong number of arguments for 'lpop' command -``` + - Error Message: `(error) ERR wrong number of arguments for 'lpop' command` + - Occurs if command is executed without any arguments or with 2 or more arguments ## Example Usage @@ -85,32 +82,27 @@ LPOP mylist The list `mylist` now contains \["three"\]. -### Empty List or Non-Existent Key +### Non-Existent Key -Returns `(nil)` if the list is empty or the provided key is non-existent +Returns `(nil)` if the provided key is non-existent ```bash LPOP emptylist (nil) ``` -### Key is Not a List +### Invalid Usage: Key is Not a List -Setting a key `mystring` with the value `hello`: +Trying to `LPOP` a key `mystring` which is holding wrong data type `string`. ```bash SET mystring "hello" OK -``` - -Executing `LPOP` command on any key that is not associated with a List type will result in an error: - -```bash LPOP mystring (error) WRONGTYPE Operation against a key holding the wrong kind of value ``` -### Wrong Number of Arguments +### Invalid Usage: Wrong Number of Arguments Passing more than one key will result in an error: @@ -119,17 +111,10 @@ LPOP mylist secondlist (error) ERR wrong number of arguments for 'lpop' command ``` -### Notes - -- The key used with the `LPOP` command should be associated with a list to avoid any potential errors. -- Use the `EXISTS` command to check if a key exists before using `LPOP` to handle cases where the key might not exist. -- Consider using `LLEN` to check the length of the list if you need to handle empty lists differently. - -## Related Commands +## Best Practices -- `RPUSH`: Append one or multiple elements to the end of a list. -- `LPUSH`: Prepend one or multiple elements to the beginning of a list. -- `RPOP`: Remove and return the last element of a list. -- `LRANGE`: Get a range of elements from a list. +- `Check Key Type`: Before using `LPOP`, ensure that the key is associated with a list to avoid errors. +- `Handle Non-Existent Keys`: Be prepared to handle the case where the key does not exist, as `LPOP` will return `nil` in such scenarios. +- `Use in Conjunction with Other List Commands`: The `LPOP` command is often used alongside other list commands like `RPUSH`, `LPUSH`, `LLEN`, and `RPOP` to manage and process lists effectively. By understanding and using the `LPOP` command effectively, you can manage list data structures in DiceDB efficiently, implementing queue-like behaviors and more. diff --git a/docs/src/content/docs/commands/LPUSH.md b/docs/src/content/docs/commands/LPUSH.md index ef20ecc52..af28e7b43 100644 --- a/docs/src/content/docs/commands/LPUSH.md +++ b/docs/src/content/docs/commands/LPUSH.md @@ -16,7 +16,7 @@ LPUSH key value [value ...] | Parameter | Description | Type | Required | | ------------------ | ----------------------------------------------------------------------------------------- | ------ | -------- | | `key` | The name of the list where values are inserted. If it does not exist, it will be created. | String | Yes | -| `value [value...]` | One or more values to be inserted at the head of the list. | String | Yes | +| `value [value...]` | One or more space separated values to be inserted at the head of the list. | String | Yes | ## Return Value @@ -46,7 +46,7 @@ LPUSH key value [value ...] ## Example Usage -### Single Value Insertion +### Basic Usage Insert the value `world` at the head of the list stored at key `mylist`. If `mylist` does not exist, a new list is created. @@ -55,7 +55,7 @@ Insert the value `world` at the head of the list stored at key `mylist`. If `myl (integer) 1 ``` -### Multiple Values Insertion +### Inserting Multiple Values Insert the value `hello` and `world` at the head of the list stored at key `mylist`. After execution, `world` will be the first element, followed by `hello`. @@ -75,7 +75,7 @@ Create a new list with the key `newlist` and inserts the value `first` at the he (integer) 1 ``` -### Error Case - Wrong Type +### Invalid Usage: Key is Not a List Insert the value `value` at the head of the key `mystring`, which stores a string, not a list. @@ -86,9 +86,19 @@ OK (error) WRONGTYPE Operation against a key holding the wrong kind of value ``` -## Notes +### Invalid Usage: Wrong Number of Arguments -- The `LPUSH` command is often used in conjunction with the `RPUSH` command, which inserts values at the tail (right) of the list. +Calling `LPUSH` without passing any values: + +```bash +LPUSH mylist +(error) ERR wrong number of arguments for 'lpush' command +``` + +## Best Practices + +- `Check Key Type`: Before using `LPUSH`, ensure that the key is associated with a list to avoid errors. +- `Use in Conjunction with Other List Commands`: The `LPUSH` command is often used alongside other list commands like `RPUSH`, `LLEN`, `LPOP`, and `RPOP` to manage and process lists effectively. - The `LPUSH` command can be used to implement a stack (LIFO) by always pushing new elements to the head of the list. By understanding the `LPUSH` command, you can efficiently manage lists in DiceDB, ensuring that elements are added to the head of the list as needed. diff --git a/docs/src/content/docs/commands/RPOP.md b/docs/src/content/docs/commands/RPOP.md index a6fab6f2b..085a57a83 100644 --- a/docs/src/content/docs/commands/RPOP.md +++ b/docs/src/content/docs/commands/RPOP.md @@ -17,35 +17,39 @@ RPOP key | --------- | ---------------------------------------------------------------- | ------ | -------- | | `key` | The key of the list from which the last element will be removed. | String | Yes | + ## Return values -| Condition | Return Value | -| ------------------------------------------- | -------------------------------------------------------------------------- | -| The command is successful | The value of the last element in the list | -| The list is empty or the key does not exist | `nil` | -| The key is of the wrong type | Error: `WRONGTYPE Operation against a key holding the wrong kind of value` | +| Condition | Return Value | +| ---------------------------- | -------------------------------------------------- | +| The command is successful | `String` The value of the last element in the list | +| The key does not exist | `nil` | +| The key is of the wrong type | error | + ## Behaviour -- The `RPOP` command checks if the key exists and whether it contains a list. -- If the key does not exist, the command treats it as an empty list and returns `nil`. -- If the key exists but the list is empty, `nil` is returned. +- When the `RPOP` command is executed, DiceDB checks if the key exists and is associated with a list. - If the list has elements, the last element is removed and returned. -- If the key exists but is not of type list, an error is raised. +- If the key does not exist, the command treats it as an empty list and returns `nil`. +- If the key exists but is not associated with a list, a `WRONGTYPE` error is returned. +- If more than one key is passed, an error is returned. ## Errors -1. **Wrong type of value or key**: +1. `Wrong type of value or key`: - Error Message: `(error) WRONGTYPE Operation against a key holding the wrong kind of value` - - Occurs when attempting to run `RPOP` on a key that is not a list. + - Occurs if the key exists but is not associated with a list. + +2. `Wrong number of arguments` -2. **Non-existent or empty list**: - - Returns `nil` when the key does not exist or the list is empty. + - Error Message: `(error) ERR wrong number of arguments for 'lpop' command` + - Occurs if command is executed without any arguments or with 2 or more arguments ## Example Usage -### Example 1: Basic Usage +### Basic Usage ```bash 127.0.0.1:7379> LPUSH mylist "one" "two" "three" @@ -54,30 +58,38 @@ RPOP key "one" ``` -### Example 2: Empty List +### Non-Existent Key + +Returns `(nil)` if the provided key is non-existent ```bash 127.0.0.1:7379> RPOP emptylist (nil) ``` -### Example 3: Non-List Key +### Invalid Usage: Key is Not a List + +Trying to `LPOP` a key `mystring` which is holding wrong data type `string`. ```bash -127.0.0.1:7379> SET mystring "Hello" +SET mystring "hello" OK -127.0.0.1:7379> RPOP mystring +LPOP mystring (error) WRONGTYPE Operation against a key holding the wrong kind of value ``` -## Notes +### Invalid Usage: Wrong Number of Arguments -- The `RPOP` command is atomic, meaning it is safe to use in concurrent environments. -- If you need to remove and return the first element of the list, use the `LPOP` command instead. +Passing more than one key will result in an error: -## Related Commands +```bash +RPOP mylist secondlist +(error) ERR wrong number of arguments for 'lpop' command +``` -- `LPUSH`: Insert all the specified values at the head of the list stored at key. -- `LPOP`: Removes and returns the first element of the list stored at key. +## Best Practices +- `Check Key Type`: Before using `RPOP`, ensure that the key is associated with a list to avoid errors. +- `Handle Non-Existent Keys`: Be prepared to handle the case where the key does not exist, as `RPOP` will return `nil` in such scenarios. +- `Use in Conjunction with Other List Commands`: The `RPOP` command is often used alongside other list commands like `RPUSH`, `LPUSH`, `LLEN`, and `LPOP` to manage and process lists effectively. By understanding the `RPOP` command, you can effectively manage lists in DiceDB, ensuring that you can retrieve and process elements in a LIFO order. diff --git a/docs/src/content/docs/commands/RPUSH.md b/docs/src/content/docs/commands/RPUSH.md index cf9f5c69e..99513f554 100644 --- a/docs/src/content/docs/commands/RPUSH.md +++ b/docs/src/content/docs/commands/RPUSH.md @@ -13,17 +13,18 @@ RPUSH key value [value ...] ## Parameters -| Parameter | Description | Type | Required | -| --------- | ----------------------------------------------------------------------------------------------------------------- | ------ | -------- | -| `key` | The name of the list where the values will be inserted. If the list does not exist, a new list will be created. | String | Yes | -| `value` | One or more values to be inserted at the tail of the list. Multiple values can be specified, separated by spaces. | String | Yes | +| Parameter | Description | Type | Required | +| ------------------ | ----------------------------------------------------------------------------------------- | ------ | -------- | +| `key` | The name of the list where values are inserted. If it does not exist, it will be created. | String | Yes | +| `value [value...]` | One or more space separated values to be inserted at the tail of the list. | String | Yes | + ## Return Value -| Condition | Return Value | -| ------------------------------------------- | ------------ | -| Command is successful | integer | -| Syntax or specified constraints are invalid | error | +| Condition | Return Value | +| ------------------------------------------- | ---------------------------------------------- | +| Command is successful | `Integer` - length of the list after execution | +| Syntax or specified constraints are invalid | error | ## Behaviour @@ -34,14 +35,15 @@ RPUSH key value [value ...] ## Errors -1. `Non-List Key`: If the key exists but is not a list, DiceDB returns an error. +1. `Wrong Number of Arguments` - - Error Message: `WRONGTYPE Operation against a key holding the wrong kind of value` - - Occurs when trying to use the command on a key that does not contain a list. + - Error Message: `(error) ERR wrong number of arguments for 'rpush' command` + - Occurs if the key parameters is not provided or at least one value is not provided. -2. `Invalid Syntax`: If the command is not used with the correct syntax, DiceDB returns a syntax error. - - Error Message: `ERR wrong number of arguments for 'rpush' command` - - Occurs if the command's syntax is incorrect, like wrong number of arguments. +2. `Wrong Type of Key or Value`: + + - Error Message: `(error) WRONGTYPE Operation against a key holding the wrong kind of value` + - Occurs if the key exists and is not a list. DiceDB expects the key to either be non-existent or to hold a list data type. ## Example Usage @@ -60,22 +62,30 @@ Inserting multiple values `world`, `foo`, `bar` into `mylist` ```bash 127.0.0.1:7379> RPUSH mylist "world" "foo" "bar" -(integer) 4 +(integer) 3 ``` -### Invalid Usage +### Invalid Usage: Key is Not a List -Trying to insert value `val` into a non-list type `mykey` +Insert the value `value` at the tail of the key `mystring`, which stores a string, not a list. -```bash -127.0.0.1:7379> SET mykey "notalist" -127.0.0.1:7379> RPUSH mykey "val" -(error) ERROR WRONGTYPE Operation against a key holding the wrong kind of value +```shell +127.0.0.1:7379> SET mystring "not a list" +OK +127.0.0.1:7379> RPUSH mystring "value" +(error) WRONGTYPE Operation against a key holding the wrong kind of value ``` -Trying to run `rpush` with only 1 argument +### Invalid Usage: Wrong Number of Arguments + +Calling `RPUSH` without passing any values: ```bash -127.0.0.1:7379> RPUSH mylist -(error) ERROR wrong number of arguments +RPUSH mylist +(error) ERR wrong number of arguments for 'rpush' command ``` + +## Best Practices + +- `Check Key Type`: Before using `RPUSH`, ensure that the key is associated with a list to avoid errors. +- `Use in Conjunction with Other List Commands`: The `RPUSH` command is often used alongside other list commands like `LLEN`, `LPUSH`, `LPOP`, and `RPOP` to manage and process lists effectively. \ No newline at end of file diff --git a/integration_tests/commands/async/deque_test.go b/integration_tests/commands/async/deque_test.go deleted file mode 100644 index e7f8945cb..000000000 --- a/integration_tests/commands/async/deque_test.go +++ /dev/null @@ -1,504 +0,0 @@ -package async - -import ( - "fmt" - "math/rand" - "net" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -var deqRandGenerator *rand.Rand -var deqRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_!@#$%^&*()-=+[]\\;':,.<>/?~.|") - -var ( - deqNormalValues []string - deqEdgeValues []string -) - -func deqRandStr(n int) string { - b := make([]rune, n) - for i := range b { - b[i] = deqRunes[deqRandGenerator.Intn(len(deqRunes))] - } - return string(b) -} - -func deqTestInit() { - randSeed := time.Now().UnixNano() - deqRandGenerator = rand.New(rand.NewSource(randSeed)) - fmt.Printf("rand seed: %v", randSeed) - deqNormalValues = []string{ - deqRandStr(10), // 6 bit string - deqRandStr(256), // 12 bit string - deqRandStr((1 << 13) - 1000), // 32 bit string - "28", // 7 bit uint - "2024", // + 13 bit int - "-2024", // - 13 bit int - "15384", // + 16 bit int - "-15384", // - 16 bit int - "4193301", // + 24 bit int - "-4193301", // - 24 bit int - "1073731765", // + 32 bit int - "-1073731765", // - 32 bit int - "4611686018427287903", // + 64 bit int - "-4611686018427287903", // - 64 bit int - } - deqEdgeValues = []string{ - deqRandStr(1), // min 6 bit string - deqRandStr((1 << 6) - 1), // max 6 bit string - deqRandStr(1 << 6), // min 12 bit string - deqRandStr((1 << 12) - 1), // max 12 bit string - deqRandStr(1 << 12), // min 32 bit string - // randStr((1 << 32) - 1), // max 32 bit string, maybe too huge to test. - - "0", // min 7 bit uint - "127", // max 7 bit uint - "-4096", // min 13 bit int - "4095", // max 13 bit int - "-32768", // min 16 bit int - "32767", // max 16 bit int - "-8388608", // min 24 bit int - "8388607", // max 24 bit int - "-2147483648", // min 32 bit int - "2147483647", // max 32 bit int - "-9223372036854775808", // min 64 bit int - "9223372036854775807", // max 64 bit int - } -} - -func TestLPush(t *testing.T) { - deqTestInit() - conn := getLocalConnection() - defer conn.Close() - - testCases := []struct { - name string - cmds []string - expect []any - }{ - { - name: "LPUSH", - cmds: []string{"LPUSH k v", "LPUSH k v1 1 v2 2", "LPUSH k 3 3 3 v3 v3 v3"}, - expect: []any{int64(1), int64(5), int64(11)}, - }, - { - name: "LPUSH normal values", - cmds: []string{"LPUSH k " + strings.Join(deqNormalValues, " ")}, - expect: []any{int64(25)}, - }, - { - name: "LPUSH edge values", - cmds: []string{"LPUSH k " + strings.Join(deqEdgeValues, " ")}, - expect: []any{int64(42)}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - for i, cmd := range tc.cmds { - result := FireCommand(conn, cmd) - assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) - } - }) - } - - deqCleanUp(conn, "k") -} - -func TestRPush(t *testing.T) { - deqTestInit() - conn := getLocalConnection() - defer conn.Close() - - testCases := []struct { - name string - cmds []string - expect []any - }{ - { - name: "RPUSH", - cmds: []string{"RPUSH k v", "RPUSH k v1 1 v2 2", "RPUSH k 3 3 3 v3 v3 v3"}, - expect: []any{int64(1), int64(5), int64(11)}, - }, - { - name: "RPUSH normal values", - cmds: []string{"RPUSH k " + strings.Join(deqNormalValues, " ")}, - expect: []any{int64(25)}, - }, - { - name: "RPUSH edge values", - cmds: []string{"RPUSH k " + strings.Join(deqEdgeValues, " ")}, - expect: []any{int64(42)}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - for i, cmd := range tc.cmds { - result := FireCommand(conn, cmd) - assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) - } - }) - } - - deqCleanUp(conn, "k") -} - -func TestLPushLPop(t *testing.T) { - deqTestInit() - conn := getLocalConnection() - defer conn.Close() - - getPops := func(values []string) []string { - pops := make([]string, len(values)+1) - for i := 0; i < len(values)+1; i++ { - pops[i] = "LPOP k" - } - return pops - } - getPopExpects := func(values []string) []any { - expects := make([]any, len(values)) - for i := 0; i < len(values); i++ { - expects[i] = values[len(values)-1-i] - } - return expects - } - - testCases := []struct { - name string - cmds []string - expect []any - }{ - { - name: "LPUSH LPOP", - cmds: []string{"LPUSH k v1 1", "LPOP k", "LPOP k", "LPOP k"}, - expect: []any{int64(2), "1", "v1", "(nil)"}, - }, - { - name: "LPUSH LPOP normal values", - cmds: append([]string{"LPUSH k " + strings.Join(deqNormalValues, " ")}, getPops(deqNormalValues)...), - expect: append(append([]any{int64(14)}, getPopExpects(deqNormalValues)...), "(nil)"), - }, - { - name: "LPUSH LPOP edge values", - cmds: append([]string{"LPUSH k " + strings.Join(deqEdgeValues, " ")}, getPops(deqEdgeValues)...), - expect: append(append([]any{int64(17)}, getPopExpects(deqEdgeValues)...), "(nil)"), - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - for i, cmd := range tc.cmds { - result := FireCommand(conn, cmd) - assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) - } - }) - } - - deqCleanUp(conn, "k") -} - -func TestLPushRPop(t *testing.T) { - deqTestInit() - conn := getLocalConnection() - defer conn.Close() - - getPops := func(values []string) []string { - pops := make([]string, len(values)+1) - for i := 0; i < len(values)+1; i++ { - pops[i] = "RPOP k" - } - return pops - } - getPopExpects := func(values []string) []any { - expects := make([]any, len(values)) - for i := 0; i < len(values); i++ { - expects[i] = values[i] - } - return expects - } - - testCases := []struct { - name string - cmds []string - expect []any - }{ - { - name: "LPUSH RPOP", - cmds: []string{"LPUSH k v1 1", "RPOP k", "RPOP k", "RPOP k"}, - expect: []any{int64(2), "v1", "1", "(nil)"}, - }, - { - name: "LPUSH RPOP normal values", - cmds: append([]string{"LPUSH k " + strings.Join(deqNormalValues, " ")}, getPops(deqNormalValues)...), - expect: append(append([]any{int64(14)}, getPopExpects(deqNormalValues)...), "(nil)"), - }, - { - name: "LPUSH RPOP edge values", - cmds: append([]string{"LPUSH k " + strings.Join(deqEdgeValues, " ")}, getPops(deqEdgeValues)...), - expect: append(append([]any{int64(17)}, getPopExpects(deqEdgeValues)...), "(nil)"), - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - for i, cmd := range tc.cmds { - result := FireCommand(conn, cmd) - assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) - } - }) - } - - deqCleanUp(conn, "k") -} - -func TestRPushLPop(t *testing.T) { - deqTestInit() - conn := getLocalConnection() - defer conn.Close() - - getPops := func(values []string) []string { - pops := make([]string, len(values)+1) - for i := 0; i < len(values)+1; i++ { - pops[i] = "LPOP k" - } - return pops - } - getPopExpects := func(values []string) []any { - expects := make([]any, len(values)) - for i := 0; i < len(values); i++ { - expects[i] = values[i] - } - return expects - } - - testCases := []struct { - name string - cmds []string - expect []any - }{ - { - name: "RPUSH LPOP", - cmds: []string{"RPUSH k v1 1", "LPOP k", "LPOP k", "LPOP k"}, - expect: []any{int64(2), "v1", "1", "(nil)"}, - }, - { - name: "RPUSH LPOP normal values", - cmds: append([]string{"RPUSH k " + strings.Join(deqNormalValues, " ")}, getPops(deqNormalValues)...), - expect: append(append([]any{int64(14)}, getPopExpects(deqNormalValues)...), "(nil)"), - }, - { - name: "RPUSH LPOP edge values", - cmds: append([]string{"RPUSH k " + strings.Join(deqEdgeValues, " ")}, getPops(deqEdgeValues)...), - expect: append(append([]any{int64(17)}, getPopExpects(deqEdgeValues)...), "(nil)"), - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - for i, cmd := range tc.cmds { - result := FireCommand(conn, cmd) - assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) - } - }) - } - - deqCleanUp(conn, "k") -} - -func TestRPushRPop(t *testing.T) { - deqTestInit() - conn := getLocalConnection() - defer conn.Close() - - getPops := func(values []string) []string { - pops := make([]string, len(values)+1) - for i := 0; i < len(values)+1; i++ { - pops[i] = "RPOP k" - } - return pops - } - getPopExpects := func(values []string) []any { - expects := make([]any, len(values)) - for i := 0; i < len(values); i++ { - expects[i] = values[len(values)-1-i] - } - return expects - } - - testCases := []struct { - name string - cmds []string - expect []any - }{ - { - name: "RPUSH RPOP", - cmds: []string{"RPUSH k v1 1", "RPOP k", "RPOP k", "RPOP k"}, - expect: []any{int64(2), "1", "v1", "(nil)"}, - }, - { - name: "RPUSH RPOP normal values", - cmds: append([]string{"RPUSH k " + strings.Join(deqNormalValues, " ")}, getPops(deqNormalValues)...), - expect: append(append([]any{int64(14)}, getPopExpects(deqNormalValues)...), "(nil)"), - }, - { - name: "RPUSH RPOP edge values", - cmds: append([]string{"RPUSH k " + strings.Join(deqEdgeValues, " ")}, getPops(deqEdgeValues)...), - expect: append(append([]any{int64(17)}, getPopExpects(deqEdgeValues)...), "(nil)"), - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - for i, cmd := range tc.cmds { - result := FireCommand(conn, cmd) - assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) - } - }) - } - - deqCleanUp(conn, "k") -} - -func TestLRPushLRPop(t *testing.T) { - deqTestInit() - conn := getLocalConnection() - defer conn.Close() - - testCases := []struct { - name string - cmds []string - expect []any - }{ - { - name: "L/RPush L/RPop", - cmds: []string{ - "RPUSH k v1000 1000", "LPUSH k v2000 2000", - "RPOP k", "RPOP k", "LPOP k", - "LPUSH k v6", - "RPOP k", "LPOP k", "LPOP k", "RPOP k", - }, - expect: []any{ - int64(2), int64(4), - "1000", "v1000", "2000", - int64(2), - "v2000", "v6", "(nil)", "(nil)", - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - for i, cmd := range tc.cmds { - result := FireCommand(conn, cmd) - assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) - } - }) - } - - deqCleanUp(conn, "k") -} - -func TestLLEN(t *testing.T) { - deqTestInit() - conn := getLocalConnection() - defer conn.Close() - - testCases := []struct { - name string - cmds []string - expect []any - }{ - { - name: "L/RPush L/RPop", - cmds: []string{ - "RPUSH k v1000 1000", "LPUSH k v2000 2000", "LLEN k", - "RPOP k", "LLEN k", "RPOP k", "LPOP k", "LLEN k", - "LPUSH k v6", "LLEN k", - "RPOP k", "LLEN k", "LPOP k", "LPOP k", "RPOP k", "LLEN k", - }, - expect: []any{ - int64(2), int64(4), int64(4), - "1000", int64(3), "v1000", "2000", int64(1), - int64(2), int64(2), - "v2000", int64(1), "v6", "(nil)", "(nil)", int64(0), - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - for i, cmd := range tc.cmds { - result := FireCommand(conn, cmd) - assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) - } - }) - } - - deqCleanUp(conn, "k") -} - -func TestLPOPCount(t *testing.T) { - deqTestInit() - conn := getLocalConnection() - defer conn.Close() - - testCases := []struct { - name string - cmds []string - expect []interface{} - }{ - { - name: "LPOP with count argument - valid, invalid, and edge cases", - cmds: []string{ - "RPUSH k v1 v2 v3 v4", - "LPOP k 2", - "LLEN k", - "LPOP k 0", - "LLEN k", - "LPOP k 5", - "LLEN k", - "LPOP k -1", - "LPOP k abc", - "LLEN k", - }, - expect: []any{ - int64(4), - []interface{}{"v1", "v2"}, - int64(2), - []interface{}{}, - int64(2), - []interface{}{"v3", "v4"}, - int64(0), - "ERR value is out of range", - "ERR value is not an integer or out of range", - int64(0), - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - for i, cmd := range tc.cmds { - result := FireCommand(conn, cmd) - assert.Equal(t, tc.expect[i], result) - } - }) - } - - deqCleanUp(conn, "k") - -} - -func deqCleanUp(conn net.Conn, key string) { - for { - result := FireCommand(conn, "LPOP "+key) - if result == "(nil)" { - break - } - } -} diff --git a/integration_tests/commands/http/deque_test.go b/integration_tests/commands/http/deque_test.go index 605ce4fe7..96f48031f 100644 --- a/integration_tests/commands/http/deque_test.go +++ b/integration_tests/commands/http/deque_test.go @@ -103,7 +103,7 @@ func TestLPush(t *testing.T) { t.Run(tc.name, func(t *testing.T) { for i, cmd := range tc.commands { result, _ := exec.FireCommand(cmd) - assert.Equal(t, tc.expected[i], result) + assert.Equal(t, tc.expected[i], result, "Value mismatch for cmd %s", cmd) } }) } @@ -146,7 +146,7 @@ func TestRPush(t *testing.T) { t.Run(tc.name, func(t *testing.T) { for i, cmd := range tc.commands { result, _ := exec.FireCommand(cmd) - assert.Equal(t, tc.expected[i], result) + assert.Equal(t, tc.expected[i], result, "Value mismatch for cmd %s", cmd) } }) } @@ -560,6 +560,7 @@ func TestLLEN(t *testing.T) { func TestLPOPCount(t *testing.T) { deqTestInit() exec := NewHTTPCommandExecutor() + exec.FireCommand(HTTPCommand{Command: "FLUSHDB"}) testCases := []struct { name string @@ -586,8 +587,8 @@ func TestLPOPCount(t *testing.T) { float64(4), []interface{}{"v1", "v2"}, []interface{}{"v3", "v4"}, - "ERR value is out of range", "ERR value is not an integer or out of range", + "ERR value is not an integer or a float", float64(0), }, }, @@ -602,4 +603,6 @@ func TestLPOPCount(t *testing.T) { } }) } + + exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"keys": [...]string{"k"}}}) } diff --git a/integration_tests/commands/resp/deque_test.go b/integration_tests/commands/resp/deque_test.go index e022e8b5d..7dc77f3f0 100644 --- a/integration_tests/commands/resp/deque_test.go +++ b/integration_tests/commands/resp/deque_test.go @@ -1,12 +1,447 @@ package resp import ( + "fmt" + "math/rand" "net" + "strings" "testing" + "time" "github.com/stretchr/testify/assert" ) +var deqRandGenerator *rand.Rand +var deqRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_!@#$%^&*()-=+[]\\;':,.<>/?~.|") + +var ( + deqNormalValues []string + deqEdgeValues []string +) + +func deqRandStr(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = deqRunes[deqRandGenerator.Intn(len(deqRunes))] + } + return string(b) +} + +func deqTestInit() { + randSeed := time.Now().UnixNano() + deqRandGenerator = rand.New(rand.NewSource(randSeed)) + fmt.Printf("rand seed: %v", randSeed) + deqNormalValues = []string{ + deqRandStr(10), // 6 bit string + deqRandStr(256), // 12 bit string + deqRandStr((1 << 13) - 1000), // 32 bit string + "28", // 7 bit uint + "2024", // + 13 bit int + "-2024", // - 13 bit int + "15384", // + 16 bit int + "-15384", // - 16 bit int + "4193301", // + 24 bit int + "-4193301", // - 24 bit int + "1073731765", // + 32 bit int + "-1073731765", // - 32 bit int + "4611686018427287903", // + 64 bit int + "-4611686018427287903", // - 64 bit int + } + deqEdgeValues = []string{ + deqRandStr(1), // min 6 bit string + deqRandStr((1 << 6) - 1), // max 6 bit string + deqRandStr(1 << 6), // min 12 bit string + deqRandStr((1 << 12) - 1), // max 12 bit string + deqRandStr(1 << 12), // min 32 bit string + // randStr((1 << 32) - 1), // max 32 bit string, maybe too huge to test. + + "0", // min 7 bit uint + "127", // max 7 bit uint + "-4096", // min 13 bit int + "4095", // max 13 bit int + "-32768", // min 16 bit int + "32767", // max 16 bit int + "-8388608", // min 24 bit int + "8388607", // max 24 bit int + "-2147483648", // min 32 bit int + "2147483647", // max 32 bit int + "-9223372036854775808", // min 64 bit int + "9223372036854775807", // max 64 bit int + } +} + +func TestLPush(t *testing.T) { + deqTestInit() + conn := getLocalConnection() + defer conn.Close() + + testCases := []struct { + name string + cmds []string + expect []any + }{ + { + name: "LPUSH", + cmds: []string{"LPUSH k v", "LPUSH k v1 1 v2 2", "LPUSH k 3 3 3 v3 v3 v3"}, + expect: []any{int64(1), int64(5), int64(11)}, + }, + { + name: "LPUSH normal values", + cmds: []string{"LPUSH k " + strings.Join(deqNormalValues, " ")}, + expect: []any{int64(25)}, + }, + { + name: "LPUSH edge values", + cmds: []string{"LPUSH k " + strings.Join(deqEdgeValues, " ")}, + expect: []any{int64(42)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for i, cmd := range tc.cmds { + result := FireCommand(conn, cmd) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } + + deqCleanUp(conn, "k") +} + +func TestRPush(t *testing.T) { + deqTestInit() + conn := getLocalConnection() + defer conn.Close() + + testCases := []struct { + name string + cmds []string + expect []any + }{ + { + name: "RPUSH", + cmds: []string{"RPUSH k v", "RPUSH k v1 1 v2 2", "RPUSH k 3 3 3 v3 v3 v3"}, + expect: []any{int64(1), int64(5), int64(11)}, + }, + { + name: "RPUSH normal values", + cmds: []string{"RPUSH k " + strings.Join(deqNormalValues, " ")}, + expect: []any{int64(25)}, + }, + { + name: "RPUSH edge values", + cmds: []string{"RPUSH k " + strings.Join(deqEdgeValues, " ")}, + expect: []any{int64(42)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for i, cmd := range tc.cmds { + result := FireCommand(conn, cmd) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } + + deqCleanUp(conn, "k") +} + +func TestLPushLPop(t *testing.T) { + deqTestInit() + conn := getLocalConnection() + defer conn.Close() + + getPops := func(values []string) []string { + pops := make([]string, len(values)+1) + for i := 0; i < len(values)+1; i++ { + pops[i] = "LPOP k" + } + return pops + } + getPopExpects := func(values []string) []any { + expects := make([]any, len(values)) + for i := 0; i < len(values); i++ { + expects[i] = values[len(values)-1-i] + } + return expects + } + + testCases := []struct { + name string + cmds []string + expect []any + }{ + { + name: "LPUSH LPOP", + cmds: []string{"LPUSH k v1 1", "LPOP k", "LPOP k", "LPOP k"}, + expect: []any{int64(2), "1", "v1", "(nil)"}, + }, + { + name: "LPUSH LPOP normal values", + cmds: append([]string{"LPUSH k " + strings.Join(deqNormalValues, " ")}, getPops(deqNormalValues)...), + expect: append(append([]any{int64(14)}, getPopExpects(deqNormalValues)...), "(nil)"), + }, + { + name: "LPUSH LPOP edge values", + cmds: append([]string{"LPUSH k " + strings.Join(deqEdgeValues, " ")}, getPops(deqEdgeValues)...), + expect: append(append([]any{int64(17)}, getPopExpects(deqEdgeValues)...), "(nil)"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for i, cmd := range tc.cmds { + result := FireCommand(conn, cmd) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } + + deqCleanUp(conn, "k") +} + +func TestLPushRPop(t *testing.T) { + deqTestInit() + conn := getLocalConnection() + defer conn.Close() + + getPops := func(values []string) []string { + pops := make([]string, len(values)+1) + for i := 0; i < len(values)+1; i++ { + pops[i] = "RPOP k" + } + return pops + } + getPopExpects := func(values []string) []any { + expects := make([]any, len(values)) + for i := 0; i < len(values); i++ { + expects[i] = values[i] + } + return expects + } + + testCases := []struct { + name string + cmds []string + expect []any + }{ + { + name: "LPUSH RPOP", + cmds: []string{"LPUSH k v1 1", "RPOP k", "RPOP k", "RPOP k"}, + expect: []any{int64(2), "v1", "1", "(nil)"}, + }, + { + name: "LPUSH RPOP normal values", + cmds: append([]string{"LPUSH k " + strings.Join(deqNormalValues, " ")}, getPops(deqNormalValues)...), + expect: append(append([]any{int64(14)}, getPopExpects(deqNormalValues)...), "(nil)"), + }, + { + name: "LPUSH RPOP edge values", + cmds: append([]string{"LPUSH k " + strings.Join(deqEdgeValues, " ")}, getPops(deqEdgeValues)...), + expect: append(append([]any{int64(17)}, getPopExpects(deqEdgeValues)...), "(nil)"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for i, cmd := range tc.cmds { + result := FireCommand(conn, cmd) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } + + deqCleanUp(conn, "k") +} + +func TestRPushLPop(t *testing.T) { + deqTestInit() + conn := getLocalConnection() + defer conn.Close() + + getPops := func(values []string) []string { + pops := make([]string, len(values)+1) + for i := 0; i < len(values)+1; i++ { + pops[i] = "LPOP k" + } + return pops + } + getPopExpects := func(values []string) []any { + expects := make([]any, len(values)) + for i := 0; i < len(values); i++ { + expects[i] = values[i] + } + return expects + } + + testCases := []struct { + name string + cmds []string + expect []any + }{ + { + name: "RPUSH LPOP", + cmds: []string{"RPUSH k v1 1", "LPOP k", "LPOP k", "LPOP k"}, + expect: []any{int64(2), "v1", "1", "(nil)"}, + }, + { + name: "RPUSH LPOP normal values", + cmds: append([]string{"RPUSH k " + strings.Join(deqNormalValues, " ")}, getPops(deqNormalValues)...), + expect: append(append([]any{int64(14)}, getPopExpects(deqNormalValues)...), "(nil)"), + }, + { + name: "RPUSH LPOP edge values", + cmds: append([]string{"RPUSH k " + strings.Join(deqEdgeValues, " ")}, getPops(deqEdgeValues)...), + expect: append(append([]any{int64(17)}, getPopExpects(deqEdgeValues)...), "(nil)"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for i, cmd := range tc.cmds { + result := FireCommand(conn, cmd) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } + + deqCleanUp(conn, "k") +} + +func TestRPushRPop(t *testing.T) { + deqTestInit() + conn := getLocalConnection() + defer conn.Close() + + getPops := func(values []string) []string { + pops := make([]string, len(values)+1) + for i := 0; i < len(values)+1; i++ { + pops[i] = "RPOP k" + } + return pops + } + getPopExpects := func(values []string) []any { + expects := make([]any, len(values)) + for i := 0; i < len(values); i++ { + expects[i] = values[len(values)-1-i] + } + return expects + } + + testCases := []struct { + name string + cmds []string + expect []any + }{ + { + name: "RPUSH RPOP", + cmds: []string{"RPUSH k v1 1", "RPOP k", "RPOP k", "RPOP k"}, + expect: []any{int64(2), "1", "v1", "(nil)"}, + }, + { + name: "RPUSH RPOP normal values", + cmds: append([]string{"RPUSH k " + strings.Join(deqNormalValues, " ")}, getPops(deqNormalValues)...), + expect: append(append([]any{int64(14)}, getPopExpects(deqNormalValues)...), "(nil)"), + }, + { + name: "RPUSH RPOP edge values", + cmds: append([]string{"RPUSH k " + strings.Join(deqEdgeValues, " ")}, getPops(deqEdgeValues)...), + expect: append(append([]any{int64(17)}, getPopExpects(deqEdgeValues)...), "(nil)"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for i, cmd := range tc.cmds { + result := FireCommand(conn, cmd) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } + + deqCleanUp(conn, "k") +} + +func TestLRPushLRPop(t *testing.T) { + deqTestInit() + conn := getLocalConnection() + defer conn.Close() + + testCases := []struct { + name string + cmds []string + expect []any + }{ + { + name: "L/RPush L/RPop", + cmds: []string{ + "RPUSH k v1000 1000", "LPUSH k v2000 2000", + "RPOP k", "RPOP k", "LPOP k", + "LPUSH k v6", + "RPOP k", "LPOP k", "LPOP k", "RPOP k", + }, + expect: []any{ + int64(2), int64(4), + "1000", "v1000", "2000", + int64(2), + "v2000", "v6", "(nil)", "(nil)", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for i, cmd := range tc.cmds { + result := FireCommand(conn, cmd) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } + + deqCleanUp(conn, "k") +} + +func TestLLEN(t *testing.T) { + deqTestInit() + conn := getLocalConnection() + defer conn.Close() + + testCases := []struct { + name string + cmds []string + expect []any + }{ + { + name: "L/RPush L/RPop", + cmds: []string{ + "RPUSH k v1000 1000", "LPUSH k v2000 2000", "LLEN k", + "RPOP k", "LLEN k", "RPOP k", "LPOP k", "LLEN k", + "LPUSH k v6", "LLEN k", + "RPOP k", "LLEN k", "LPOP k", "LPOP k", "RPOP k", "LLEN k", + }, + expect: []any{ + int64(2), int64(4), int64(4), + "1000", int64(3), "v1000", "2000", int64(1), + int64(2), int64(2), + "v2000", int64(1), "v6", "(nil)", "(nil)", int64(0), + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for i, cmd := range tc.cmds { + result := FireCommand(conn, cmd) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } + + deqCleanUp(conn, "k") +} + func TestLInsert(t *testing.T) { conn := getLocalConnection() defer conn.Close() @@ -95,6 +530,58 @@ func TestLRange(t *testing.T) { deqCleanUp(conn, "k") } +func TestLPOPCount(t *testing.T) { + deqTestInit() + conn := getLocalConnection() + defer conn.Close() + + testCases := []struct { + name string + cmds []string + expect []interface{} + }{ + { + name: "LPOP with count argument - valid, invalid, and edge cases", + cmds: []string{ + "RPUSH k v1 v2 v3 v4", + "LPOP k 2", + "LLEN k", + "LPOP k 0", + "LLEN k", + "LPOP k 5", + "LLEN k", + "LPOP k -1", + "LPOP k abc", + "LLEN k", + }, + expect: []any{ + int64(4), + []interface{}{"v1", "v2"}, + int64(2), + []interface{}{}, + int64(2), + []interface{}{"v3", "v4"}, + int64(0), + "ERR value is out of range", + "ERR value is not an integer or out of range", + int64(0), + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for i, cmd := range tc.cmds { + result := FireCommand(conn, cmd) + assert.Equal(t, tc.expect[i], result) + } + }) + } + + deqCleanUp(conn, "k") + +} + func deqCleanUp(conn net.Conn, key string) { for { result := FireCommand(conn, "LPOP "+key) diff --git a/integration_tests/commands/websocket/deque_test.go b/integration_tests/commands/websocket/deque_test.go index 741a48145..1aa8e84e6 100644 --- a/integration_tests/commands/websocket/deque_test.go +++ b/integration_tests/commands/websocket/deque_test.go @@ -1,11 +1,462 @@ package websocket import ( + "fmt" + "math/rand" + "strings" "testing" + "time" - "github.com/stretchr/testify/assert" + "gotest.tools/v3/assert" ) +var deqRandGenerator *rand.Rand +var deqRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_!@#$%^&*()-=+[]\\;':,.<>/?~.|") + +var ( + deqNormalValues []string + deqEdgeValues []string +) + +func deqRandStr(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = deqRunes[deqRandGenerator.Intn(len(deqRunes))] + } + return string(b) +} + +func deqTestInit() { + randSeed := time.Now().UnixNano() + deqRandGenerator = rand.New(rand.NewSource(randSeed)) + fmt.Printf("rand seed: %v", randSeed) + deqNormalValues = []string{ + deqRandStr(10), // 6 bit string + deqRandStr(256), // 12 bit string + deqRandStr((1 << 13) - 1000), // 32 bit string + "28", // 7 bit uint + "2024", // + 13 bit int + "-2024", // - 13 bit int + "15384", // + 16 bit int + "-15384", // - 16 bit int + "4193301", // + 24 bit int + "-4193301", // - 24 bit int + "1073731765", // + 32 bit int + "-1073731765", // - 32 bit int + "4611686018427287903", // + 64 bit int + "-4611686018427287903", // - 64 bit int + } + deqEdgeValues = []string{ + deqRandStr(1), // min 6 bit string + deqRandStr((1 << 6) - 1), // max 6 bit string + deqRandStr(1 << 6), // min 12 bit string + deqRandStr((1 << 12) - 1), // max 12 bit string + deqRandStr(1 << 12), // min 32 bit string + // randStr((1 << 32) - 1), // max 32 bit string, maybe too huge to test. + + "0", // min 7 bit uint + "127", // max 7 bit uint + "-4096", // min 13 bit int + "4095", // max 13 bit int + "-32768", // min 16 bit int + "32767", // max 16 bit int + "-8388608", // min 24 bit int + "8388607", // max 24 bit int + "-2147483648", // min 32 bit int + "2147483647", // max 32 bit int + "-9223372036854775808", // min 64 bit int + "9223372036854775807", // max 64 bit int + } +} + +func TestLPush(t *testing.T) { + deqTestInit() + exec := NewWebsocketCommandExecutor() + + testCases := []struct { + name string + cmds []string + expect []any + }{ + { + name: "LPUSH", + cmds: []string{"LPUSH k v", "LPUSH k v1 1 v2 2", "LPUSH k 3 3 3 v3 v3 v3"}, + expect: []any{float64(1), float64(5), float64(11)}, + }, + { + name: "LPUSH normal values", + cmds: []string{"LPUSH k " + strings.Join(deqNormalValues, " ")}, + expect: []any{float64(25)}, + }, + { + name: "LPUSH edge values", + cmds: []string{"LPUSH k " + strings.Join(deqEdgeValues, " ")}, + expect: []any{float64(42)}, + }, + } + + conn := exec.ConnectToServer() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + for i, cmd := range tc.cmds { + result, err := exec.FireCommandAndReadResponse(conn, cmd) + assert.NilError(t, err) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } + + DeleteKey(t, conn, exec, "k") +} + +func TestRPush(t *testing.T) { + deqTestInit() + exec := NewWebsocketCommandExecutor() + + testCases := []struct { + name string + cmds []string + expect []any + }{ + { + name: "RPUSH", + cmds: []string{"RPUSH k v", "RPUSH k v1 1 v2 2", "RPUSH k 3 3 3 v3 v3 v3"}, + expect: []any{float64(1), float64(5), float64(11)}, + }, + { + name: "RPUSH normal values", + cmds: []string{"RPUSH k " + strings.Join(deqNormalValues, " ")}, + expect: []any{float64(25)}, + }, + { + name: "RPUSH edge values", + cmds: []string{"RPUSH k " + strings.Join(deqEdgeValues, " ")}, + expect: []any{float64(42)}, + }, + } + + conn := exec.ConnectToServer() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + for i, cmd := range tc.cmds { + result, err := exec.FireCommandAndReadResponse(conn, cmd) + assert.NilError(t, err) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } + + DeleteKey(t, conn, exec, "k") +} + +func TestLPushLPop(t *testing.T) { + deqTestInit() + exec := NewWebsocketCommandExecutor() + + getPops := func(values []string) []string { + pops := make([]string, len(values)+1) + for i := 0; i < len(values)+1; i++ { + pops[i] = "LPOP k" + } + return pops + } + getPopExpects := func(values []string) []any { + expects := make([]any, len(values)) + for i := 0; i < len(values); i++ { + expects[i] = values[len(values)-1-i] + } + return expects + } + + testCases := []struct { + name string + cmds []string + expect []any + }{ + { + name: "LPUSH LPOP", + cmds: []string{"LPUSH k v1 1", "LPOP k", "LPOP k", "LPOP k"}, + expect: []any{float64(2), "1", "v1", nil}, + }, + { + name: "LPUSH LPOP normal values", + cmds: append([]string{"LPUSH k " + strings.Join(deqNormalValues, " ")}, getPops(deqNormalValues)...), + expect: append(append([]any{float64(14)}, getPopExpects(deqNormalValues)...), nil), + }, + { + name: "LPUSH LPOP edge values", + cmds: append([]string{"LPUSH k " + strings.Join(deqEdgeValues, " ")}, getPops(deqEdgeValues)...), + expect: append(append([]any{float64(17)}, getPopExpects(deqEdgeValues)...), nil), + }, + } + + conn := exec.ConnectToServer() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + for i, cmd := range tc.cmds { + result, err := exec.FireCommandAndReadResponse(conn, cmd) + assert.NilError(t, err) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } + + DeleteKey(t, conn, exec, "k") +} + +func TestLPushRPop(t *testing.T) { + deqTestInit() + exec := NewWebsocketCommandExecutor() + + getPops := func(values []string) []string { + pops := make([]string, len(values)+1) + for i := 0; i < len(values)+1; i++ { + pops[i] = "RPOP k" + } + return pops + } + getPopExpects := func(values []string) []any { + expects := make([]any, len(values)) + for i := 0; i < len(values); i++ { + expects[i] = values[i] + } + return expects + } + + testCases := []struct { + name string + cmds []string + expect []any + }{ + { + name: "LPUSH RPOP", + cmds: []string{"LPUSH k v1 1", "RPOP k", "RPOP k", "RPOP k"}, + expect: []any{float64(2), "v1", "1", nil}, + }, + { + name: "LPUSH RPOP normal values", + cmds: append([]string{"LPUSH k " + strings.Join(deqNormalValues, " ")}, getPops(deqNormalValues)...), + expect: append(append([]any{float64(14)}, getPopExpects(deqNormalValues)...), nil), + }, + { + name: "LPUSH RPOP edge values", + cmds: append([]string{"LPUSH k " + strings.Join(deqEdgeValues, " ")}, getPops(deqEdgeValues)...), + expect: append(append([]any{float64(17)}, getPopExpects(deqEdgeValues)...), nil), + }, + } + + conn := exec.ConnectToServer() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + for i, cmd := range tc.cmds { + result, err := exec.FireCommandAndReadResponse(conn, cmd) + assert.NilError(t, err) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } + + DeleteKey(t, conn, exec, "k") +} + +func TestRPushLPop(t *testing.T) { + deqTestInit() + exec := NewWebsocketCommandExecutor() + + getPops := func(values []string) []string { + pops := make([]string, len(values)+1) + for i := 0; i < len(values)+1; i++ { + pops[i] = "LPOP k" + } + return pops + } + getPopExpects := func(values []string) []any { + expects := make([]any, len(values)) + for i := 0; i < len(values); i++ { + expects[i] = values[i] + } + return expects + } + + testCases := []struct { + name string + cmds []string + expect []any + }{ + { + name: "RPUSH LPOP", + cmds: []string{"RPUSH k v1 1", "LPOP k", "LPOP k", "LPOP k"}, + expect: []any{float64(2), "v1", "1", nil}, + }, + { + name: "RPUSH LPOP normal values", + cmds: append([]string{"RPUSH k " + strings.Join(deqNormalValues, " ")}, getPops(deqNormalValues)...), + expect: append(append([]any{float64(14)}, getPopExpects(deqNormalValues)...), nil), + }, + { + name: "RPUSH LPOP edge values", + cmds: append([]string{"RPUSH k " + strings.Join(deqEdgeValues, " ")}, getPops(deqEdgeValues)...), + expect: append(append([]any{float64(17)}, getPopExpects(deqEdgeValues)...), nil), + }, + } + + conn := exec.ConnectToServer() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + for i, cmd := range tc.cmds { + result, err := exec.FireCommandAndReadResponse(conn, cmd) + assert.NilError(t, err) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } + + DeleteKey(t, conn, exec, "k") +} + +func TestRPushRPop(t *testing.T) { + deqTestInit() + exec := NewWebsocketCommandExecutor() + + getPops := func(values []string) []string { + pops := make([]string, len(values)+1) + for i := 0; i < len(values)+1; i++ { + pops[i] = "RPOP k" + } + return pops + } + getPopExpects := func(values []string) []any { + expects := make([]any, len(values)) + for i := 0; i < len(values); i++ { + expects[i] = values[len(values)-1-i] + } + return expects + } + + testCases := []struct { + name string + cmds []string + expect []any + }{ + { + name: "RPUSH RPOP", + cmds: []string{"RPUSH k v1 1", "RPOP k", "RPOP k", "RPOP k"}, + expect: []any{float64(2), "1", "v1", nil}, + }, + { + name: "RPUSH RPOP normal values", + cmds: append([]string{"RPUSH k " + strings.Join(deqNormalValues, " ")}, getPops(deqNormalValues)...), + expect: append(append([]any{float64(14)}, getPopExpects(deqNormalValues)...), nil), + }, + { + name: "RPUSH RPOP edge values", + cmds: append([]string{"RPUSH k " + strings.Join(deqEdgeValues, " ")}, getPops(deqEdgeValues)...), + expect: append(append([]any{float64(17)}, getPopExpects(deqEdgeValues)...), nil), + }, + } + + conn := exec.ConnectToServer() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + for i, cmd := range tc.cmds { + result, err := exec.FireCommandAndReadResponse(conn, cmd) + assert.NilError(t, err) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } + + DeleteKey(t, conn, exec, "k") +} + +func TestLRPushLRPop(t *testing.T) { + deqTestInit() + exec := NewWebsocketCommandExecutor() + + testCases := []struct { + name string + cmds []string + expect []any + }{ + { + name: "L/RPush L/RPop", + cmds: []string{ + "RPUSH k v1000 1000", "LPUSH k v2000 2000", + "RPOP k", "RPOP k", "LPOP k", + "LPUSH k v6", + "RPOP k", "LPOP k", "LPOP k", "RPOP k", + }, + expect: []any{ + float64(2), float64(4), + "1000", "v1000", "2000", + float64(2), + "v2000", "v6", nil, nil, + }, + }, + } + + conn := exec.ConnectToServer() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + for i, cmd := range tc.cmds { + result, err := exec.FireCommandAndReadResponse(conn, cmd) + assert.NilError(t, err) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } + + DeleteKey(t, conn, exec, "k") +} + +func TestLLEN(t *testing.T) { + deqTestInit() + exec := NewWebsocketCommandExecutor() + + testCases := []struct { + name string + cmds []string + expect []any + }{ + { + name: "L/RPush L/RPop", + cmds: []string{ + "RPUSH k v1000 1000", "LPUSH k v2000 2000", "LLEN k", + "RPOP k", "LLEN k", "RPOP k", "LPOP k", "LLEN k", + "LPUSH k v6", "LLEN k", + "RPOP k", "LLEN k", "LPOP k", "LPOP k", "RPOP k", "LLEN k", + }, + expect: []any{ + float64(2), float64(4), float64(4), + "1000", float64(3), "v1000", "2000", float64(1), + float64(2), float64(2), + "v2000", float64(1), "v6", nil, nil, float64(0), + }, + }, + } + + conn := exec.ConnectToServer() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + for i, cmd := range tc.cmds { + result, err := exec.FireCommandAndReadResponse(conn, cmd) + assert.NilError(t, err) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } + + DeleteKey(t, conn, exec, "k") +} + func TestLPOPCount(t *testing.T) { exec := NewWebsocketCommandExecutor() @@ -28,15 +479,15 @@ func TestLPOPCount(t *testing.T) { "LPOP k abc", "LLEN k", }, - expected: []any{ + expected: []interface{}{ float64(1), float64(2), float64(3), float64(4), []interface{}{"v1", "v2"}, []interface{}{"v3", "v4"}, - "ERR value is out of range", "ERR value is not an integer or out of range", + "ERR value is not an integer or a float", float64(0), }, cleanupKey: "k", @@ -48,8 +499,8 @@ func TestLPOPCount(t *testing.T) { for i, cmd := range tc.commands { result, err := exec.FireCommandAndReadResponse(conn, cmd) - assert.Nil(t, err) - assert.Equal(t, tc.expected[i], result) + assert.NilError(t, err) + assert.DeepEqual(t, tc.expected[i], result) } DeleteKey(t, conn, exec, tc.cleanupKey) }) diff --git a/internal/eval/commands.go b/internal/eval/commands.go index 87e1d284b..f8df197a2 100644 --- a/internal/eval/commands.go +++ b/internal/eval/commands.go @@ -850,28 +850,32 @@ var ( KeySpecs: KeySpecs{BeginIndex: 1}, } lpushCmdMeta = DiceCmdMeta{ - Name: "LPUSH", - Info: "LPUSH pushes values into the left side of the deque", - Eval: evalLPUSH, - Arity: -3, + Name: "LPUSH", + Info: "LPUSH pushes values into the left side of the deque", + NewEval: evalLPUSH, + IsMigrated: true, + Arity: -3, } rpushCmdMeta = DiceCmdMeta{ - Name: "RPUSH", - Info: "RPUSH pushes values into the right side of the deque", - Eval: evalRPUSH, - Arity: -3, + Name: "RPUSH", + Info: "RPUSH pushes values into the right side of the deque", + NewEval: evalRPUSH, + IsMigrated: true, + Arity: -3, } lpopCmdMeta = DiceCmdMeta{ - Name: "LPOP", - Info: "LPOP pops a value from the left side of the deque", - Eval: evalLPOP, - Arity: 2, + Name: "LPOP", + Info: "LPOP pops a value from the left side of the deque", + NewEval: evalLPOP, + IsMigrated: true, + Arity: 2, } rpopCmdMeta = DiceCmdMeta{ - Name: "RPOP", - Info: "RPOP pops a value from the right side of the deque", - Eval: evalRPOP, - Arity: 2, + Name: "RPOP", + Info: "RPOP pops a value from the right side of the deque", + NewEval: evalRPOP, + IsMigrated: true, + Arity: 2, } llenCmdMeta = DiceCmdMeta{ Name: "LLEN", @@ -879,8 +883,9 @@ var ( Returns the length of the list stored at key. If key does not exist, it is interpreted as an empty list and 0 is returned. An error is returned when the value stored at key is not a list.`, - Eval: evalLLEN, - Arity: 1, + NewEval: evalLLEN, + IsMigrated: true, + Arity: 1, } dbSizeCmdMeta = DiceCmdMeta{ Name: "DBSIZE", diff --git a/internal/eval/eval.go b/internal/eval/eval.go index dba9ceb75..d079fcea3 100644 --- a/internal/eval/eval.go +++ b/internal/eval/eval.go @@ -1,4 +1,4 @@ -package eval + package eval import ( "bytes" @@ -1505,195 +1505,6 @@ func evalTOUCH(args []string, store *dstore.Store) []byte { return clientio.Encode(count, false) } -func evalLPUSH(args []string, store *dstore.Store) []byte { - if len(args) < 2 { - return diceerrors.NewErrArity("LPUSH") - } - - obj := store.Get(args[0]) - if obj == nil { - obj = store.NewObj(NewDeque(), -1, object.ObjTypeByteList, object.ObjEncodingDeque) - } - - // if object is a set type, return error - if object.AssertType(obj.TypeEncoding, object.ObjTypeSet) == nil { - return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr) - } - - if err := object.AssertType(obj.TypeEncoding, object.ObjTypeByteList); err != nil { - return clientio.Encode(err, false) - } - - if err := object.AssertEncoding(obj.TypeEncoding, object.ObjEncodingDeque); err != nil { - return clientio.Encode(err, false) - } - - store.Put(args[0], obj) - for i := 1; i < len(args); i++ { - obj.Value.(*Deque).LPush(args[i]) - } - - deq := obj.Value.(*Deque) - - return clientio.Encode(deq.Length, false) -} - -func evalRPUSH(args []string, store *dstore.Store) []byte { - if len(args) < 2 { - return diceerrors.NewErrArity("RPUSH") - } - - obj := store.Get(args[0]) - if obj == nil { - obj = store.NewObj(NewDeque(), -1, object.ObjTypeByteList, object.ObjEncodingDeque) - } - - // if object is a set type, return error - if object.AssertType(obj.TypeEncoding, object.ObjTypeSet) == nil { - return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr) - } - - if err := object.AssertType(obj.TypeEncoding, object.ObjTypeByteList); err != nil { - return clientio.Encode(err, false) - } - - if err := object.AssertEncoding(obj.TypeEncoding, object.ObjEncodingDeque); err != nil { - return clientio.Encode(err, false) - } - - store.Put(args[0], obj) - for i := 1; i < len(args); i++ { - obj.Value.(*Deque).RPush(args[i]) - } - - deq := obj.Value.(*Deque) - - return clientio.Encode(deq.Length, false) -} - -func evalRPOP(args []string, store *dstore.Store) []byte { - if len(args) != 1 { - return diceerrors.NewErrArity("RPOP") - } - - obj := store.Get(args[0]) - if obj == nil { - return clientio.RespNIL - } - - // if object is a set type, return error - if object.AssertType(obj.TypeEncoding, object.ObjTypeSet) == nil { - return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr) - } - - if err := object.AssertType(obj.TypeEncoding, object.ObjTypeByteList); err != nil { - return clientio.Encode(err, false) - } - - if err := object.AssertEncoding(obj.TypeEncoding, object.ObjEncodingDeque); err != nil { - return clientio.Encode(err, false) - } - - deq := obj.Value.(*Deque) - x, err := deq.RPop() - if err != nil { - if errors.Is(err, ErrDequeEmpty) { - return clientio.RespNIL - } - panic(fmt.Sprintf("unknown error: %v", err)) - } - - return clientio.Encode(x, false) -} - -func evalLPOP(args []string, store *dstore.Store) []byte { - // By default we pop only 1 - popNumber := 1 - - // LPOP accepts 1 or 2 arguments only - LPOP key [count] - if len(args) < 1 || len(args) > 2 { - return diceerrors.NewErrArity("LPOP") - } - - // to updated the number of pops - if len(args) == 2 { - nos, err := strconv.Atoi(args[1]) - if err != nil { - return diceerrors.NewErrWithFormattedMessage(diceerrors.IntOrOutOfRangeErr) - } - if nos == 0 { - // returns empty string if count given is 0 - return clientio.Encode([]string{}, false) - } - if nos < 0 { - // returns an out of range err if count is negetive - return diceerrors.NewErrWithFormattedMessage(diceerrors.ValOutOfRangeErr) - } - popNumber = nos - } - - obj := store.Get(args[0]) - if obj == nil { - return clientio.RespNIL - } - - // if object is a set type, return error - if object.AssertType(obj.TypeEncoding, object.ObjTypeSet) == nil { - return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr) - } - - if err := object.AssertType(obj.TypeEncoding, object.ObjTypeByteList); err != nil { - return clientio.Encode(err, false) - } - - if err := object.AssertEncoding(obj.TypeEncoding, object.ObjEncodingDeque); err != nil { - return clientio.Encode(err, false) - } - - deq := obj.Value.(*Deque) - - // holds the elements popped - var elements []string - for iter := 0; iter < popNumber; iter++ { - x, err := deq.LPop() - if err != nil { - if errors.Is(err, ErrDequeEmpty) { - break - } - panic(fmt.Sprintf("unknown error: %v", err)) - } - elements = append(elements, x) - } - - if len(elements) == 0 { - return clientio.RespNIL - } - - if len(elements) == 1 { - return clientio.Encode(elements[0], false) - } - - return clientio.Encode(elements, false) -} - -func evalLLEN(args []string, store *dstore.Store) []byte { - if len(args) != 1 { - return diceerrors.NewErrArity("LLEN") - } - - obj := store.Get(args[0]) - if obj == nil { - return clientio.Encode(0, false) - } - - if err := object.AssertTypeAndEncoding(obj.TypeEncoding, object.ObjTypeByteList, object.ObjEncodingDeque); err != nil { - return err - } - - deq := obj.Value.(*Deque) - return clientio.Encode(deq.Length, false) -} - func evalFLUSHDB(args []string, store *dstore.Store) []byte { slog.Info("FLUSHDB called", slog.Any("args", args)) if len(args) > 1 { diff --git a/internal/eval/eval_test.go b/internal/eval/eval_test.go index dfa189def..6d5d3ba43 100644 --- a/internal/eval/eval_test.go +++ b/internal/eval/eval_test.go @@ -91,7 +91,13 @@ func TestEval(t *testing.T) { testEvalJSONOBJLEN(t, store) testEvalHLEN(t, store) testEvalSELECT(t, store) + testEvalLPUSH(t, store) + testEvalRPUSH(t, store) + testEvalLPOP(t, store) + testEvalRPOP(t, store) testEvalLLEN(t, store) + testEvalLINSERT(t, store) + testEvalLRANGE(t, store) testEvalGETDEL(t, store) testEvalGETEX(t, store) testEvalJSONNUMINCRBY(t, store) @@ -139,9 +145,6 @@ func TestEval(t *testing.T) { testEvalBFINFO(t, store) testEvalBFEXISTS(t, store) testEvalBFADD(t, store) - testEvalLINSERT(t, store) - testEvalLRANGE(t, store) - testEvalLPOP(t, store) } func testEvalPING(t *testing.T, store *dstore.Store) { @@ -3776,92 +3779,235 @@ func testEvalJSONSTRLEN(t *testing.T, store *dstore.Store) { } } -func testEvalLLEN(t *testing.T, store *dstore.Store) { +func testEvalLPUSH(t *testing.T, store *dstore.Store) { tests := map[string]evalTestCase{ "nil value": { - input: nil, - output: []byte("-ERR wrong number of arguments for 'llen' command\r\n"), + input: nil, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'lpush' command")}, }, "empty args": { - input: []string{}, - output: []byte("-ERR wrong number of arguments for 'llen' command\r\n"), + input: []string{}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'lpush' command")}, }, "wrong number of args": { - input: []string{"KEY1", "KEY2"}, - output: []byte("-ERR wrong number of arguments for 'llen' command\r\n"), + input: []string{"KEY1"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'lpush' command")}, + }, + "key with different type": { + setup: func() { + evalSET([]string{"EXISTING_KEY", "mock_value"}, store) + }, + input: []string{"EXISTING_KEY", "value_1"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("WRONGTYPE Operation against a key holding the wrong kind of value")}, }, "key does not exist": { - input: []string{"NONEXISTENT_KEY"}, - output: clientio.RespZero, + input: []string{"NONEXISTENT_KEY", "value_1"}, + migratedOutput: EvalResponse{Result: int64(1), Error: nil}, }, "key exists": { setup: func() { evalLPUSH([]string{"EXISTING_KEY", "mock_value"}, store) }, - input: []string{"EXISTING_KEY"}, - output: clientio.RespOne, + input: []string{"EXISTING_KEY", "value_1", "value_2"}, + migratedOutput: EvalResponse{Result: int64(3), Error: nil}, + }, + } + runMigratedEvalTests(t, tests, evalLPUSH, store) +} +func testEvalRPUSH(t *testing.T, store *dstore.Store) { + tests := map[string]evalTestCase{ + "nil value": { + input: nil, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'rpush' command")}, + }, + "empty args": { + input: []string{}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'rpush' command")}, + }, + "wrong number of args": { + input: []string{"KEY1"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'rpush' command")}, }, "key with different type": { setup: func() { evalSET([]string{"EXISTING_KEY", "mock_value"}, store) }, - input: []string{"EXISTING_KEY"}, - output: []byte("-ERR Existing key has wrong Dice type\r\n"), + input: []string{"EXISTING_KEY", "value_1"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("WRONGTYPE Operation against a key holding the wrong kind of value")}, + }, + "key does not exist": { + input: []string{"NONEXISTENT_KEY", "value_1"}, + migratedOutput: EvalResponse{Result: int64(1), Error: nil}, + }, + "key exists": { + setup: func() { + evalLPUSH([]string{"EXISTING_KEY", "mock_value"}, store) + }, + input: []string{"EXISTING_KEY", "value_1", "value_2"}, + migratedOutput: EvalResponse{Result: int64(3), Error: nil}, }, } - - runEvalTests(t, tests, evalLLEN, store) + runMigratedEvalTests(t, tests, evalRPUSH, store) } - func testEvalLPOP(t *testing.T, store *dstore.Store) { tests := map[string]evalTestCase{ + "nil value": { + input: nil, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'lpop' command")}, + }, "empty args": { - input: nil, - output: []byte("-ERR wrong number of arguments for 'lpop' command\r\n"), + input: []string{}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'lpop' command")}, }, "more than 2 args": { - input: []string{"k", "2", "3"}, - output: []byte("-ERR wrong number of arguments for 'lpop' command\r\n"), + input: []string{"k", "2", "3"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'lpop' command")}, + }, + "non-integer count": { + input: []string{"k", "abc"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR value is not an integer or a float")}, + }, + "negative count": { + input: []string{"k", "-1"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR value is not an integer or out of range")}, + }, + "key with different type": { + setup: func() { + evalSET([]string{"EXISTING_KEY", "mock_value"}, store) + }, + input: []string{"EXISTING_KEY"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("WRONGTYPE Operation against a key holding the wrong kind of value")}, + }, + "key does not exist": { + input: []string{"NONEXISTENT_KEY"}, + migratedOutput: EvalResponse{Result: clientio.NIL, Error: nil}, + }, + "key exists with 1 value": { + setup: func() { + evalLPUSH([]string{"EXISTING_KEY", "mock_value"}, store) + }, + input: []string{"EXISTING_KEY"}, + migratedOutput: EvalResponse{Result: "mock_value", Error: nil}, + }, + "key exists with multiple values": { + setup: func() { + evalLPUSH([]string{"EXISTING_KEY", "value_1", "value_2"}, store) + evalRPUSH([]string{"EXISTING_KEY", "value_3", "value_4"}, store) + }, + input: []string{"EXISTING_KEY"}, + migratedOutput: EvalResponse{Result: "value_2", Error: nil}, }, "pop one element": { setup: func() { evalRPUSH([]string{"k", "v1", "v2", "v3", "v4"}, store) }, - input: []string{"k"}, - output: []byte("$2\r\nv1\r\n"), + input: []string{"k"}, + migratedOutput: EvalResponse{Result: "v1", Error: nil}, }, "pop two elements": { setup: func() { evalRPUSH([]string{"k", "v1", "v2", "v3", "v4"}, store) }, - input: []string{"k", "2"}, - output: []byte("*2\r\n$2\r\nv1\r\n$2\r\nv2\r\n")}, + input: []string{"k", "2"}, + migratedOutput: EvalResponse{Result: []string{"v1", "v2"}, Error: nil}, + }, "pop more elements than available": { setup: func() { evalRPUSH([]string{"k", "v1", "v2"}, store) }, - input: []string{"k", "5"}, - output: []byte("*2\r\n$2\r\nv1\r\n$2\r\nv2\r\n")}, + input: []string{"k", "5"}, + migratedOutput: EvalResponse{Result: []string{"v1", "v2"}, Error: nil}, + }, "pop 0 elements": { setup: func() { evalRPUSH([]string{"k", "v1", "v2"}, store) }, - input: []string{"k", "0"}, - output: []byte("*0\r\n")}, - "negative count": { - input: []string{"k", "-1"}, - output: []byte("-ERR value is out of range\r\n"), + input: []string{"k", "0"}, + migratedOutput: EvalResponse{Result: clientio.NIL, Error: nil}, }, - "non-integer count": { - input: []string{"k", "abc"}, - output: []byte("-ERR value is not an integer or out of range\r\n"), + } + runMigratedEvalTests(t, tests, evalLPOP, store) +} + +func testEvalRPOP(t *testing.T, store *dstore.Store) { + tests := map[string]evalTestCase{ + "nil value": { + input: nil, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'rpop' command")}, + }, + "empty args": { + input: []string{}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'rpop' command")}, + }, + "wrong number of args": { + input: []string{"KEY1", "KEY2"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'rpop' command")}, + }, + "key with different type": { + setup: func() { + evalSET([]string{"EXISTING_KEY", "mock_value"}, store) + }, + input: []string{"EXISTING_KEY"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("WRONGTYPE Operation against a key holding the wrong kind of value")}, }, "key does not exist": { - input: []string{"nonexistent_key"}, - output: []byte("$-1\r\n"), + input: []string{"NONEXISTENT_KEY"}, + migratedOutput: EvalResponse{Result: clientio.NIL, Error: nil}, + }, + "key exists with 1 value": { + setup: func() { + evalLPUSH([]string{"EXISTING_KEY", "mock_value"}, store) + }, + input: []string{"EXISTING_KEY"}, + migratedOutput: EvalResponse{Result: "mock_value", Error: nil}, + }, + "key exists with multiple values": { + setup: func() { + evalLPUSH([]string{"EXISTING_KEY", "value_1", "value_2"}, store) + evalRPUSH([]string{"EXISTING_KEY", "value_3", "value_4"}, store) + }, + input: []string{"EXISTING_KEY"}, + migratedOutput: EvalResponse{Result: "value_4", Error: nil}, }, } - runEvalTests(t, tests, evalLPOP, store) + runMigratedEvalTests(t, tests, evalRPOP, store) +} + +func testEvalLLEN(t *testing.T, store *dstore.Store) { + tests := map[string]evalTestCase{ + "nil value": { + input: nil, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'llen' command")}, + }, + "empty args": { + input: []string{}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'llen' command")}, + }, + "wrong number of args": { + input: []string{"KEY1", "KEY2"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'llen' command")}, + }, + "key does not exist": { + input: []string{"NONEXISTENT_KEY"}, + migratedOutput: EvalResponse{Result: clientio.IntegerZero, Error: nil}, + }, + "key exists": { + setup: func() { + evalLPUSH([]string{"EXISTING_KEY", "mock_value"}, store) + }, + input: []string{"EXISTING_KEY"}, + migratedOutput: EvalResponse{Result: int64(1), Error: nil}, + }, + "key with different type": { + setup: func() { + evalSET([]string{"EXISTING_KEY", "mock_value"}, store) + }, + input: []string{"EXISTING_KEY"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("WRONGTYPE Operation against a key holding the wrong kind of value")}, + }, + } + + runMigratedEvalTests(t, tests, evalLLEN, store) } func testEvalJSONNUMINCRBY(t *testing.T, store *dstore.Store) { diff --git a/internal/eval/store_eval.go b/internal/eval/store_eval.go index 0b4f19296..5484e3c06 100644 --- a/internal/eval/store_eval.go +++ b/internal/eval/store_eval.go @@ -2737,6 +2737,280 @@ func evalJSONARRTRIM(args []string, store *dstore.Store) *EvalResponse { } } +// evalLPUSH inserts value(s) one by one at the head of of the list +// +// # Returns list length after command execution +// +// Usage: LPUSH key value [value...] +func evalLPUSH(args []string, store *dstore.Store) *EvalResponse { + if len(args) < 2 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("LPUSH"), + } + } + + obj := store.Get(args[0]) + if obj == nil { + obj = store.NewObj(NewDeque(), -1, object.ObjTypeByteList, object.ObjEncodingDeque) + } + + if err := object.AssertType(obj.TypeEncoding, object.ObjTypeByteList); err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + if err := object.AssertEncoding(obj.TypeEncoding, object.ObjEncodingDeque); err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + store.Put(args[0], obj) + for i := 1; i < len(args); i++ { + obj.Value.(*Deque).LPush(args[i]) + } + + deq := obj.Value.(*Deque) + + return &EvalResponse{ + Result: deq.Length, + Error: nil, + } +} + +// evalRPUSH inserts value(s) one by one at the tail of of the list +// +// # Returns list length after command execution +// +// Usage: RPUSH key value [value...] +func evalRPUSH(args []string, store *dstore.Store) *EvalResponse { + if len(args) < 2 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("RPUSH"), + } + } + + obj := store.Get(args[0]) + if obj == nil { + obj = store.NewObj(NewDeque(), -1, object.ObjTypeByteList, object.ObjEncodingDeque) + } + + if err := object.AssertType(obj.TypeEncoding, object.ObjTypeByteList); err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + if err := object.AssertEncoding(obj.TypeEncoding, object.ObjEncodingDeque); err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + store.Put(args[0], obj) + for i := 1; i < len(args); i++ { + obj.Value.(*Deque).RPush(args[i]) + } + + deq := obj.Value.(*Deque) + + return &EvalResponse{ + Result: deq.Length, + Error: nil, + } +} + +// evalLPOP pops the element at the head of the list and returns it +// +// # Returns the element at the head of the list +// +// Usage: LPOP key +func evalLPOP(args []string, store *dstore.Store) *EvalResponse { + // By default we pop only 1 + popNumber := 1 + + // LPOP accepts 1 or 2 arguments only - LPOP key [count] + + if len(args) < 1 || len(args) > 2 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("LPOP"), + } + } + + if len(args) == 2 { + nos, err := strconv.Atoi(args[1]) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrInvalidNumberFormat, + } + } + if nos == 0 { + // returns empty string if count given is 0 + return &EvalResponse{ + Result: clientio.NIL, + Error: nil, + } + } + if nos < 0 { + // returns an out of range err if count is negetive + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrIntegerOutOfRange, + } + } + popNumber = nos + } + + obj := store.Get(args[0]) + if obj == nil { + return &EvalResponse{ + Result: clientio.NIL, + Error: nil, + } + } + + if err := object.AssertType(obj.TypeEncoding, object.ObjTypeByteList); err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + if err := object.AssertEncoding(obj.TypeEncoding, object.ObjEncodingDeque); err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + deq := obj.Value.(*Deque) + + // holds the elements popped + var elements []string + for iter := 0; iter < popNumber; iter++ { + x, err := deq.LPop() + if err != nil { + if errors.Is(err, ErrDequeEmpty) { + break + } + } + elements = append(elements, x) + } + + if len(elements) == 0 { + return &EvalResponse{ + Result: clientio.NIL, + Error: nil, + } + } + + if len(elements) == 1 { + return &EvalResponse{ + Result: elements[0], + Error: nil, + } + } + + return &EvalResponse{ + Result: elements, + Error: nil, + } +} + +// evalRPOP pops the element at the tail of the list and returns it +// +// # Returns the element at the tail of the list +// +// Usage: RPOP key +func evalRPOP(args []string, store *dstore.Store) *EvalResponse { + if len(args) != 1 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("RPOP"), + } + } + + obj := store.Get(args[0]) + if obj == nil { + return &EvalResponse{ + Result: clientio.NIL, + Error: nil, + } + } + + if err := object.AssertType(obj.TypeEncoding, object.ObjTypeByteList); err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + if err := object.AssertEncoding(obj.TypeEncoding, object.ObjEncodingDeque); err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + deq := obj.Value.(*Deque) + x, err := deq.RPop() + if err != nil { + if errors.Is(err, ErrDequeEmpty) { + return &EvalResponse{ + Result: clientio.NIL, + Error: nil, + } + } + } + + return &EvalResponse{ + Result: x, + Error: nil, + } +} + +// evalLLEN returns the number of elements in the list +// +// Usage: LLEN key +func evalLLEN(args []string, store *dstore.Store) *EvalResponse { + if len(args) != 1 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("LLEN"), + } + } + + obj := store.Get(args[0]) + if obj == nil { + return &EvalResponse{ + Result: clientio.IntegerZero, + Error: nil, + } + } + + if err := object.AssertTypeAndEncoding(obj.TypeEncoding, object.ObjTypeByteList, object.ObjEncodingDeque); err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + deq := obj.Value.(*Deque) + return &EvalResponse{ + Result: deq.Length, + Error: nil, + } +} + // evalJSONARRAPPEND appends the value(s) provided in the args to the given array path // in the JSON object saved at key in arguments. // Args must contain atleast a key, path and value. diff --git a/internal/server/cmd_meta.go b/internal/server/cmd_meta.go index 717f3b069..85a3bb8b4 100644 --- a/internal/server/cmd_meta.go +++ b/internal/server/cmd_meta.go @@ -290,32 +290,26 @@ var ( Cmd: "BF.INFO", CmdType: SingleShard, } - cmsInitByDimCmdMeta = CmdsMeta{ Cmd: "CMS.INITBYDIM", CmdType: SingleShard, } - cmsInitByProbCmdMeta = CmdsMeta{ Cmd: "CMS.INITBYPROB", CmdType: SingleShard, } - cmsInfoCmdMeta = CmdsMeta{ Cmd: "CMS.INFO", CmdType: SingleShard, } - cmsIncrByCmdMeta = CmdsMeta{ Cmd: "CMS.INCRBY", CmdType: SingleShard, } - cmsQueryCmdMeta = CmdsMeta{ Cmd: "CMS.QUERY", CmdType: SingleShard, } - cmsMergeCmdMeta = CmdsMeta{ Cmd: "CMS.MERGE", CmdType: SingleShard, @@ -352,6 +346,26 @@ var ( Cmd: "HMGET", CmdType: SingleShard, } + lpushCmdMeta = CmdsMeta{ + Cmd: "LPUSH", + CmdType: SingleShard, + } + rpushCmdMeta = CmdsMeta{ + Cmd: "RPUSH", + CmdType: SingleShard, + } + lpopCmdMeta = CmdsMeta{ + Cmd: "LPOP", + CmdType: SingleShard, + } + rpopCmdMeta = CmdsMeta{ + Cmd: "RPOP", + CmdType: SingleShard, + } + llenCmdMeta = CmdsMeta{ + Cmd: "LLEN", + CmdType: SingleShard, + } jsonForgetCmdMeta = CmdsMeta{ Cmd: "JSON.FORGET", @@ -476,6 +490,11 @@ func init() { WorkerCmdsMeta["BITFIELD"] = bitfieldCmdMeta WorkerCmdsMeta["BITPOS"] = bitposCmdMeta WorkerCmdsMeta["BITFIELD_RO"] = bitfieldroCmdMeta + WorkerCmdsMeta["LPUSH"] = lpushCmdMeta + WorkerCmdsMeta["RPUSH"] = rpushCmdMeta + WorkerCmdsMeta["LPOP"] = lpopCmdMeta + WorkerCmdsMeta["RPOP"] = rpopCmdMeta + WorkerCmdsMeta["LLEN"] = llenCmdMeta WorkerCmdsMeta["JSON.FORGET"] = jsonForgetCmdMeta WorkerCmdsMeta["JSON.DEL"] = jsonDelCmdMeta diff --git a/internal/worker/cmd_meta.go b/internal/worker/cmd_meta.go index 453063bb8..03577d084 100644 --- a/internal/worker/cmd_meta.go +++ b/internal/worker/cmd_meta.go @@ -63,6 +63,11 @@ const ( CmdJSONToggle = "JSON.TOGGLE" CmdJSONNumIncrBY = "JSON.NUMINCRBY" CmdJSONNumMultBY = "JSON.NUMMULTBY" + CmdLPush = "LPUSH" + CmdRPush = "RPUSH" + CmdLPop = "LPOP" + CmdRPop = "RPOP" + CmdLLEN = "LLEN" ) // Multi-shard commands. @@ -312,6 +317,21 @@ var CommandsMeta = map[string]CmdMeta{ CmdJSONNumMultBY: { CmdType: SingleShard, }, + CmdLPush: { + CmdType: SingleShard, + }, + CmdRPush: { + CmdType: SingleShard, + }, + CmdLPop: { + CmdType: SingleShard, + }, + CmdRPop: { + CmdType: SingleShard, + }, + CmdLLEN: { + CmdType: SingleShard, + }, // Multi-shard commands. CmdRename: {