From cd796f4049f7cc0de5b3ea00a73d42c378ad7754 Mon Sep 17 00:00:00 2001 From: Pranay Garg <37935794+pg30@users.noreply.github.com> Date: Sun, 20 Oct 2024 18:44:29 +0530 Subject: [PATCH] #1016 Migrated `INCR` commands (#1141) * migrated incr commands * fixed test cases * fixed integration tests * fixed linting issues * updated docs and added error for consistency with redis * fixed integration tests * fixed * updated docs * integration tests * update * integration tests * added integration tests * tests * fix * added integration tests for websocket * updated docs: --- docs/src/content/docs/commands/DECR.md | 13 +- docs/src/content/docs/commands/DECRBY.md | 12 +- docs/src/content/docs/commands/INCR.md | 10 +- docs/src/content/docs/commands/INCRBY.md | 104 ++++ docs/src/content/docs/commands/INCRBYFLOAT.md | 104 ++++ integration_tests/commands/http/decr_test.go | 109 ++++ .../commands/http/incr_by_float_test.go | 5 +- .../commands/{async => resp}/decr_test.go | 2 +- .../{async => resp}/incr_by_float_test.go | 7 +- .../commands/{async => resp}/incr_test.go | 9 +- .../commands/websocket/decr_test.go | 76 +++ .../commands/websocket/incr_by_float_test.go | 92 +++ .../commands/websocket/incr_test.go | 150 +++++ internal/eval/commands.go | 33 +- internal/eval/eval.go | 229 +------- internal/eval/eval_test.go | 539 +++++++++++++++--- internal/eval/store_eval.go | 231 ++++++++ internal/server/cmd_meta.go | 27 + internal/worker/cmd_meta.go | 21 + 19 files changed, 1438 insertions(+), 335 deletions(-) create mode 100644 docs/src/content/docs/commands/INCRBY.md create mode 100644 docs/src/content/docs/commands/INCRBYFLOAT.md create mode 100644 integration_tests/commands/http/decr_test.go rename integration_tests/commands/{async => resp}/decr_test.go (99%) rename integration_tests/commands/{async => resp}/incr_by_float_test.go (91%) rename integration_tests/commands/{async => resp}/incr_test.go (98%) create mode 100644 integration_tests/commands/websocket/decr_test.go create mode 100644 integration_tests/commands/websocket/incr_by_float_test.go create mode 100644 integration_tests/commands/websocket/incr_test.go diff --git a/docs/src/content/docs/commands/DECR.md b/docs/src/content/docs/commands/DECR.md index 40e9fb669..73d6f941b 100644 --- a/docs/src/content/docs/commands/DECR.md +++ b/docs/src/content/docs/commands/DECR.md @@ -31,16 +31,16 @@ When the `DECR` command is executed, the following steps occur: - If the specified key doesn't exist, it is created, with -1 as it's value, and same is returned. - If the specified key exists with an integer value, value is decremented and new value is returned. - If the specified key exists with a non-integer OR out-of-range value, error message is returned as: - - `(error) ERROR value is not an integer or out of range` + - `(error) ERR value is not an integer or out of range` ## Errors The `DECR` command can raise errors in the following scenarios: 1. `Wrong Type Error`: If the key exists but the value is not a string that can be represented as an integer, DiceDB will return an error. - - `Error Message`: `(error) ERROR value is not an integer or out of range` + - `Error Message`: `(error) ERR value is not an integer or out of range` 1. `Out of Range Error`: If the value of the key is out of the range of a 64-bit signed integer after the decrement operation, DiceDB will return an error. - - `Error Message`: `(error) ERROR value is not an integer or out of range` + - `Error Message`: `(error) ERR increment or decrement would overflow` ## Example Usage @@ -78,7 +78,7 @@ OK 127.0.0.1:7379> SET mystring "hello" OK 127.0.0.1:7379> DECR mystring -(error) ERROR value is not an integer or out of range +(error) ERR value is not an integer or out of range ``` `Explanation`: @@ -93,7 +93,7 @@ OK 127.0.0.1:7379> SET mycounter 234293482390480948029348230948 OK 127.0.0.1:7379> DECR mycounter -(error) ERROR value is not an integer or out of range +(error) ERR value is not an integer or out of range ``` `Explanation`: @@ -101,3 +101,6 @@ OK 1. The `SET` command initializes the key `mycounter` with the out-of-range value for a 64-bit signed integer. 1. The `DECR` command attempts to decrement the value of `mycounter`, but this would result in an overflow, so an error is raised. +## Alternatives + +You can also use the `DECRBY` command to decrement the value of a key by a specified amount. diff --git a/docs/src/content/docs/commands/DECRBY.md b/docs/src/content/docs/commands/DECRBY.md index 0bd71a95c..0372b876e 100644 --- a/docs/src/content/docs/commands/DECRBY.md +++ b/docs/src/content/docs/commands/DECRBY.md @@ -41,15 +41,19 @@ The `DECRBY` command can raise errors in the following scenarios: 1. `Wrong Type Error`: - - Error Message: `ERROR value is not an integer or out of range` + - Error Message: `(error) ERR value is not an integer or out of range` - This error occurs if the decrement value provided is not a valid integer. - This error occurs if the key exists but its value is not a string that can be represented as an integer 2. `Syntax Error`: - - Error Message: `ERROR wrong number of arguments for 'decrby' command` + - Error Message: `(error) ERR wrong number of arguments for 'decrby' command` - Occurs if the command is called without the required parameter. +3. `Overflow Error`: + + - Error Message: `(error) ERR increment or decrement would overflow` + - If the decrement operation causes the value to exceed the maximum integer value that DiceDB can handle, an overflow error will occur. ## Examples @@ -81,7 +85,7 @@ OK 127.0.0.1:7379>SET mystring "hello" OK 127.0.0.1:7379>DECRBY mystring 2 -(error) ERROR value is not an integer or out of range +(error) ERR value is not an integer or out of range ``` `Explanation:` - In this example, the key `mystring` holds a non-integer value, so the `DECRBY` command returns an error. @@ -90,7 +94,7 @@ OK ```bash 127.0.0.1:7379>DECRBY mycounter "two" -(error) ERROR value is not an integer or out of range +(error) ERR value is not an integer or out of range ``` `Explanation:` diff --git a/docs/src/content/docs/commands/INCR.md b/docs/src/content/docs/commands/INCR.md index 16b66b18a..2e2c0c463 100644 --- a/docs/src/content/docs/commands/INCR.md +++ b/docs/src/content/docs/commands/INCR.md @@ -35,13 +35,13 @@ When the `INCR` command is executed, the following steps occur: ## Errors 1. `Wrong Type Error`: - - Error Message: `(error) ERROR value is not an integer or out of range` + - Error Message: `(error) ERR value is not an integer or out of range` - If the key exists but does not hold a string value that can be interpreted as an integer, DiceDB will return an error. 2. `Overflow Error`: - - Error Message: `(error) ERROR increment or decrement would overflow` + - Error Message: `(error) ERR increment or decrement would overflow` - If the increment operation causes the value to exceed the maximum integer value that DiceDB can handle, an overflow error will occur. @@ -89,3 +89,9 @@ Incrementing a key `mykey` with a value that exceeds the maximum integer value: - The `INCR` command is often used in scenarios where counters are needed, such as counting page views, tracking user actions, or generating unique IDs. - The atomic nature of the `INCR` command ensures that it is safe to use in concurrent environments without additional synchronization mechanisms. - For decrementing a value, you can use the `DECR` command, which works similarly but decreases the value by one. + + +## Alternatives + +- You can also use the `INCRBY` command to increment the value of a key by a specified amount. +- You can also use the `INCRBYFLOAT` command to increment the value of a key by a fractional amount. diff --git a/docs/src/content/docs/commands/INCRBY.md b/docs/src/content/docs/commands/INCRBY.md new file mode 100644 index 000000000..9ebc383ec --- /dev/null +++ b/docs/src/content/docs/commands/INCRBY.md @@ -0,0 +1,104 @@ +--- +title: INCRBY +description: The `INCRBY` command in DiceDB is used to increment the integer value of a key by a specified amount. This command is useful for scenarios where you need to increase a counter or a numeric value stored in a key. +--- + +The `INCRBY` command in DiceDB is used to increment the integer value of a key by a specified amount. This command is useful for scenarios where you need to increase a counter or a numeric value stored in a key. + +## Syntax + +``` +INCRBY key delta +``` + +## Parameters + +| Parameter | Description | Type | Required | +|-----------|---------------------------------------------------------------------------------------------------------------|---------|----------| +| `key` | The key whose value you want to increment. This key must hold a string that can be represented as an integer. | String | Yes | +|`delta` | The integer value by which the key's value should be increased. This value can be positive or negative. | String | Yes | + + +## Return values + +| Condition | Return Value | +|--------------------------------------------------|------------------------------------------------------------------| +| Key exists and holds an integer string | `(integer)` The value of the key after incrementing by delta. | +| Key does not exist | `(integer)` delta | + + +## Behaviour +When the `INCRBY` command is executed, the following steps occur: + +- DiceDB checks if the key exists. +- If the key does not exist, DiceDB treats the key's value as 0 before performing the increment operation. +- If the key exists but does not hold a string that can be represented as an integer, an error is returned. +- The value of the key is incremented by the specified increment value. +- The new value of the key is returned. +## Errors + +The `INCRBY` command can raise errors in the following scenarios: + +1. `Wrong Type Error`: + + - Error Message: `ERR value is not an integer or out of range` + - This error occurs if the increment value provided is not a valid integer. + - This error occurs if the key exists but its value is not a string that can be represented as an integer + +2. `Syntax Error`: + + - Error Message: `ERR wrong number of arguments for 'incrby' command` + - Occurs if the command is called without the required parameter. + +3. `Overflow Error`: + + - Error Message: `ERR increment or decrement would overflow` + - If the increment operation causes the value to exceed the maximum integer value that DiceDB can handle, an overflow error will occur. + + +## Examples + +### Example with Incrementing the Value of an Existing Key + + +```bash +127.0.0.1:7379>SET mycounter 10 +OK +127.0.0.1:7379>INCRBY mycounter 3 +(integer)13 +``` +`Explanation:` + +- In this example, the value of `mycounter` is set to 10 +- The `INCRBY` command incremented `mycounter` by 3, resulting in a new value of 13. + +### Example with Incrementing a Non-Existent Key (Implicit Initialization to 0) + +```bash +127.0.0.1:7379>INCRBY newcounter 5 +(integer)5 +``` +`Explanation:` +- In this example, since `newcounter` does not exist, DiceDB treats its value as 0 and increments it by 5, resulting in a new value of 5. +### Example with Error Due to Non-Integer Value in Key + +```bash +127.0.0.1:7379>SET mystring "hello" +OK +127.0.0.1:7379>INCRBY mystring 2 +(error) ERR value is not an integer or out of range +``` +`Explanation:` +- In this example, the key `mystring` holds a non-integer value, so the `INCRBY` command returns an error. + +### Example with Error Due to Invalid Increment Value (Non-Integer Decrement) + +```bash +127.0.0.1:7379>INCRBY mycounter "two" +(error) ERR value is not an integer or out of range +``` + +`Explanation:` +- In this example, the increment value "two" is not a valid integer, so the `INCRBY` command returns an error. + + diff --git a/docs/src/content/docs/commands/INCRBYFLOAT.md b/docs/src/content/docs/commands/INCRBYFLOAT.md new file mode 100644 index 000000000..fcdd293a7 --- /dev/null +++ b/docs/src/content/docs/commands/INCRBYFLOAT.md @@ -0,0 +1,104 @@ +--- +title: INCRBYFLOAT +description: The `INCRBYFLOAT` command in DiceDB is used to increment the numeric value of a key by a fractional amount. This command is useful for scenarios where you need to increase a number by a fractional amount. +--- + +The `INCRBYFLOAT` command in DiceDB is used to increment the numeric value of a key by a fractional amount. This command is useful for scenarios where you need to increase a number by a fractional amount. + +## Syntax + +``` +INCRBYFLOAT key delta +``` + +## Parameters + +| Parameter | Description | Type | Required | +|-----------|---------------------------------------------------------------------------------------------------------------|---------|----------| +| `key` | The key whose value you want to increment. This key must hold a string that can be represented as an number. | String | Yes | +|`delta` | The fractional value by which the key's value should be increased. This value can be positive or negative. | String | Yes | + + +## Return values + +| Condition | Return Value | +|--------------------------------------------------|------------------------------------------------------------------| +| Key exists and holds an numeric string | `(float)` The value of the key after incrementing by delta. | +| Key does not exist | `(float)` delta | + + +## Behaviour +When the `INCRBYFLOAT` command is executed, the following steps occur: + +- DiceDB checks if the key exists. +- If the key does not exist, DiceDB treats the key's value as 0 before performing the increment operation. +- If the key exists but does not hold a string that can be represented as an number, an error is returned. +- The value of the key is incremented by the specified increment value. +- The new value of the key is returned. +## Errors + +The `INCRBYFLOAT` command can raise errors in the following scenarios: + +1. `Wrong Type Error`: + + - Error Message: `ERR value is not a valid float` + - This error occurs if the increment value provided is not a valid number. + - This error occurs if the key exists but its value is not a string that can be represented as an number + +2. `Syntax Error`: + + - Error Message: `ERR wrong number of arguments for 'incrbyfloat' command` + - Occurs if the command is called without the required parameter. + +3. `Overflow Error`: + + - Error Message: `(error) ERR value is out of range` + - If the increment operation causes the value to exceed the maximum float value that DiceDB can handle, an overflow error will occur. + + +## Examples + +### Example with Incrementing the Value of an Existing Key + + +```bash +127.0.0.1:7379>SET mycounter 10 +OK +127.0.0.1:7379>INCRBYFLOAT mycounter 3.4 +"13.4" +``` +`Explanation:` + +- In this example, the value of `mycounter` is set to 10 +- The `INCRBYFLOAT` command incremented `mycounter` by 3.4, resulting in a new value of 13.4 + +### Example with Incrementing a Non-Existent Key (Implicit Initialization to 0) + +```bash +127.0.0.1:7379>INCRBYFLOAT newcounter 5.3 +"5.3" +``` +`Explanation:` +- In this example, since `newcounter` does not exist, DiceDB treats its value as 0 and increments it by 5.3, resulting in a new value of 5.3. +### Example with Error Due to Wrong Value in Key + +```bash +127.0.0.1:7379>SET mystring "hello" +OK +127.0.0.1:7379>INCRBYFLOAT mystring 2.3 +(error) ERR value is not a valid float +``` +`Explanation:` +- In this example, the key `mystring` holds a string value, so the `INCRBYFLOAT` command returns an error. + +### Example with Error Due to Invalid Increment Value (Non-Integer Decrement) + +```bash +127.0.0.1:7379>INCRBYFLOAT mycounter "two" +(error) ERR value is not a valid float +``` + +`Explanation:` +- In this example, the increment value "two" is not a valid number, so the `INCRBYFLOAT` command returns an error. + + diff --git a/integration_tests/commands/http/decr_test.go b/integration_tests/commands/http/decr_test.go new file mode 100644 index 000000000..4b5892bd4 --- /dev/null +++ b/integration_tests/commands/http/decr_test.go @@ -0,0 +1,109 @@ +package http + +import ( + "math" + "strconv" + "testing" + "time" + + "gotest.tools/v3/assert" +) + +func TestDECR(t *testing.T) { + exec := NewHTTPCommandExecutor() + + exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"keys": [...]string{"key1", "key2", "key3"}}}) + + testCases := []struct { + name string + commands []HTTPCommand + expected []interface{} + delays []time.Duration + }{ + { + name: "Decrement multiple keys", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "key1", "value": 3}}, + {Command: "DECR", Body: map[string]interface{}{"key": "key1"}}, + {Command: "DECR", Body: map[string]interface{}{"key": "key1"}}, + {Command: "DECR", Body: map[string]interface{}{"key": "key2"}}, + {Command: "GET", Body: map[string]interface{}{"key": "key1"}}, + {Command: "GET", Body: map[string]interface{}{"key": "key2"}}, + {Command: "SET", Body: map[string]interface{}{"key": "key3", "value": strconv.Itoa(math.MinInt64 + 1)}}, + {Command: "DECR", Body: map[string]interface{}{"key": "key3"}}, + {Command: "DECR", Body: map[string]interface{}{"key": "key3"}}, + }, + expected: []interface{}{"OK", float64(2), float64(1), float64(-1), float64(1), float64(-1), "OK", float64(math.MinInt64), "ERR increment or decrement would overflow"}, + delays: []time.Duration{0, 0, 0, 0, 0, 0, 0, 0, 0}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"keys": [...]string{"key1", "key2", "key3"}}}) + + for i, cmd := range tc.commands { + if tc.delays[i] > 0 { + time.Sleep(tc.delays[i]) + } + result, err := exec.FireCommand(cmd) + if err != nil { + // Check if the error message matches the expected result + assert.Equal(t, tc.expected[i], err.Error(), "Error message mismatch for cmd %s", cmd) + } else { + assert.Equal(t, tc.expected[i], result, "Value mismatch for cmd %s, expected %v, got %v", cmd, tc.expected[i], result) + } + } + }) + } +} + +func TestDECRBY(t *testing.T) { + exec := NewHTTPCommandExecutor() + + exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"keys": [...]string{"key1", "key2", "key3"}}}) + + testCases := []struct { + name string + commands []HTTPCommand + expected []interface{} + delays []time.Duration + }{ + { + name: "Decrement multiple keys", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "key1", "value": 3}}, + {Command: "SET", Body: map[string]interface{}{"key": "key3", "value": strconv.Itoa(math.MinInt64 + 1)}}, + {Command: "DECRBY", Body: map[string]interface{}{"key": "key1", "value": 2}}, + {Command: "DECRBY", Body: map[string]interface{}{"key": "key1", "value": 1}}, + {Command: "DECRBY", Body: map[string]interface{}{"key": "key4", "value": 1}}, + {Command: "DECRBY", Body: map[string]interface{}{"key": "key3", "value": 1}}, + {Command: "DECRBY", Body: map[string]interface{}{"key": "key3", "value": strconv.Itoa(math.MinInt64)}}, + {Command: "DECRBY", Body: map[string]interface{}{"key": "key5", "value": "abc"}}, + {Command: "GET", Body: map[string]interface{}{"key": "key1"}}, + {Command: "GET", Body: map[string]interface{}{"key": "key4"}}, + }, + expected: []interface{}{"OK", "OK", float64(1), float64(0), float64(-1), float64(math.MinInt64), "ERR increment or decrement would overflow", "ERR value is not an integer or out of range", float64(0), float64(-1)}, + delays: []time.Duration{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"keys": [...]string{"key1", "key2", "expiry_key", "max_int", "min_int", "float_key", "string_key", "bool_key"}}}) + + for i, cmd := range tc.commands { + if tc.delays[i] > 0 { + time.Sleep(tc.delays[i]) + } + result, err := exec.FireCommand(cmd) + if err != nil { + // Check if the error message matches the expected result + assert.Equal(t, tc.expected[i], err.Error(), "Error message mismatch for cmd %s", cmd) + } else { + assert.Equal(t, tc.expected[i], result, "Value mismatch for cmd %s, expected %v, got %v", cmd, tc.expected[i], result) + } + } + }) + } +} diff --git a/integration_tests/commands/http/incr_by_float_test.go b/integration_tests/commands/http/incr_by_float_test.go index d218d8619..0ad0054a6 100644 --- a/integration_tests/commands/http/incr_by_float_test.go +++ b/integration_tests/commands/http/incr_by_float_test.go @@ -11,8 +11,7 @@ func TestINCRBYFLOAT(t *testing.T) { exec := NewHTTPCommandExecutor() invalidArgMessage := "ERR wrong number of arguments for 'incrbyfloat' command" - invalidValueTypeMessage := "WRONGTYPE Operation against a key holding the wrong kind of value" - invalidIncrTypeMessage := "ERR value is not an integer or a float" + invalidIncrTypeMessage := "ERR value is not a valid float" valueOutOfRangeMessage := "ERR value is out of range" testCases := []struct { @@ -67,7 +66,7 @@ func TestINCRBYFLOAT(t *testing.T) { {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "bar"}}, {Command: "INCRBYFLOAT", Body: map[string]interface{}{"key": "foo", "value": 0.1}}, }, - expected: []interface{}{"OK", invalidValueTypeMessage}, + expected: []interface{}{"OK", invalidIncrTypeMessage}, delays: []time.Duration{0, 0}, }, { diff --git a/integration_tests/commands/async/decr_test.go b/integration_tests/commands/resp/decr_test.go similarity index 99% rename from integration_tests/commands/async/decr_test.go rename to integration_tests/commands/resp/decr_test.go index 05bbced7e..dfe06b2e3 100644 --- a/integration_tests/commands/async/decr_test.go +++ b/integration_tests/commands/resp/decr_test.go @@ -1,4 +1,4 @@ -package async +package resp import ( "fmt" diff --git a/integration_tests/commands/async/incr_by_float_test.go b/integration_tests/commands/resp/incr_by_float_test.go similarity index 91% rename from integration_tests/commands/async/incr_by_float_test.go rename to integration_tests/commands/resp/incr_by_float_test.go index 41581e17a..95dd4443f 100644 --- a/integration_tests/commands/async/incr_by_float_test.go +++ b/integration_tests/commands/resp/incr_by_float_test.go @@ -1,4 +1,4 @@ -package async +package resp import ( "testing" @@ -10,8 +10,7 @@ func TestINCRBYFLOAT(t *testing.T) { conn := getLocalConnection() defer conn.Close() invalidArgMessage := "ERR wrong number of arguments for 'incrbyfloat' command" - invalidValueTypeMessage := "WRONGTYPE Operation against a key holding the wrong kind of value" - invalidIncrTypeMessage := "ERR value is not an integer or a float" + invalidIncrTypeMessage := "ERR value is not a valid float" valueOutOfRangeMessage := "ERR value is out of range" testCases := []struct { @@ -48,7 +47,7 @@ func TestINCRBYFLOAT(t *testing.T) { name: "Increment a non numeric value", setupData: "SET foo bar", commands: []string{"INCRBYFLOAT foo 0.1"}, - expected: []interface{}{invalidValueTypeMessage}, + expected: []interface{}{invalidIncrTypeMessage}, }, { name: "Increment by a non numeric value", diff --git a/integration_tests/commands/async/incr_test.go b/integration_tests/commands/resp/incr_test.go similarity index 98% rename from integration_tests/commands/async/incr_test.go rename to integration_tests/commands/resp/incr_test.go index 4ce010474..298b42059 100644 --- a/integration_tests/commands/async/incr_test.go +++ b/integration_tests/commands/resp/incr_test.go @@ -1,4 +1,4 @@ -package async +package resp import ( "fmt" @@ -132,8 +132,10 @@ func TestINCR(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Clean up keys before each test case - keys := []string{"key1", "key2", "max_int", "min_int", "float_key", "string_key", "bool_key", - "non_existent", "str_int1", "str_int2", "str_int3", "expiry_key"} + keys := []string{ + "key1", "key2", "max_int", "min_int", "float_key", "string_key", "bool_key", + "non_existent", "str_int1", "str_int2", "str_int3", "expiry_key", + } for _, key := range keys { FireCommand(conn, fmt.Sprintf("DEL %s", key)) } @@ -270,7 +272,6 @@ func TestINCRBY(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - defer FireCommand(conn, "DEL key unsetKey stringkey") for _, cmd := range tc.setCommands { diff --git a/integration_tests/commands/websocket/decr_test.go b/integration_tests/commands/websocket/decr_test.go new file mode 100644 index 000000000..6727f05b1 --- /dev/null +++ b/integration_tests/commands/websocket/decr_test.go @@ -0,0 +1,76 @@ +package websocket + +import ( + "fmt" + "math" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestDECR(t *testing.T) { + exec := NewWebsocketCommandExecutor() + conn := exec.ConnectToServer() + + testCases := []struct { + name string + cmds []string + expect []interface{} + delays []time.Duration + }{ + { + name: "Decrement multiple keys", + cmds: []string{"SET key1 3", "DECR key1", "DECR key1", "DECR key2", "GET key1", "GET key2", fmt.Sprintf("SET key3 %s", strconv.Itoa(math.MinInt64+1)), "DECR key3", "DECR key3"}, + expect: []interface{}{"OK", float64(2), float64(1), float64(-1), float64(1), float64(-1), "OK", float64(math.MinInt64), "ERR increment or decrement would overflow"}, + delays: []time.Duration{0, 0, 0, 0, 0, 0, 0, 0, 0}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for i, cmd := range tc.cmds { + if tc.delays[i] > 0 { + time.Sleep(tc.delays[i]) + } + result, err := exec.FireCommandAndReadResponse(conn, cmd) + assert.Nil(t, err) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } +} + +func TestDECRBY(t *testing.T) { + exec := NewWebsocketCommandExecutor() + conn := exec.ConnectToServer() + + testCases := []struct { + name string + cmds []string + expect []interface{} + delays []time.Duration + }{ + { + name: "Decrement multiple keys", + cmds: []string{"SET key1 3", fmt.Sprintf("SET key3 %s", strconv.Itoa(math.MinInt64+1)), "DECRBY key1 2", "DECRBY key1 1", "DECRBY key4 1", "DECRBY key3 1", fmt.Sprintf("DECRBY key3 %s", strconv.Itoa(math.MinInt64)), "DECRBY key5 abc"}, + expect: []interface{}{"OK", "OK", float64(1), float64(0), float64(-1), float64(math.MinInt64), "ERR increment or decrement would overflow", "ERR value is not an integer or out of range"}, + delays: []time.Duration{0, 0, 0, 0, 0, 0, 0, 0}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer exec.FireCommandAndReadResponse(conn, "DEL key unsetKey stringkey") + for i, cmd := range tc.cmds { + if tc.delays[i] > 0 { + time.Sleep(tc.delays[i]) + } + result, err := exec.FireCommandAndReadResponse(conn, cmd) + assert.Nil(t, err) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } +} diff --git a/integration_tests/commands/websocket/incr_by_float_test.go b/integration_tests/commands/websocket/incr_by_float_test.go new file mode 100644 index 000000000..9e95bd673 --- /dev/null +++ b/integration_tests/commands/websocket/incr_by_float_test.go @@ -0,0 +1,92 @@ +package websocket + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestINCRBYFLOAT(t *testing.T) { + exec := NewWebsocketCommandExecutor() + conn := exec.ConnectToServer() + invalidArgMessage := "ERR wrong number of arguments for 'incrbyfloat' command" + invalidIncrTypeMessage := "ERR value is not a valid float" + valueOutOfRangeMessage := "ERR value is out of range" + + testCases := []struct { + name string + cmds []string + expect []interface{} + delays []time.Duration + }{ + { + name: "Invalid number of arguments", + cmds: []string{"INCRBYFLOAT", "INCRBYFLOAT foo"}, + expect: []interface{}{invalidArgMessage, invalidArgMessage}, + delays: []time.Duration{0, 0}, + }, + { + name: "Increment a non existing key", + cmds: []string{"INCRBYFLOAT foo 0.1", "GET foo"}, + expect: []interface{}{"0.1", "0.1"}, + delays: []time.Duration{0, 0}, + }, + { + name: "Increment a key with an integer value", + cmds: []string{"SET foo 1", "INCRBYFLOAT foo 0.1", "GET foo"}, + expect: []interface{}{"OK", "1.1", "1.1"}, + delays: []time.Duration{0, 0, 0}, + }, + { + name: "Increment and then decrement a key with the same value", + cmds: []string{"SET foo 1", "INCRBYFLOAT foo 0.1", "GET foo", "INCRBYFLOAT foo -0.1", "GET foo"}, + expect: []interface{}{"OK", "1.1", "1.1", "1", "1"}, + delays: []time.Duration{0, 0, 0, 0, 0}, + }, + { + name: "Increment a non numeric value", + cmds: []string{"SET foo bar", "INCRBYFLOAT foo 0.1"}, + expect: []interface{}{"OK", invalidIncrTypeMessage}, + delays: []time.Duration{0, 0}, + }, + { + name: "Increment by a non numeric value", + cmds: []string{"SET foo 1", "INCRBYFLOAT foo bar"}, + expect: []interface{}{"OK", invalidIncrTypeMessage}, + delays: []time.Duration{0, 0}, + }, + { + name: "Increment by both integer and float", + cmds: []string{"SET foo 1", "INCRBYFLOAT foo 1", "INCRBYFLOAT foo 0.1"}, + expect: []interface{}{"OK", "2", "2.1"}, + delays: []time.Duration{0, 0, 0}, + }, + { + name: "Increment that would make the value Inf", + cmds: []string{"SET foo 1e308", "INCRBYFLOAT foo 1e308", "INCRBYFLOAT foo -1e308"}, + expect: []interface{}{"OK", valueOutOfRangeMessage, "0"}, + delays: []time.Duration{0, 0, 0}, + }, + { + name: "Increment that would make the value -Inf", + cmds: []string{"SET foo -1e308", "INCRBYFLOAT foo -1e308", "INCRBYFLOAT foo 1e308"}, + expect: []interface{}{"OK", valueOutOfRangeMessage, "0"}, + delays: []time.Duration{0, 0, 0}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer exec.FireCommandAndReadResponse(conn, "DEL key unsetKey stringkey") + for i, cmd := range tc.cmds { + if tc.delays[i] > 0 { + time.Sleep(tc.delays[i]) + } + result, err := exec.FireCommandAndReadResponse(conn, cmd) + assert.Nil(t, err) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } +} diff --git a/integration_tests/commands/websocket/incr_test.go b/integration_tests/commands/websocket/incr_test.go new file mode 100644 index 000000000..e066e8999 --- /dev/null +++ b/integration_tests/commands/websocket/incr_test.go @@ -0,0 +1,150 @@ +package websocket + +import ( + "fmt" + "math" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestINCR(t *testing.T) { + exec := NewWebsocketCommandExecutor() + conn := exec.ConnectToServer() + + testCases := []struct { + name string + cmds []string + expect []interface{} + delays []time.Duration + }{ + { + name: "Increment multiple keys", + cmds: []string{"SET key1 0", "INCR key1", "INCR key1", "INCR key2", "GET key1", "GET key2"}, + expect: []interface{}{"OK", float64(1), float64(2), float64(1), float64(2), float64(1)}, + delays: []time.Duration{0, 0, 0, 0, 0, 0}, + }, + { + name: "Increment to and from max int64", + cmds: []string{fmt.Sprintf("SET max_int %s", strconv.Itoa(math.MaxInt64-1)), "INCR max_int", "INCR max_int", fmt.Sprintf("SET max_int %s", strconv.Itoa(math.MaxInt64)), "INCR max_int"}, + expect: []interface{}{"OK", float64(math.MaxInt64), "ERR increment or decrement would overflow", "OK", "ERR increment or decrement would overflow"}, + delays: []time.Duration{0, 0, 0, 0, 0}, + }, + { + name: "Increment from min int64", + cmds: []string{fmt.Sprintf("SET min_int %s", strconv.Itoa(math.MinInt64)), "INCR min_int", "INCR min_int"}, + expect: []interface{}{"OK", float64(math.MinInt64 + 1), float64(math.MinInt64 + 2)}, + delays: []time.Duration{0, 0, 0}, + }, + { + name: "Increment non-integer values", + cmds: []string{"SET float_key 3.14", "INCR float_key", "SET string_key hello", "INCR string_key", "SET bool_key true", "INCR bool_key"}, + expect: []interface{}{"OK", "ERR value is not an integer or out of range", "OK", "ERR value is not an integer or out of range", "OK", "ERR value is not an integer or out of range"}, + delays: []time.Duration{0, 0, 0, 0, 0, 0}, + }, + { + name: "Increment non-existent key", + cmds: []string{"INCR non_existent", "GET non_existent", "INCR non_existent"}, + expect: []interface{}{float64(1), float64(1), float64(2)}, + delays: []time.Duration{0, 0, 0}, + }, + { + name: "Increment string representing integers", + cmds: []string{"SET str_int1 42", "INCR str_int1", "SET str_int2 -10", "INCR str_int2", "SET str_int3 0", "INCR str_int3"}, + expect: []interface{}{"OK", float64(43), "OK", float64(-9), "OK", float64(1)}, + delays: []time.Duration{0, 0, 0, 0, 0, 0}, + }, + { + name: "Increment with expiry", + cmds: []string{"SET expiry_key 0 EX 1", "INCR expiry_key", "INCR expiry_key", "INCR expiry_key"}, + expect: []interface{}{"OK", float64(1), float64(2), float64(1)}, + delays: []time.Duration{0, 0, 0, 2 * time.Second}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + keys := []string{ + "key1", "key2", "max_int", "min_int", "float_key", "string_key", "bool_key", + "non_existent", "str_int1", "str_int2", "str_int3", "expiry_key", + } + for _, key := range keys { + exec.FireCommandAndReadResponse(conn, fmt.Sprintf("DEL %s", key)) + } + + for i, cmd := range tc.cmds { + if tc.delays[i] > 0 { + time.Sleep(tc.delays[i]) + } + result, err := exec.FireCommandAndReadResponse(conn, cmd) + assert.Nil(t, err) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } +} + +func TestINCRBY(t *testing.T) { + exec := NewWebsocketCommandExecutor() + conn := exec.ConnectToServer() + + testCases := []struct { + name string + cmds []string + expect []interface{} + delays []time.Duration + }{ + { + name: "happy flow", + cmds: []string{"SET key 3", "INCRBY key 2", "INCRBY key 1", "GET key"}, + expect: []interface{}{"OK", float64(5), float64(6), float64(6)}, + delays: []time.Duration{0, 0, 0, 0}, + }, + { + name: "happy flow with negative increment", + cmds: []string{"SET key 100", "INCRBY key -2", "INCRBY key -10", "INCRBY key -88", "INCRBY key -100", "GET key"}, + expect: []interface{}{"OK", float64(98), float64(88), float64(0), float64(-100), float64(-100)}, + delays: []time.Duration{0, 0, 0, 0, 0, 0}, + }, + { + name: "happy flow with unset key", + cmds: []string{"SET key 3", "INCRBY unsetKey 2", "GET key", "GET unsetKey"}, + expect: []interface{}{"OK", float64(2), float64(3), float64(2)}, + delays: []time.Duration{0, 0, 0, 0}, + }, + { + name: "edge case with maxInt64", + cmds: []string{fmt.Sprintf("SET key %s", strconv.Itoa(math.MaxInt64-1)), "INCRBY key 1", "INCRBY key 1", "GET key"}, + expect: []interface{}{"OK", float64(math.MaxInt64), "ERR increment or decrement would overflow", float64(math.MaxInt64)}, + delays: []time.Duration{0, 0, 0, 0}, + }, + { + name: "edge case with negative increment", + cmds: []string{fmt.Sprintf("SET key %s", strconv.Itoa(math.MinInt64+1)), "INCRBY key -1", "INCRBY key -1", "GET key"}, + expect: []interface{}{"OK", float64(math.MinInt64), "ERR increment or decrement would overflow", float64(math.MinInt64)}, + delays: []time.Duration{0, 0, 0, 0}, + }, + { + name: "edge case with string values", + cmds: []string{"SET key 1", "INCRBY stringkey abc"}, + expect: []interface{}{"OK", "ERR value is not an integer or out of range"}, + delays: []time.Duration{0, 0}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer exec.FireCommandAndReadResponse(conn, "DEL key unsetKey stringkey") + for i, cmd := range tc.cmds { + if tc.delays[i] > 0 { + time.Sleep(tc.delays[i]) + } + result, err := exec.FireCommandAndReadResponse(conn, cmd) + assert.Nil(t, err) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } +} diff --git a/internal/eval/commands.go b/internal/eval/commands.go index 5fc45de1f..2248ec454 100644 --- a/internal/eval/commands.go +++ b/internal/eval/commands.go @@ -361,9 +361,10 @@ var ( The value for the queried key should be of integer format, if not INCR returns encoded error response. evalINCR returns the incremented value for the key if there are no errors.`, - Eval: evalINCR, - Arity: 2, - KeySpecs: KeySpecs{BeginIndex: 1, Step: 1}, + NewEval: evalINCR, + IsMigrated: true, + Arity: 2, + KeySpecs: KeySpecs{BeginIndex: 1, Step: 1}, } incrByFloatCmdMeta = DiceCmdMeta{ Name: "INCRBYFLOAT", @@ -374,8 +375,9 @@ var ( If the value at the key is a string, it should be parsable to float64, if not INCRBYFLOAT returns an error response. INCRBYFLOAT returns the incremented value for the key after applying the specified increment if there are no errors.`, - Eval: evalINCRBYFLOAT, - Arity: 2, + NewEval: evalINCRBYFLOAT, + IsMigrated: true, + Arity: 2, } infoCmdMeta = DiceCmdMeta{ Name: "INFO", @@ -560,9 +562,10 @@ var ( The value for the queried key should be of integer format, if not DECR returns encoded error response. evalDECR returns the decremented value for the key if there are no errors.`, - Eval: evalDECR, - Arity: 2, - KeySpecs: KeySpecs{BeginIndex: 1, Step: 1}, + NewEval: evalDECR, + IsMigrated: true, + Arity: 2, + KeySpecs: KeySpecs{BeginIndex: 1, Step: 1}, } decrByCmdMeta = DiceCmdMeta{ Name: "DECRBY", @@ -574,9 +577,10 @@ var ( The value for the queried key should be of integer format, if not, DECRBY returns an encoded error response. evalDECRBY returns the decremented value for the key after applying the specified decrement if there are no errors.`, - Eval: evalDECRBY, - Arity: 3, - KeySpecs: KeySpecs{BeginIndex: 1, Step: 1}, + NewEval: evalDECRBY, + IsMigrated: true, + Arity: 3, + KeySpecs: KeySpecs{BeginIndex: 1, Step: 1}, } existsCmdMeta = DiceCmdMeta{ Name: "EXISTS", @@ -980,9 +984,10 @@ var ( The value for the queried key should be of integer format, if not INCRBY returns encoded error response. evalINCRBY returns the incremented value for the key if there are no errors.`, - Eval: evalINCRBY, - Arity: 2, - KeySpecs: KeySpecs{BeginIndex: 1, Step: 1}, + NewEval: evalINCRBY, + IsMigrated: true, + Arity: 2, + KeySpecs: KeySpecs{BeginIndex: 1, Step: 1}, } getRangeCmdMeta = DiceCmdMeta{ Name: "GETRANGE", diff --git a/internal/eval/eval.go b/internal/eval/eval.go index 6c1a02521..0eae681bd 100644 --- a/internal/eval/eval.go +++ b/internal/eval/eval.go @@ -3,12 +3,9 @@ package eval import ( "bytes" "crypto/rand" - "errors" "fmt" - "log/slog" - "math" "math/big" "math/bits" @@ -66,9 +63,11 @@ const ( MultBy = "MULTBY" ) -const defaultRootPath = "$" -const maxExDuration = 9223372036854775 -const CountConst = "COUNT" +const ( + defaultRootPath = "$" + maxExDuration = 9223372036854775 + CountConst = "COUNT" +) func init() { diceCommandsCount = len(DiceCmds) @@ -659,7 +658,7 @@ func evalJSONARRPOP(args []string, store *dstore.Store) []byte { } key := args[0] - var path = defaultRootPath + path := defaultRootPath if len(args) >= 2 { path = args[1] } @@ -1096,7 +1095,6 @@ func evalJSONTOGGLE(args []string, store *dstore.Store) []byte { modified = true return newValue, true }) - if err != nil { return diceerrors.NewErrWithMessage("failed to toggle values") } @@ -1314,7 +1312,6 @@ func evalJSONNUMMULTBY(args []string, store *dstore.Store) []byte { path := args[1] // Parse the JSONPath expression expr, err := jp.ParseString(path) - if err != nil { return diceerrors.NewErrWithMessage("invalid JSONPath") } @@ -1500,7 +1497,7 @@ func evalTTL(args []string, store *dstore.Store) []byte { return diceerrors.NewErrArity("TTL") } - var key = args[0] + key := args[0] obj := store.Get(key) @@ -1525,7 +1522,7 @@ func evalTTL(args []string, store *dstore.Store) []byte { // evalDEL deletes all the specified keys in args list // returns the count of total deleted keys after encoding func evalDEL(args []string, store *dstore.Store) []byte { - var countDeleted = 0 + countDeleted := 0 if len(args) < 1 { return diceerrors.NewErrArity("DEL") @@ -1550,7 +1547,7 @@ func evalEXPIRE(args []string, store *dstore.Store) []byte { return diceerrors.NewErrArity("EXPIRE") } - var key = args[0] + key := args[0] exDurationSec, err := strconv.ParseInt(args[1], 10, 64) if err != nil { return diceerrors.NewErrWithMessage(diceerrors.IntOrOutOfRangeErr) @@ -1586,7 +1583,7 @@ func evalEXPIRETIME(args []string, store *dstore.Store) []byte { return diceerrors.NewErrArity("EXPIRETIME") } - var key = args[0] + key := args[0] obj := store.Get(key) @@ -1614,7 +1611,7 @@ func evalEXPIREAT(args []string, store *dstore.Store) []byte { return clientio.Encode(errors.New("ERR wrong number of arguments for 'expireat' command"), false) } - var key = args[0] + key := args[0] exUnixTimeSec, err := strconv.ParseInt(args[1], 10, 64) if exUnixTimeSec < 0 || exUnixTimeSec > maxExDuration { return diceerrors.NewErrExpireTime("EXPIREAT") @@ -1641,8 +1638,9 @@ func evalEXPIREAT(args []string, store *dstore.Store) []byte { // Returns Boolean False and error nil if conditions didn't met. // Returns Boolean False and error not-nil if invalid combination of subCommands or if subCommand is invalid func evaluateAndSetExpiry(subCommands []string, newExpiry int64, key string, - store *dstore.Store) (shouldSetExpiry bool, err []byte) { - var newExpInMilli = newExpiry * 1000 + store *dstore.Store, +) (shouldSetExpiry bool, err []byte) { + newExpInMilli := newExpiry * 1000 var prevExpiry *uint64 = nil var nxCmd, xxCmd, gtCmd, ltCmd bool @@ -1723,184 +1721,6 @@ func evalHELLO(args []string, store *dstore.Store) []byte { return clientio.Encode(resp, false) } -// evalINCR increments the value of the specified key in args by 1, -// if the key exists and the value is integer format. -// The key should be the only param in args. -// If the key does not exist, new key is created with value 0, -// the value of the new key is then incremented. -// The value for the queried key should be of integer format, -// if not evalINCR returns encoded error response. -// evalINCR returns the incremented value for the key if there are no errors. -func evalINCR(args []string, store *dstore.Store) []byte { - if len(args) != 1 { - return diceerrors.NewErrArity("INCR") - } - return incrDecrCmd(args, 1, store) -} - -// evalINCRBYFLOAT increments the value of the key in args by the specified increment, -// if the key exists and the value is a number. -// The key should be the first parameter in args, and the increment should be the second parameter. -// If the key does not exist, a new key is created with increment's value. -// If the value at the key is a string, it should be parsable to float64, -// if not evalINCRBYFLOAT returns an error response. -// evalINCRBYFLOAT returns the incremented value for the key after applying the specified increment if there are no errors. -func evalINCRBYFLOAT(args []string, store *dstore.Store) []byte { - if len(args) != 2 { - return diceerrors.NewErrArity("INCRBYFLOAT") - } - incr, err := strconv.ParseFloat(strings.TrimSpace(args[1]), 64) - - if err != nil { - return diceerrors.NewErrWithMessage(diceerrors.IntOrFloatErr) - } - return incrByFloatCmd(args, incr, store) -} - -// evalDECR decrements the value of the specified key in args by 1, -// if the key exists and the value is integer format. -// The key should be the only param in args. -// If the key does not exist, new key is created with value 0, -// the value of the new key is then decremented. -// The value for the queried key should be of integer format, -// if not evalDECR returns encoded error response. -// evalDECR returns the decremented value for the key if there are no errors. -func evalDECR(args []string, store *dstore.Store) []byte { - if len(args) != 1 { - return diceerrors.NewErrArity("DECR") - } - return incrDecrCmd(args, -1, store) -} - -// evalDECRBY decrements the value of the specified key in args by the specified decrement, -// if the key exists and the value is integer format. -// The key should be the first parameter in args, and the decrement should be the second parameter. -// If the key does not exist, new key is created with value 0, -// the value of the new key is then decremented by specified decrement. -// The value for the queried key should be of integer format, -// if not evalDECRBY returns an encoded error response. -// evalDECRBY returns the decremented value for the key after applying the specified decrement if there are no errors. -func evalDECRBY(args []string, store *dstore.Store) []byte { - if len(args) != 2 { - return diceerrors.NewErrArity("DECRBY") - } - decrementAmount, err := strconv.ParseInt(args[1], 10, 64) - if err != nil { - return diceerrors.NewErrWithMessage(diceerrors.IntOrOutOfRangeErr) - } - return incrDecrCmd(args, -decrementAmount, store) -} - -// INCRBY increments the value of the specified key in args by increment integer specified, -// if the key exists and the value is integer format. -// The key and the increment integer should be the only param in args. -// If the key does not exist, new key is created with value 0, -// the value of the new key is then incremented. -// The value for the queried key should be of integer format, -// if not INCRBY returns encoded error response. -// evalINCRBY returns the incremented value for the key if there are no errors. -func evalINCRBY(args []string, store *dstore.Store) []byte { - if len(args) != 2 { - return diceerrors.NewErrArity("INCRBY") - } - incrementAmount, err := strconv.ParseInt(args[1], 10, 64) - if err != nil { - return diceerrors.NewErrWithMessage(diceerrors.IntOrOutOfRangeErr) - } - return incrDecrCmd(args, incrementAmount, store) -} - -func incrDecrCmd(args []string, incr int64, store *dstore.Store) []byte { - key := args[0] - obj := store.Get(key) - if obj == nil { - obj = store.NewObj(int64(0), -1, object.ObjTypeInt, object.ObjEncodingInt) - store.Put(key, obj) - } - - // If the object exists, check if it is a set object. - if err := object.AssertType(obj.TypeEncoding, object.ObjTypeSet); err == nil { - return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr) - } - - if err := object.AssertType(obj.TypeEncoding, object.ObjTypeInt); err != nil { - return diceerrors.NewErrWithFormattedMessage(diceerrors.IntOrOutOfRangeErr) - } - - if err := object.AssertEncoding(obj.TypeEncoding, object.ObjEncodingInt); err != nil { - return diceerrors.NewErrWithFormattedMessage(diceerrors.IntOrOutOfRangeErr) - } - - i, _ := obj.Value.(int64) - // check overflow - if (incr < 0 && i < 0 && incr < (math.MinInt64-i)) || - (incr > 0 && i > 0 && incr > (math.MaxInt64-i)) { - return diceerrors.NewErrWithMessage(diceerrors.IncrDecrOverflowErr) - } - - i += incr - obj.Value = i - - return clientio.Encode(i, false) -} - -func incrByFloatCmd(args []string, incr float64, store *dstore.Store) []byte { - key := args[0] - obj := store.Get(key) - - // If the key does not exist store set the key equal to the increment and return early - if obj == nil { - strValue := formatFloat(incr, false) - oType, oEnc := deduceTypeEncoding(strValue) - obj = store.NewObj(strValue, -1, oType, oEnc) - store.Put(key, obj) - return clientio.Encode(obj.Value, false) - } - - // Return with error if the obj type is not string or Int - errString := object.AssertType(obj.TypeEncoding, object.ObjTypeString) - errInt := object.AssertType(obj.TypeEncoding, object.ObjTypeInt) - if errString != nil && errInt != nil { - return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr) - } - - value, err := floatValue(obj.Value) - if err != nil { - return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr) - } - value += incr - if math.IsInf(value, 0) { - return diceerrors.NewErrWithFormattedMessage(diceerrors.ValOutOfRangeErr) - } - strValue := formatFloat(value, true) - - oType, oEnc := deduceTypeEncoding(strValue) - - // Remove the trailing decimal for integer values - // to maintain consistency with redis - obj.Value = strings.TrimSuffix(strValue, ".0") - obj.TypeEncoding = oType | oEnc - - return clientio.Encode(obj.Value, false) -} - -// floatValue returns the float64 value for an interface which -// contains either a string or an int. -func floatValue(value interface{}) (float64, error) { - switch raw := value.(type) { - case string: - parsed, err := strconv.ParseFloat(raw, 64) - if err != nil { - return 0, err - } - return parsed, nil - case int64: - return float64(raw), nil - } - - return 0, fmt.Errorf(diceerrors.IntOrFloatErr) -} - // evalINFO creates a buffer with the info of total keys per db // Returns the encoded buffer as response func evalINFO(args []string, store *dstore.Store) []byte { @@ -2769,7 +2589,7 @@ func evalGETEX(args []string, store *dstore.Store) []byte { return diceerrors.NewErrArity("GETEX") } - var key = args[0] + key := args[0] // Get the key from the hash table obj := store.Get(key) @@ -2786,8 +2606,8 @@ func evalGETEX(args []string, store *dstore.Store) []byte { } var exDurationMs int64 = -1 - var state = Uninitialized - var persist = false + state := Uninitialized + persist := false for i := 1; i < len(args); i++ { arg := strings.ToUpper(args[i]) switch arg { @@ -2912,7 +2732,6 @@ func evalHSET(args []string, store *dstore.Store) []byte { } numKeys, err := insertInHashMap(args, store) - if err != nil { return err } @@ -2935,7 +2754,6 @@ func evalHMSET(args []string, store *dstore.Store) []byte { } _, err := insertInHashMap(args, store) - if err != nil { return err } @@ -3362,6 +3180,7 @@ func evalObjectIdleTime(key string, store *dstore.Store) []byte { return clientio.Encode(int64(dstore.GetIdleTime(obj.LastAccessedAt)), true) } + func evalObjectEncoding(key string, store *dstore.Store) []byte { var encodingTypeStr string @@ -3420,6 +3239,7 @@ func evalObjectEncoding(key string, store *dstore.Store) []byte { return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr) } } + func evalOBJECT(args []string, store *dstore.Store) []byte { if len(args) < 2 { return diceerrors.NewErrArity("OBJECT") @@ -3639,10 +3459,10 @@ func evalSADD(args []string, store *dstore.Store) []byte { obj := store.Get(key) lengthOfItems := len(args[1:]) - var count = 0 + count := 0 if obj == nil { var exDurationMs int64 = -1 - var keepttl = false + keepttl := false // If the object does not exist, create a new set object. value := make(map[string]struct{}, lengthOfItems) // Create a new object. @@ -3713,7 +3533,7 @@ func evalSREM(args []string, store *dstore.Store) []byte { // Get the set object from the store. obj := store.Get(key) - var count = 0 + count := 0 if obj == nil { return clientio.Encode(count, false) } @@ -3853,7 +3673,7 @@ func evalSINTER(args []string, store *dstore.Store) []byte { sets := make([]map[string]struct{}, 0, len(args)) - var empty = 0 + empty := 0 for _, arg := range args { // Get the set object from the store. @@ -4019,7 +3839,6 @@ func evalJSONNUMINCRBY(args []string, store *dstore.Store) []byte { jsonData := obj.Value // Parse the JSONPath expression expr, err := jp.ParseString(path) - if err != nil { return diceerrors.NewErrWithMessage("invalid JSONPath") } @@ -4567,7 +4386,6 @@ func evalHINCRBYFLOAT(args []string, store *dstore.Store) []byte { return diceerrors.NewErrArity("HINCRBYFLOAT") } incr, err := strconv.ParseFloat(strings.TrimSpace(args[2]), 64) - if err != nil { return diceerrors.NewErrWithMessage(diceerrors.IntOrFloatErr) } @@ -4732,7 +4550,6 @@ func evalGEODIST(args []string, store *dstore.Store) []byte { distance := geo.GetDistance(lon1, lat1, lon2, lat2) result, err := geo.ConvertDistance(distance, unit) - if err != nil { return err } diff --git a/internal/eval/eval_test.go b/internal/eval/eval_test.go index 68f2248ed..2ee70e34f 100644 --- a/internal/eval/eval_test.go +++ b/internal/eval/eval_test.go @@ -120,6 +120,10 @@ func TestEval(t *testing.T) { testEvalSINTER(t, store) testEvalOBJECTENCODING(t, store) testEvalJSONSTRAPPEND(t, store) + testEvalINCR(t, store) + testEvalINCRBY(t, store) + testEvalDECR(t, store) + testEvalDECRBY(t, store) } func testEvalPING(t *testing.T, store *dstore.Store) { @@ -150,7 +154,8 @@ func testEvalHELLO(t *testing.T, store *dstore.Store) { "id", serverID, "mode", "standalone", "role", "master", - "modules", []interface{}{}, + "modules", + []interface{}{}, } tests := map[string]evalTestCase{ @@ -164,7 +169,6 @@ func testEvalHELLO(t *testing.T, store *dstore.Store) { } func testEvalSET(t *testing.T, store *dstore.Store) { - tests := []evalTestCase{ { name: "nil value", @@ -322,7 +326,6 @@ func testEvalSET(t *testing.T, store *dstore.Store) { func testEvalGETEX(t *testing.T, store *dstore.Store) { tests := map[string]evalTestCase{ - "key val pair and valid EX": { setup: func() { key := "foo" @@ -345,11 +348,11 @@ func testEvalGETEX(t *testing.T, store *dstore.Store) { store.Put(key, obj) }, input: []string{"foo", Ex, "10000000000000000"}, - output: []byte("-ERR invalid expire time in 'getex' command\r\n")}, + output: []byte("-ERR invalid expire time in 'getex' command\r\n"), + }, "key holding json type": { setup: func() { evalJSONSET([]string{"JSONKEY", "$", "1"}, store) - }, input: []string{"JSONKEY"}, output: []byte("-WRONGTYPE Operation against a key holding the wrong kind of value\r\n"), @@ -357,7 +360,6 @@ func testEvalGETEX(t *testing.T, store *dstore.Store) { "key holding set type": { setup: func() { evalSADD([]string{"SETKEY", "FRUITS", "APPLE", "MANGO", "BANANA"}, store) - }, input: []string{"SETKEY"}, output: []byte("-WRONGTYPE Operation against a key holding the wrong kind of value\r\n"), @@ -574,7 +576,6 @@ func testEvalEXPIRE(t *testing.T, store *dstore.Store) { LastAccessedAt: uint32(time.Now().Unix()), } store.Put(key, obj) - }, input: []string{"EXISTING_KEY", strconv.FormatInt(9223372036854776, 10)}, output: []byte("-ERR invalid expire time in 'expire' command\r\n"), @@ -589,7 +590,6 @@ func testEvalEXPIRE(t *testing.T, store *dstore.Store) { LastAccessedAt: uint32(time.Now().Unix()), } store.Put(key, obj) - }, input: []string{"EXISTING_KEY", strconv.FormatInt(-1, 10)}, output: []byte("-ERR invalid expire time in 'expire' command\r\n"), @@ -603,7 +603,6 @@ func testEvalEXPIRE(t *testing.T, store *dstore.Store) { LastAccessedAt: uint32(time.Now().Unix()), } store.Put(key, obj) - }, input: []string{"EXISTING_KEY", ""}, output: []byte("-ERR value is not an integer or out of range\r\n"), @@ -617,7 +616,6 @@ func testEvalEXPIRE(t *testing.T, store *dstore.Store) { LastAccessedAt: uint32(time.Now().Unix()), } store.Put(key, obj) - }, input: []string{"EXISTING_KEY", "0.456"}, output: []byte("-ERR value is not an integer or out of range\r\n"), @@ -710,7 +708,6 @@ func testEvalEXPIREAT(t *testing.T, store *dstore.Store) { LastAccessedAt: uint32(time.Now().Unix()), } store.Put(key, obj) - }, input: []string{"EXISTING_KEY", strconv.FormatInt(9223372036854776, 10)}, output: []byte("-ERR invalid expire time in 'expireat' command\r\n"), @@ -724,7 +721,6 @@ func testEvalEXPIREAT(t *testing.T, store *dstore.Store) { LastAccessedAt: uint32(time.Now().Unix()), } store.Put(key, obj) - }, input: []string{"EXISTING_KEY", strconv.FormatInt(-1, 10)}, output: []byte("-ERR invalid expire time in 'expireat' command\r\n"), @@ -1975,7 +1971,6 @@ func testEvalJSONTOGGLE(t *testing.T, store *dstore.Store) { } obj := store.NewObj(rootData, -1, object.ObjTypeJSON, object.ObjEncodingJSON) store.Put(key, obj) - }, input: []string{"EXISTING_KEY", ".active"}, output: clientio.Encode([]interface{}{0}, false), @@ -2288,7 +2283,7 @@ func testEvalPFADD(t *testing.T, store *dstore.Store) { key, value := "EXISTING_KEY", "VALUE" oType, oEnc := deduceTypeEncoding(value) var exDurationMs int64 = -1 - var keepttl = false + keepttl := false store.Put(key, store.NewObj(value, exDurationMs, oType, oEnc), dstore.WithKeepTTL(keepttl)) }, @@ -2637,6 +2632,7 @@ func testEvalHVALS(t *testing.T, store *dstore.Store) { runEvalTests(t, tests, evalHVALS, store) } + func testEvalHSTRLEN(t *testing.T, store *dstore.Store) { tests := map[string]evalTestCase{ "wrong number of args passed": { @@ -3502,6 +3498,7 @@ func BenchmarkEvalHKEYS(b *testing.B) { evalHKEYS([]string{"KEY"}, store) } } + func BenchmarkEvalPFCOUNT(b *testing.B) { store := *dstore.NewStore(nil, nil) @@ -3573,7 +3570,6 @@ func BenchmarkEvalPFCOUNT(b *testing.B) { func testEvalDebug(t *testing.T, store *dstore.Store) { tests := map[string]evalTestCase{ - // invalid subcommand tests "no subcommand passed": { setup: func() {}, @@ -4863,144 +4859,150 @@ func testEvalFLUSHDB(t *testing.T, store *dstore.Store) { } func testEvalINCRBYFLOAT(t *testing.T, store *dstore.Store) { - tests := map[string]evalTestCase{ - "INCRBYFLOAT on a non existing key": { - input: []string{"float", "0.1"}, - output: clientio.Encode("0.1", false), + tests := []evalTestCase{ + { + name: "INCRBYFLOAT on a non existing key", + input: []string{"float", "0.1"}, + migratedOutput: EvalResponse{Result: "0.1", Error: nil}, }, - "INCRBYFLOAT on an existing key": { + { + name: "INCRBYFLOAT on an existing key", setup: func() { key := "key" value := "2.1" - obj := &object.Obj{ - Value: value, - LastAccessedAt: uint32(time.Now().Unix()), - } + obj := store.NewObj(value, -1, object.ObjTypeString, object.ObjEncodingRaw) store.Put(key, obj) }, - input: []string{"key", "0.1"}, - output: clientio.Encode("2.2", false), + input: []string{"key", "0.1"}, + migratedOutput: EvalResponse{Result: "2.2", Error: nil}, }, - "INCRBYFLOAT on a key with integer value": { + { + name: "INCRBYFLOAT on a key with integer value", setup: func() { key := "key" value := "2" - obj := &object.Obj{ - Value: value, - LastAccessedAt: uint32(time.Now().Unix()), - } + obj := store.NewObj(value, -1, object.ObjTypeInt, object.ObjEncodingInt) store.Put(key, obj) }, - input: []string{"key", "0.1"}, - output: clientio.Encode("2.1", false), + input: []string{"key", "0.1"}, + migratedOutput: EvalResponse{Result: "2.1", Error: nil}, }, - "INCRBYFLOAT by a negative increment": { + { + name: "INCRBYFLOAT by a negative increment", setup: func() { key := "key" value := "2" - obj := &object.Obj{ - Value: value, - LastAccessedAt: uint32(time.Now().Unix()), - } + obj := store.NewObj(value, -1, object.ObjTypeInt, object.ObjEncodingInt) store.Put(key, obj) }, - input: []string{"key", "-0.1"}, - output: clientio.Encode("1.9", false), + input: []string{"key", "-0.1"}, + migratedOutput: EvalResponse{Result: "1.9", Error: nil}, }, - "INCRBYFLOAT by a scientific notation increment": { + { + name: "INCRBYFLOAT by a scientific notation increment", setup: func() { key := "key" value := "1" - obj := &object.Obj{ - Value: value, - LastAccessedAt: uint32(time.Now().Unix()), - } + obj := store.NewObj(value, -1, object.ObjTypeInt, object.ObjEncodingInt) store.Put(key, obj) }, - input: []string{"key", "1e-2"}, - output: clientio.Encode("1.01", false), + input: []string{"key", "1e-2"}, + migratedOutput: EvalResponse{Result: "1.01", Error: nil}, }, - "INCRBYFLOAT on a key holding a scientific notation value": { + { + name: "INCRBYFLOAT on a key holding a scientific notation value", setup: func() { key := "key" value := "1e2" - obj := &object.Obj{ - Value: value, - LastAccessedAt: uint32(time.Now().Unix()), - } + obj := store.NewObj(value, -1, object.ObjTypeString, object.ObjEncodingEmbStr) store.Put(key, obj) }, - input: []string{"key", "1e-1"}, - output: clientio.Encode("100.1", false), + input: []string{"key", "1e-1"}, + migratedOutput: EvalResponse{Result: "100.1", Error: nil}, }, - "INCRBYFLOAT by an negative increment of the same value": { + { + name: "INCRBYFLOAT by an negative increment of the same value", setup: func() { key := "key" value := "0.1" - obj := &object.Obj{ - Value: value, - LastAccessedAt: uint32(time.Now().Unix()), - } + obj := store.NewObj(value, -1, object.ObjTypeString, object.ObjEncodingEmbStr) store.Put(key, obj) }, - input: []string{"key", "-0.1"}, - output: clientio.Encode("0", false), + input: []string{"key", "-0.1"}, + migratedOutput: EvalResponse{Result: "0", Error: nil}, }, - "INCRBYFLOAT on a key with spaces": { + { + name: "INCRBYFLOAT on a key with spaces", setup: func() { key := "key" value := " 2 " - obj := &object.Obj{ - Value: value, - LastAccessedAt: uint32(time.Now().Unix()), - } + obj := store.NewObj(value, -1, object.ObjTypeString, object.ObjEncodingEmbStr) store.Put(key, obj) }, - input: []string{"key", "0.1"}, - output: []byte("-WRONGTYPE Operation against a key holding the wrong kind of value\r\n"), + input: []string{"key", "0.1"}, + migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrGeneral("value is not a valid float")}, }, - "INCRBYFLOAT on a key with non numeric value": { + { + name: "INCRBYFLOAT on a key with non numeric value", setup: func() { key := "key" value := "string" - obj := &object.Obj{ - Value: value, - LastAccessedAt: uint32(time.Now().Unix()), - } + obj := store.NewObj(value, -1, object.ObjTypeString, object.ObjEncodingEmbStr) store.Put(key, obj) }, - input: []string{"key", "0.1"}, - output: []byte("-WRONGTYPE Operation against a key holding the wrong kind of value\r\n"), + input: []string{"key", "0.1"}, + migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrGeneral("value is not a valid float")}, }, - "INCRBYFLOAT by a non numeric increment": { + { + name: "INCRBYFLOAT by a non numeric increment", setup: func() { key := "key" value := "2.0" - obj := &object.Obj{ - Value: value, - LastAccessedAt: uint32(time.Now().Unix()), - } + obj := store.NewObj(value, -1, object.ObjTypeString, object.ObjEncodingEmbStr) store.Put(key, obj) }, - input: []string{"key", "a"}, - output: []byte("-ERR value is not an integer or a float\r\n"), + input: []string{"key", "a"}, + migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrGeneral("value is not a valid float")}, }, - "INCRBYFLOAT by a number that would turn float64 to Inf": { + { + name: "INCRBYFLOAT by a number that would turn float64 to Inf", setup: func() { key := "key" value := "1e308" - obj := &object.Obj{ - Value: value, - LastAccessedAt: uint32(time.Now().Unix()), - } + obj := store.NewObj(value, -1, object.ObjTypeString, object.ObjEncodingEmbStr) store.Put(key, obj) }, - input: []string{"key", "1e308"}, - output: []byte("-ERR value is out of range\r\n"), + input: []string{"key", "1e308"}, + migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrValueOutOfRange}, }, } - runEvalTests(t, tests, evalINCRBYFLOAT, store) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + store = setupTest(store) + if tt.setup != nil { + tt.setup() + } + + response := evalINCRBYFLOAT(tt.input, store) + fmt.Printf("Response: %v | Expected: %v\n", *response, tt.migratedOutput) + + // Handle comparison for byte slices + if b, ok := response.Result.([]byte); ok && tt.migratedOutput.Result != nil { + if expectedBytes, ok := tt.migratedOutput.Result.([]byte); ok { + testifyAssert.True(t, bytes.Equal(b, expectedBytes), "expected and actual byte slices should be equal") + } + } else { + assert.Equal(t, tt.migratedOutput.Result, response.Result) + } + + if tt.migratedOutput.Error != nil { + testifyAssert.EqualError(t, response.Error, tt.migratedOutput.Error.Error()) + } else { + testifyAssert.NoError(t, response.Error) + } + }) + } } func BenchmarkEvalINCRBYFLOAT(b *testing.B) { @@ -5200,7 +5202,6 @@ func testEvalHRANDFIELD(t *testing.T, store *dstore.Store) { } store.Put(key, obj) - }, input: []string{"KEY_MOCK", "2"}, validator: func(output []byte) { @@ -5233,7 +5234,6 @@ func testEvalHRANDFIELD(t *testing.T, store *dstore.Store) { } store.Put(key, obj) - }, input: []string{"KEY_MOCK", "2", WithValues}, validator: func(output []byte) { @@ -6089,6 +6089,7 @@ func testEvalBitField(t *testing.T, store *dstore.Store) { } runEvalTests(t, testCases, evalBITFIELD, store) } + func testEvalHINCRBYFLOAT(t *testing.T, store *dstore.Store) { tests := map[string]evalTestCase{ "HINCRBYFLOAT on a non-existing key and field": { @@ -6507,6 +6508,7 @@ func testEvalSINTER(t *testing.T, store *dstore.Store) { runEvalTests(t, tests, evalSINTER, store) } + func testEvalOBJECTENCODING(t *testing.T, store *dstore.Store) { tests := map[string]evalTestCase{ "nil value": { @@ -6540,6 +6542,7 @@ func testEvalOBJECTENCODING(t *testing.T, store *dstore.Store) { runEvalTests(t, tests, evalOBJECT, store) } + func testEvalJSONSTRAPPEND(t *testing.T, store *dstore.Store) { tests := map[string]evalTestCase{ "append to single field": { @@ -6596,3 +6599,355 @@ func BenchmarkEvalJSONSTRAPPEND(b *testing.B) { evalJSONSTRAPPEND([]string{"doc1", "$..a", "\"bar\""}, store) } } + +func testEvalINCR(t *testing.T, store *dstore.Store) { + tests := []evalTestCase{ + { + name: "INCR key does not exist", + input: []string{"KEY1"}, + migratedOutput: EvalResponse{Result: int64(1), Error: nil}, + }, + { + name: "INCR key exists", + setup: func() { + key := "KEY2" + obj := store.NewObj(int64(1), -1, object.ObjTypeInt, object.ObjEncodingInt) + store.Put(key, obj) + }, + input: []string{"KEY2"}, + migratedOutput: EvalResponse{Result: int64(2), Error: nil}, + }, + { + name: "INCR key holding string value", + setup: func() { + key := "KEY3" + obj := store.NewObj("VAL1", -1, object.ObjTypeString, object.ObjEncodingEmbStr) + store.Put(key, obj) + }, + input: []string{"KEY3"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR value is not an integer or out of range")}, + }, + { + name: "INCR key holding SET type", + setup: func() { + evalSADD([]string{"SET1", "1", "2", "3"}, store) + }, + input: []string{"SET1"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("WRONGTYPE Operation against a key holding the wrong kind of value")}, + }, + { + name: "INCR key holding MAP type", + setup: func() { + evalHSET([]string{"MAP1", "a", "1", "b", "2", "c", "3"}, store) + }, + input: []string{"MAP1"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("WRONGTYPE Operation against a key holding the wrong kind of value")}, + }, + { + name: "INCR More than one args passed", + input: []string{"KEY4", "ARG2"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'incr' command")}, + }, + { + name: "INCR Max Overflow", + setup: func() { + key := "KEY5" + obj := store.NewObj(int64(math.MaxInt64), -1, object.ObjTypeInt, object.ObjEncodingInt) + store.Put(key, obj) + }, + input: []string{"KEY5"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR increment or decrement would overflow")}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + store = setupTest(store) + if tt.setup != nil { + tt.setup() + } + + response := evalINCR(tt.input, store) + fmt.Printf("Response: %v | Expected: %v\n", *response, tt.migratedOutput) + + // Handle comparison for byte slices + if b, ok := response.Result.([]byte); ok && tt.migratedOutput.Result != nil { + if expectedBytes, ok := tt.migratedOutput.Result.([]byte); ok { + testifyAssert.True(t, bytes.Equal(b, expectedBytes), "expected and actual byte slices should be equal") + } + } else { + assert.Equal(t, tt.migratedOutput.Result, response.Result) + } + + if tt.migratedOutput.Error != nil { + testifyAssert.EqualError(t, response.Error, tt.migratedOutput.Error.Error()) + } else { + testifyAssert.NoError(t, response.Error) + } + }) + } +} + +func testEvalINCRBY(t *testing.T, store *dstore.Store) { + tests := []evalTestCase{ + { + name: "INCRBY key does not exist", + input: []string{"KEY1", "2"}, + migratedOutput: EvalResponse{Result: int64(2), Error: nil}, + }, + { + name: "INCRBY key exists", + setup: func() { + key := "KEY2" + obj := store.NewObj(int64(1), -1, object.ObjTypeInt, object.ObjEncodingInt) + store.Put(key, obj) + }, + input: []string{"KEY2", "3"}, + migratedOutput: EvalResponse{Result: int64(4), Error: nil}, + }, + { + name: "INCRBY key holding string value", + setup: func() { + key := "KEY3" + obj := store.NewObj("VAL1", -1, object.ObjTypeString, object.ObjEncodingEmbStr) + store.Put(key, obj) + }, + input: []string{"KEY3", "2"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR value is not an integer or out of range")}, + }, + { + name: "INCRBY key holding SET type", + setup: func() { + evalSADD([]string{"SET1", "1", "2", "3"}, store) + }, + input: []string{"SET1", "2"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("WRONGTYPE Operation against a key holding the wrong kind of value")}, + }, + { + name: "INCRBY key holding MAP type", + setup: func() { + evalHSET([]string{"MAP1", "a", "1", "b", "2", "c", "3"}, store) + }, + input: []string{"MAP1", "2"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("WRONGTYPE Operation against a key holding the wrong kind of value")}, + }, + { + name: "INCRBY Wrong number of args passed", + input: []string{"KEY4"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'incrby' command")}, + }, + { + name: "INCRBY Max Overflow", + setup: func() { + key := "KEY5" + obj := store.NewObj(int64(math.MaxInt64-3), -1, object.ObjTypeInt, object.ObjEncodingInt) + store.Put(key, obj) + }, + input: []string{"KEY5", "4"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR increment or decrement would overflow")}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + store = setupTest(store) + if tt.setup != nil { + tt.setup() + } + + response := evalINCRBY(tt.input, store) + fmt.Printf("Response: %v | Expected: %v\n", *response, tt.migratedOutput) + + // Handle comparison for byte slices + if b, ok := response.Result.([]byte); ok && tt.migratedOutput.Result != nil { + if expectedBytes, ok := tt.migratedOutput.Result.([]byte); ok { + testifyAssert.True(t, bytes.Equal(b, expectedBytes), "expected and actual byte slices should be equal") + } + } else { + assert.Equal(t, tt.migratedOutput.Result, response.Result) + } + + if tt.migratedOutput.Error != nil { + testifyAssert.EqualError(t, response.Error, tt.migratedOutput.Error.Error()) + } else { + testifyAssert.NoError(t, response.Error) + } + }) + } +} + +func testEvalDECR(t *testing.T, store *dstore.Store) { + tests := []evalTestCase{ + { + name: "DECR key does not exist", + input: []string{"KEY1"}, + migratedOutput: EvalResponse{Result: int64(-1), Error: nil}, + }, + { + name: "DECR key exists", + setup: func() { + key := "KEY2" + obj := store.NewObj(int64(1), -1, object.ObjTypeInt, object.ObjEncodingInt) + store.Put(key, obj) + }, + input: []string{"KEY2"}, + migratedOutput: EvalResponse{Result: int64(0), Error: nil}, + }, + { + name: "DECR key holding string value", + setup: func() { + key := "KEY3" + obj := store.NewObj("VAL1", -1, object.ObjTypeString, object.ObjEncodingEmbStr) + store.Put(key, obj) + }, + input: []string{"KEY3"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR value is not an integer or out of range")}, + }, + { + name: "DECR key holding SET type", + setup: func() { + evalSADD([]string{"SET1", "1", "2", "3"}, store) + }, + input: []string{"SET1"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("WRONGTYPE Operation against a key holding the wrong kind of value")}, + }, + { + name: "DECR key holding MAP type", + setup: func() { + evalHSET([]string{"MAP1", "a", "1", "b", "2", "c", "3"}, store) + }, + input: []string{"MAP1"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("WRONGTYPE Operation against a key holding the wrong kind of value")}, + }, + { + name: "DECR More than one args passed", + input: []string{"KEY4", "ARG2"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'decr' command")}, + }, + { + name: "DECR Min Overflow", + setup: func() { + key := "KEY5" + obj := store.NewObj(int64(math.MinInt64), -1, object.ObjTypeInt, object.ObjEncodingInt) + store.Put(key, obj) + }, + input: []string{"KEY5"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR increment or decrement would overflow")}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + store = setupTest(store) + if tt.setup != nil { + tt.setup() + } + + response := evalDECR(tt.input, store) + fmt.Printf("Response: %v | Expected: %v\n", *response, tt.migratedOutput) + + // Handle comparison for byte slices + if b, ok := response.Result.([]byte); ok && tt.migratedOutput.Result != nil { + if expectedBytes, ok := tt.migratedOutput.Result.([]byte); ok { + testifyAssert.True(t, bytes.Equal(b, expectedBytes), "expected and actual byte slices should be equal") + } + } else { + assert.Equal(t, tt.migratedOutput.Result, response.Result) + } + + if tt.migratedOutput.Error != nil { + testifyAssert.EqualError(t, response.Error, tt.migratedOutput.Error.Error()) + } else { + testifyAssert.NoError(t, response.Error) + } + }) + } +} + +func testEvalDECRBY(t *testing.T, store *dstore.Store) { + tests := []evalTestCase{ + { + name: "DECRBY key does not exist", + input: []string{"KEY1", "2"}, + migratedOutput: EvalResponse{Result: int64(-2), Error: nil}, + }, + { + name: "DECRBY key exists", + setup: func() { + key := "KEY2" + obj := store.NewObj(int64(1), -1, object.ObjTypeInt, object.ObjEncodingInt) + store.Put(key, obj) + }, + input: []string{"KEY2", "3"}, + migratedOutput: EvalResponse{Result: int64(-2), Error: nil}, + }, + { + name: "DECRBY key holding string value", + setup: func() { + key := "KEY3" + obj := store.NewObj("VAL1", -1, object.ObjTypeString, object.ObjEncodingEmbStr) + store.Put(key, obj) + }, + input: []string{"KEY3", "2"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR value is not an integer or out of range")}, + }, + { + name: "DECRBY key holding SET type", + setup: func() { + evalSADD([]string{"SET1", "1", "2", "3"}, store) + }, + input: []string{"SET1", "2"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("WRONGTYPE Operation against a key holding the wrong kind of value")}, + }, + { + name: "DECRBY key holding MAP type", + setup: func() { + evalHSET([]string{"MAP1", "a", "1", "b", "2", "c", "3"}, store) + }, + input: []string{"MAP1", "2"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("WRONGTYPE Operation against a key holding the wrong kind of value")}, + }, + { + name: "DECRBY Wrong number of args passed", + input: []string{"KEY4"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'decrby' command")}, + }, + { + name: "DECRBY Min Overflow", + setup: func() { + key := "KEY5" + obj := store.NewObj(int64(math.MinInt64+3), -1, object.ObjTypeInt, object.ObjEncodingInt) + store.Put(key, obj) + }, + input: []string{"KEY5", "4"}, + migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR increment or decrement would overflow")}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + store = setupTest(store) + if tt.setup != nil { + tt.setup() + } + + response := evalDECRBY(tt.input, store) + fmt.Printf("Response: %v | Expected: %v\n", *response, tt.migratedOutput) + + // Handle comparison for byte slices + if b, ok := response.Result.([]byte); ok && tt.migratedOutput.Result != nil { + if expectedBytes, ok := tt.migratedOutput.Result.([]byte); ok { + testifyAssert.True(t, bytes.Equal(b, expectedBytes), "expected and actual byte slices should be equal") + } + } else { + assert.Equal(t, tt.migratedOutput.Result, response.Result) + } + + if tt.migratedOutput.Error != nil { + testifyAssert.EqualError(t, response.Error, tt.migratedOutput.Error.Error()) + } else { + testifyAssert.NoError(t, response.Error) + } + }) + } +} diff --git a/internal/eval/store_eval.go b/internal/eval/store_eval.go index 684d244d5..5dcc2d9bb 100644 --- a/internal/eval/store_eval.go +++ b/internal/eval/store_eval.go @@ -1,6 +1,7 @@ package eval import ( + "fmt" "math" "strconv" "strings" @@ -996,6 +997,236 @@ func evalPFMERGE(args []string, store *dstore.Store) *EvalResponse { } } +// evalINCR increments the value of the specified key in args by 1, +// if the key exists and the value is integer format. +// The key should be the only param in args. +// If the key does not exist, new key is created with value 0, +// the value of the new key is then incremented. +// The value for the queried key should be of integer format, +// if not evalINCR returns encoded error response. +// evalINCR returns the incremented value for the key if there are no errors. +func evalINCR(args []string, store *dstore.Store) *EvalResponse { + if len(args) != 1 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("INCR"), + } + } + + return incrDecrCmd(args, 1, store) +} + +// INCRBY increments the value of the specified key in args by increment integer specified, +// if the key exists and the value is integer format. +// The key and the increment integer should be the only param in args. +// If the key does not exist, new key is created with value 0, +// the value of the new key is then incremented. +// The value for the queried key should be of integer format, +// if not INCRBY returns error response. +// evalINCRBY returns the incremented value for the key if there are no errors. +func evalINCRBY(args []string, store *dstore.Store) *EvalResponse { + if len(args) != 2 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("INCRBY"), + } + } + + incrAmount, err := strconv.ParseInt(args[1], 10, 64) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrIntegerOutOfRange, + } + } + return incrDecrCmd(args, incrAmount, store) +} + +// evalDECR decrements the value of the specified key in args by 1, +// if the key exists and the value is integer format. +// The key should be the only param in args. +// If the key does not exist, new key is created with value 0, +// the value of the new key is then decremented. +// The value for the queried key should be of integer format, +// if not evalDECR returns error response. +// evalDECR returns the decremented value for the key if there are no errors. +func evalDECR(args []string, store *dstore.Store) *EvalResponse { + if len(args) != 1 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("DECR"), + } + } + return incrDecrCmd(args, -1, store) +} + +// evalDECRBY decrements the value of the specified key in args by the specified decrement, +// if the key exists and the value is integer format. +// The key should be the first parameter in args, and the decrement should be the second parameter. +// If the key does not exist, new key is created with value 0, +// the value of the new key is then decremented by specified decrement. +// The value for the queried key should be of integer format, +// if not evalDECRBY returns an error response. +// evalDECRBY returns the decremented value for the key after applying the specified decrement if there are no errors. +func evalDECRBY(args []string, store *dstore.Store) *EvalResponse { + if len(args) != 2 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("DECRBY"), + } + } + decrAmount, err := strconv.ParseInt(args[1], 10, 64) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrIntegerOutOfRange, + } + } + return incrDecrCmd(args, -decrAmount, store) +} + +func incrDecrCmd(args []string, incr int64, store *dstore.Store) *EvalResponse { + key := args[0] + obj := store.Get(key) + if obj == nil { + obj = store.NewObj(incr, -1, object.ObjTypeInt, object.ObjEncodingInt) + store.Put(key, obj) + return &EvalResponse{ + Result: incr, + Error: nil, + } + } + // if the type is not KV : return wrong type error + // if the encoding or type is not int : return value is not an int error + errStr := object.AssertType(obj.TypeEncoding, object.ObjTypeString) + if errStr == nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrIntegerOutOfRange, + } + } + + errTypeInt := object.AssertType(obj.TypeEncoding, object.ObjTypeInt) + errEncInt := object.AssertEncoding(obj.TypeEncoding, object.ObjEncodingInt) + if errEncInt != nil || errTypeInt != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + i, _ := obj.Value.(int64) + if (incr < 0 && i < 0 && incr < (math.MinInt64-i)) || + (incr > 0 && i > 0 && incr > (math.MaxInt64-i)) { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrOverflow, + } + } + + i += incr + obj.Value = i + return &EvalResponse{ + Result: i, + Error: nil, + } +} + +// evalINCRBYFLOAT increments the value of the key in args by the specified increment, +// if the key exists and the value is a number. +// The key should be the first parameter in args, and the increment should be the second parameter. +// If the key does not exist, a new key is created with increment's value. +// If the value at the key is a string, it should be parsable to float64, +// if not evalINCRBYFLOAT returns an error response. +// evalINCRBYFLOAT returns the incremented value for the key after applying the specified increment if there are no errors. +func evalINCRBYFLOAT(args []string, store *dstore.Store) *EvalResponse { + if len(args) != 2 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("INCRBYFLOAT"), + } + } + incr, err := strconv.ParseFloat(strings.TrimSpace(args[1]), 64) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrGeneral("value is not a valid float"), + } + } + return incrByFloatCmd(args, incr, store) +} + +func incrByFloatCmd(args []string, incr float64, store *dstore.Store) *EvalResponse { + key := args[0] + obj := store.Get(key) + + if obj == nil { + strValue := formatFloat(incr, false) + oType, oEnc := deduceTypeEncoding(strValue) + obj = store.NewObj(strValue, -1, oType, oEnc) + store.Put(key, obj) + return &EvalResponse{ + Result: strValue, + Error: nil, + } + } + + errString := object.AssertType(obj.TypeEncoding, object.ObjTypeString) + errInt := object.AssertType(obj.TypeEncoding, object.ObjTypeInt) + if errString != nil && errInt != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + value, err := floatValue(obj.Value) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrGeneral("value is not a valid float"), + } + } + value += incr + if math.IsInf(value, 0) { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrValueOutOfRange, + } + } + strValue := formatFloat(value, true) + + oType, oEnc := deduceTypeEncoding(strValue) + + // Remove the trailing decimal for integer values + // to maintain consistency with redis + strValue = strings.TrimSuffix(strValue, ".0") + + obj.Value = strValue + obj.TypeEncoding = oType | oEnc + + return &EvalResponse{ + Result: strValue, + Error: nil, + } +} + +// floatValue returns the float64 value for an interface which +// contains either a string or an int. +func floatValue(value interface{}) (float64, error) { + switch raw := value.(type) { + case string: + parsed, err := strconv.ParseFloat(raw, 64) + if err != nil { + return 0, err + } + return parsed, nil + case int64: + return float64(raw), nil + } + + return 0, fmt.Errorf(diceerrors.IntOrFloatErr) +} + // ZPOPMIN Removes and returns the member with the lowest score from the sorted set at the specified key. // If multiple members have the same score, the one that comes first alphabetically is returned. // You can also specify a count to remove and return multiple members at once. diff --git a/internal/server/cmd_meta.go b/internal/server/cmd_meta.go index cfdb314de..e26efa8be 100644 --- a/internal/server/cmd_meta.go +++ b/internal/server/cmd_meta.go @@ -106,6 +106,27 @@ var ( CmdType: SingleShard, } + incrCmdMeta = CmdsMeta{ + Cmd: "INCR", + CmdType: SingleShard, + } + incrByCmdMeta = CmdsMeta{ + Cmd: "INCRBY", + CmdType: SingleShard, + } + decrCmdMeta = CmdsMeta{ + Cmd: "DECR", + CmdType: SingleShard, + } + decrByCmdMeta = CmdsMeta{ + Cmd: "DECRBY", + CmdType: SingleShard, + } + incrByFloatCmdMeta = CmdsMeta{ + Cmd: "INCRBYFLOAT", + CmdType: SingleShard, + } + // Metadata for multishard commands would go here. // These commands require both breakup and gather logic. @@ -132,5 +153,11 @@ func init() { WorkerCmdsMeta["ZPOPMIN"] = zpopminCmdMeta WorkerCmdsMeta["PFCOUNT"] = pfcountCmdMeta WorkerCmdsMeta["PFMERGE"] = pfmergeCmdMeta + WorkerCmdsMeta["INCR"] = incrCmdMeta + WorkerCmdsMeta["INCRBY"] = incrByCmdMeta + WorkerCmdsMeta["INCR"] = incrCmdMeta + WorkerCmdsMeta["DECR"] = decrCmdMeta + WorkerCmdsMeta["DECRBY"] = decrByCmdMeta + WorkerCmdsMeta["INCRBYFLOAT"] = incrByFloatCmdMeta // Additional commands (multishard, custom) can be added here as needed. } diff --git a/internal/worker/cmd_meta.go b/internal/worker/cmd_meta.go index aeed09a5f..a237fc182 100644 --- a/internal/worker/cmd_meta.go +++ b/internal/worker/cmd_meta.go @@ -74,6 +74,11 @@ const ( CmdPFAdd = "PFADD" CmdPFCount = "PFCOUNT" CmdPFMerge = "PFMERGE" + CmdIncr = "INCR" + CmdIncrBy = "INCRBY" + CmdDecr = "DECR" + CmdDecrBy = "DECRBY" + CmdIncrByFloat = "INCRBYFLOAT" ) type CmdMeta struct { @@ -195,6 +200,22 @@ var CommandsMeta = map[string]CmdMeta{ CmdZRange: { CmdType: SingleShard, }, + + CmdIncr: { + CmdType: SingleShard, + }, + CmdIncrBy: { + CmdType: SingleShard, + }, + CmdDecr: { + CmdType: SingleShard, + }, + CmdDecrBy: { + CmdType: SingleShard, + }, + CmdIncrByFloat: { + CmdType: SingleShard, + }, CmdZPopMin: { CmdType: SingleShard, },