From 876ac0b4baf57a2fb788a47ee2273a19d790200d Mon Sep 17 00:00:00 2001 From: Sahil Date: Tue, 25 Jun 2024 02:11:30 +0530 Subject: [PATCH] feat: added DECRBY command --- echovault/api_generic.go | 25 +++++ echovault/api_generic_test.go | 69 ++++++++++++++ internal/modules/generic/commands.go | 80 ++++++++++++++-- internal/modules/generic/commands_test.go | 106 ++++++++++++++++++++++ internal/modules/generic/key_funcs.go | 9 ++ 5 files changed, 282 insertions(+), 7 deletions(-) diff --git a/echovault/api_generic.go b/echovault/api_generic.go index 8a685b7..c9a06a4 100644 --- a/echovault/api_generic.go +++ b/echovault/api_generic.go @@ -461,3 +461,28 @@ func (server *EchoVault) Decr(key string) (int, error) { // Parse the integer response return internal.ParseIntegerResponse(b) } + +// DecrBy decrements the integer value of the specified key by the given increment. +// If the key does not exist, it is created with an initial value of 0 before decrementing. +// If the value stored at the key is not an integer, an error is returned. +// +// Parameters: +// - `key` (string): The key whose value is to be decremented. +// - `increment` (int): The amount by which to decrement the key's value. This can be a positive or negative integer. +// +// Returns: +// - (int): The new value of the key after the decrement operation. + +func (server *EchoVault) DecrBy(key string, value string) (int, error) { + // Construct the command + cmd := []string{"DECRBY", key, value} + + // Execute the command + b, err := server.handleCommand(server.context, internal.EncodeCommand(cmd), nil, false, true) + if err != nil { + return 0, err + } + + // Parse the integer response + return internal.ParseIntegerResponse(b) +} diff --git a/echovault/api_generic_test.go b/echovault/api_generic_test.go index 1edd62f..d3510eb 100644 --- a/echovault/api_generic_test.go +++ b/echovault/api_generic_test.go @@ -1047,3 +1047,72 @@ func TestEchoVault_DECR(t *testing.T) { }) } } + +func TestEchoVault_DECRBY(t *testing.T) { + server := createEchoVault() + + tests := []struct { + name string + key string + decrement string + presetValues map[string]internal.KeyData + want int + wantErr bool + }{ + { + name: "1. Decrement non-existent key by 4", + key: "DecrByKey1", + decrement: "4", + presetValues: nil, + want: -4, + wantErr: false, + }, + { + name: "2. Decrement existing key with integer value by 3", + key: "DecrByKey2", + decrement: "3", + presetValues: map[string]internal.KeyData{ + "DecrByKey2": {Value: "-5"}, + }, + want: -8, + wantErr: false, + }, + { + name: "3. Decrement existing key with non-integer value by 2", + key: "DecrByKey3", + decrement: "2", + presetValues: map[string]internal.KeyData{ + "DecrByKey3": {Value: "not_an_int"}, + }, + want: 0, + wantErr: true, + }, + { + name: "4. Decrement existing key with int64 value by 7", + key: "DecrByKey4", + decrement: "7", + presetValues: map[string]internal.KeyData{ + "DecrByKey4": {Value: int64(10)}, + }, + want: 3, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.presetValues != nil { + for k, d := range tt.presetValues { + presetKeyData(server, context.Background(), k, d) + } + } + got, err := server.DecrBy(tt.key, tt.decrement) + if (err != nil) != tt.wantErr { + t.Errorf("IncrBy() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("IncrBy() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/modules/generic/commands.go b/internal/modules/generic/commands.go index 03f9785..5b32446 100644 --- a/internal/modules/generic/commands.go +++ b/internal/modules/generic/commands.go @@ -477,6 +477,63 @@ func handleDecr(params internal.HandlerFuncParams) ([]byte, error) { return []byte(fmt.Sprintf(":%d\r\n", newValue)), nil } +func handleDecrBy(params internal.HandlerFuncParams) ([]byte, error) { + // Ensure command has the correct number of arguments + if len(params.Command) != 3 { + return nil, errors.New("wrong number of arguments for DECRBY") + } + + // Extract key from command + keys, err := decrKeyByFunc(params.Command) + if err != nil { + return nil, err + } + + // Parse decrement value + decrValue, err := strconv.ParseInt(params.Command[2], 10, 64) + if err != nil { + return nil, errors.New("decrement value is not an integer or out of range") + } + + key := keys.WriteKeys[0] + values := params.GetValues(params.Context, []string{key}) // Get the current values for the specified keys + currentValue, ok := values[key] // Check if the key exists + + var newValue int64 + var currentValueInt int64 + + // Check if the key exists and its current value + if !ok || currentValue == nil { + // If key does not exist, initialize it with the decrement value + newValue = decrValue * -1 + } else { + // Use type switch to handle different types of currentValue + switch v := currentValue.(type) { + case string: + currentValueInt, err = strconv.ParseInt(v, 10, 64) // Parse the string to int64 + if err != nil { + return nil, errors.New("value is not an integer or out of range") + } + case int: + currentValueInt = int64(v) // Convert int to int64 + case int64: + currentValueInt = v // Use int64 value directly + default: + fmt.Printf("unexpected type for currentValue: %T\n", currentValue) + return nil, errors.New("unexpected type for currentValue") // Handle unexpected types + } + newValue = currentValueInt - decrValue // decrement the value by the specified amount + } + + // Set the new incremented value + if err := params.SetValues(params.Context, map[string]interface{}{key: fmt.Sprintf("%d", newValue)}); err != nil { + return nil, err + } + + // Prepare response with the actual new value + return []byte(fmt.Sprintf(":%d\r\n", newValue)), nil +} + func Commands() []internal.Command { return []internal.Command{ { @@ -649,9 +706,9 @@ LT - Only set the expiry time if the new expiry time is less than the current on Command: "incr", Module: constants.GenericModule, Categories: []string{constants.KeyspaceCategory, constants.WriteCategory, constants.FastCategory}, - Description: `(INCR key) -Increments the number stored at key by one. If the key does not exist, it is set to 0 before performing the operation. -An error is returned if the key contains a value of the wrong type or contains a string that cannot be represented as integer. + Description: `(INCR key) +Increments the number stored at key by one. If the key does not exist, it is set to 0 before performing the operation. +An error is returned if the key contains a value of the wrong type or contains a string that cannot be represented as integer. This operation is limited to 64 bit signed integers.`, Sync: true, KeyExtractionFunc: incrKeyFunc, @@ -661,14 +718,23 @@ This operation is limited to 64 bit signed integers.`, Command: "decr", Module: constants.GenericModule, Categories: []string{constants.KeyspaceCategory, constants.WriteCategory, constants.FastCategory}, - Description: `(DECR key) -Decrements the number stored at key by one. -If the key does not exist, it is set to 0 before performing the operation. -An error is returned if the key contains a value of the wrong type or contains a string that cannot be represented as integer. + Description: `(DECR key) +Decrements the number stored at key by one. +If the key does not exist, it is set to 0 before performing the operation. +An error is returned if the key contains a value of the wrong type or contains a string that cannot be represented as integer. This operation is limited to 64 bit signed integers.`, Sync: true, KeyExtractionFunc: decrKeyFunc, HandlerFunc: handleDecr, }, + { + Command: "decrby", + Module: constants.GenericModule, + Categories: []string{constants.KeyspaceCategory, constants.WriteCategory, constants.FastCategory}, + Description: `(DECRBY key decrement) The DECRBY command reduces the value stored at the specified key by the specified decrement. If the key does not exist, it is initialized with a value of 0 before performing the operation. If the key's value is not of the correct type or cannot be represented as an integer, an error is returned.`, + Sync: true, + KeyExtractionFunc: decrKeyByFunc, + HandlerFunc: handleDecrBy, + }, } } diff --git a/internal/modules/generic/commands_test.go b/internal/modules/generic/commands_test.go index 02212d8..74f9c5a 100644 --- a/internal/modules/generic/commands_test.go +++ b/internal/modules/generic/commands_test.go @@ -2137,4 +2137,110 @@ func Test_Generic(t *testing.T) { }) } }) + + t.Run("Test_HandlerDECRBY", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + decrement string + presetValue interface{} + command []resp.Value + expectedResponse int64 + expectedError error + }{ + { + name: "1. Decrement non-existent key by 4", + key: "DecrByKey1", + decrement: "4", + presetValue: nil, + command: []resp.Value{resp.StringValue("DECRBY"), resp.StringValue("DecrByKey1"), resp.StringValue("4")}, + expectedResponse: -4, + expectedError: nil, + }, + { + name: "2. Decrement existing key with integer value by 3", + key: "DecrByKey2", + decrement: "3", + presetValue: "5", + command: []resp.Value{resp.StringValue("DECRBY"), resp.StringValue("DecrByKey2"), resp.StringValue("3")}, + expectedResponse: 2, + expectedError: nil, + }, + { + name: "3. Decrement existing key with non-integer value by 2", + key: "DecrByKey3", + decrement: "2", + presetValue: "not_an_int", + command: []resp.Value{resp.StringValue("DECRBY"), resp.StringValue("DecrByKey3"), resp.StringValue("2")}, + expectedResponse: 0, + expectedError: errors.New("value is not an integer or out of range"), + }, + { + name: "4. Decrement existing key with int64 value by 7", + key: "DecrByKey4", + decrement: "7", + presetValue: int64(10), + command: []resp.Value{resp.StringValue("DECRBY"), resp.StringValue("DecrByKey4"), resp.StringValue("7")}, + expectedResponse: 3, + expectedError: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + command := []resp.Value{resp.StringValue("SET"), resp.StringValue(test.key), resp.StringValue(fmt.Sprintf("%v", test.presetValue))} + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + if !strings.EqualFold(res.String(), "OK") { + t.Errorf("expected preset response to be OK, got %s", res.String()) + } + } + + if err = client.WriteArray(test.command); err != nil { + t.Error(err) + } + + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } + + if err != nil { + t.Error(err) + } else { + responseInt, err := strconv.ParseInt(res.String(), 10, 64) + if err != nil { + t.Errorf("error parsing response to int64: %s", err) + } + if responseInt != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, responseInt) + } + } + }) + } + }) } diff --git a/internal/modules/generic/key_funcs.go b/internal/modules/generic/key_funcs.go index 2f95fa5..82f65b6 100644 --- a/internal/modules/generic/key_funcs.go +++ b/internal/modules/generic/key_funcs.go @@ -154,3 +154,12 @@ func decrKeyFunc(cmd []string) (internal.KeyExtractionFuncResult, error) { WriteKeys: cmd[1:2], }, nil } + +func decrKeyByFunc(cmd []string) (internal.KeyExtractionFuncResult, error) { + if len(cmd) < 3 { + return internal.KeyExtractionFuncResult{}, errors.New("wrong number of arguments for DECRBY") + } + return internal.KeyExtractionFuncResult{ + WriteKeys: []string{cmd[1]}, + }, nil +}