Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added DECRBY command support #79

Merged
merged 13 commits into from
Jun 25, 2024
8,245 changes: 1,271 additions & 6,974 deletions coverage/coverage.out

Large diffs are not rendered by default.

23 changes: 22 additions & 1 deletion echovault/api_generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -476,13 +476,34 @@ func (server *EchoVault) Decr(key string) (int, error) {
func (server *EchoVault) IncrBy(key string, value string) (int, error) {
// Construct the command
cmd := []string{"INCRBY", 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)
}

// 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: The new value of the key after the decrement operation as an integer.
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)
}
76 changes: 72 additions & 4 deletions echovault/api_generic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -974,11 +974,11 @@ func TestEchoVault_INCR(t *testing.T) {
}
got, err := server.Incr(tt.key)
if (err != nil) != tt.wantErr {
t.Errorf("TTL() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("INCR() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("TTL() got = %v, want %v", got, tt.want)
t.Errorf("INCR() got = %v, want %v", got, tt.want)
}
})
}
Expand Down Expand Up @@ -1038,11 +1038,11 @@ func TestEchoVault_DECR(t *testing.T) {
}
got, err := server.Decr(tt.key)
if (err != nil) != tt.wantErr {
t.Errorf("TTL() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("DECR() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("TTL() got = %v, want %v", got, tt.want)
t.Errorf("DECR() got = %v, want %v", got, tt.want)
}
})
}
Expand Down Expand Up @@ -1116,3 +1116,71 @@ func TestEchoVault_INCRBY(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("DecrBy() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("DecrBy() got = %v, want %v", got, tt.want)
}
})
}
}
64 changes: 64 additions & 0 deletions internal/modules/generic/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,58 @@ func handleIncrBy(params internal.HandlerFuncParams) ([]byte, error) {
return []byte(fmt.Sprintf(":%d\r\n", newValue)), nil
}

func handleDecrBy(params internal.HandlerFuncParams) ([]byte, error) {
// Extract key from command
keys, err := decrByKeyFunc(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{
{
Expand Down Expand Up @@ -733,5 +785,17 @@ An error is returned if the key contains a value of the wrong type or contains a
KeyExtractionFunc: incrByKeyFunc,
HandlerFunc: handleIncrBy,
},
{
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: decrByKeyFunc,
HandlerFunc: handleDecrBy,
},
}
}
127 changes: 127 additions & 0 deletions internal/modules/generic/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2266,4 +2266,131 @@ 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,
},
{
name: "5. Command too short",
key: "DecrByKey5",
presetValue: nil,
command: []resp.Value{resp.StringValue("DECRBY"), resp.StringValue("DecrByKey5")},
expectedResponse: 0,
expectedError: errors.New(constants.WrongArgsResponse),
},
{
name: "6. Command too long",
key: "DecrKey6",
presetValue: nil,
command: []resp.Value{
resp.StringValue("DECRBY"),
resp.StringValue("DecrKey6"),
resp.StringValue("3"),
resp.StringValue("extra_arg"),
},
expectedResponse: 0,
expectedError: errors.New(constants.WrongArgsResponse),
},
}

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)
}
}
})
}
})
}
9 changes: 9 additions & 0 deletions internal/modules/generic/key_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,12 @@ func incrByKeyFunc(cmd []string) (internal.KeyExtractionFuncResult, error) {
WriteKeys: []string{cmd[1]},
}, nil
}

func decrByKeyFunc(cmd []string) (internal.KeyExtractionFuncResult, error) {
if len(cmd) != 3 {
return internal.KeyExtractionFuncResult{}, errors.New(constants.WrongArgsResponse)
}
return internal.KeyExtractionFuncResult{
WriteKeys: []string{cmd[1]},
}, nil
}
Loading