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 INCRBYFLOAT command #85

Merged
merged 14 commits into from
Jul 4, 2024
23 changes: 23 additions & 0 deletions echovault/api_generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,29 @@ func (server *EchoVault) IncrBy(key string, value string) (int, error) {
return internal.ParseIntegerResponse(b)
}

// IncrByFloat increments the floating-point 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 incrementing.
// If the value stored at the key is not a float, an error is returned.
//
// Parameters:
//
// `key` - string - The key whose value is to be incremented.
//
// `increment` - float64 - The amount by which to increment the key's value. This can be a positive or negative float.
//
// Returns: The new value of the key after the increment operation as a float64.
func (server *EchoVault) IncrByFloat(key string, value string) (float64, error) {
// Construct the command
cmd := []string{"INCRBYFLOAT", 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 float response
return internal.ParseFloatResponse(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.
Expand Down
69 changes: 69 additions & 0 deletions echovault/api_generic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1117,6 +1117,75 @@ func TestEchoVault_INCRBY(t *testing.T) {
}
}

func TestEchoVault_INCRBYFLOAT(t *testing.T) {
server := createEchoVault()

tests := []struct {
name string
key string
increment string
presetValues map[string]internal.KeyData
want float64
wantErr bool
}{
{
name: "1. Increment non-existent key by 2.5",
key: "IncrByFloatKey1",
increment: "2.5",
presetValues: nil,
want: 2.5,
wantErr: false,
},
{
name: "2. Increment existing key with integer value by 1.2",
key: "IncrByFloatKey2",
increment: "1.2",
presetValues: map[string]internal.KeyData{
"IncrByFloatKey2": {Value: "5"},
},
want: 6.2,
wantErr: false,
},
{
name: "3. Increment existing key with float value by 0.7",
key: "IncrByFloatKey4",
increment: "0.7",
presetValues: map[string]internal.KeyData{
"IncrByFloatKey4": {Value: "10.0"},
},
want: 10.7,
wantErr: false,
},
{
name: "4. Increment existing key with scientific notation value by 200",
key: "IncrByFloatKey5",
increment: "200",
presetValues: map[string]internal.KeyData{
"IncrByFloatKey5": {Value: "5.0e3"},
},
want: 5200,
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.IncrByFloat(tt.key, tt.increment)
if (err != nil) != tt.wantErr {
t.Errorf("IncrByFloat() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil && got != tt.want {
t.Errorf("IncrByFloat() got = %v, want %v", got, tt.want)
}
})
}
}

func TestEchoVault_DECRBY(t *testing.T) {
server := createEchoVault()

Expand Down
19 changes: 18 additions & 1 deletion echovault/api_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
package echovault

import (
"github.com/echovault/echovault/internal"
"strconv"

"github.com/echovault/echovault/internal"
)

// SetRange replaces a portion of the string at the provided key starting at the offset with a new string.
Expand Down Expand Up @@ -76,3 +77,19 @@ func (server *EchoVault) GetRange(key string, start, end int) (string, error) {
}
return internal.ParseStringResponse(b)
}

// Append concatenates the string at the key with the value provided
// If the key does not exists it functions like a SET command
//
// Returns: The lenght of the new concatenated value at key
//
// Errors:
//
// - "value at key <key> is not a string" - when the value at the keys is not a string.
func (server *EchoVault) Append(key string, value string) (int, error) {
b, err := server.handleCommand(server.context, internal.EncodeCommand([]string{"APPEND", key, value}), nil, false, true)
if err != nil {
return 0, err
}
return internal.ParseIntegerResponse(b)
}
54 changes: 54 additions & 0 deletions echovault/api_string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,57 @@ func TestEchoVault_STRLEN(t *testing.T) {
})
}
}

func TestEchoVault_APPEND(t *testing.T) {
server := createEchoVault()
tests := []struct {
name string
presetValue interface{}
key string
value string
want int
wantErr bool
}{
{
name: "Test APPEND with no preset value",
key: "key1",
value: "Hello ",
want: 6,
wantErr: false,
},
{
name: "Test APPEND with preset value",
presetValue: "Hello ",
key: "key2",
value: "World",
want: 11,
wantErr: false,
},
{
name: "Test APPEND with integer preset value",
key: "key3",
presetValue: 10,
value: "Hello ",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.presetValue != nil {
err := presetValue(server, context.Background(), tt.key, tt.presetValue)
if err != nil {
t.Error(err)
return
}
}
got, err := server.Append(tt.key, tt.value)
if (err != nil) != tt.wantErr {
t.Errorf("APPEND() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("APPEND() got = %v, want %v", got, tt.want)
}
})
}
}
73 changes: 71 additions & 2 deletions internal/modules/generic/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,64 @@ func handleIncrBy(params internal.HandlerFuncParams) ([]byte, error) {
return []byte(fmt.Sprintf(":%d\r\n", newValue)), nil
}

func handleIncrByFloat(params internal.HandlerFuncParams) ([]byte, error) {
// Extract key from command
keys, err := incrByFloatKeyFunc(params.Command)
if err != nil {
return nil, err
}

// Parse increment value
incrValue, err := strconv.ParseFloat(params.Command[2], 64)
if err != nil {
return nil, errors.New("increment value is not a float 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 float64
var currentValueFloat float64

// Check if the key exists and its current value
if !ok || currentValue == nil {
// If key does not exist, initialize it with the increment value
newValue = incrValue
} else {
// Use type switch to handle different types of currentValue
switch v := currentValue.(type) {
case string:
currentValueFloat, err = strconv.ParseFloat(v, 64) // Parse the string to float64
if err != nil {
currentValueInt, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return nil, errors.New("value is not a float or integer")
}
currentValueFloat = float64(currentValueInt)
}
case float64:
currentValueFloat = v // Use float64 value directly
case int64:
currentValueFloat = float64(v) // Convert int64 to float64
case int:
currentValueFloat = float64(v) // Convert int to float64
default:
fmt.Printf("unexpected type for currentValue: %T\n", currentValue)
return nil, errors.New("unexpected type for currentValue") // Handle unexpected types
}
newValue = currentValueFloat + incrValue // Increment the value by the specified amount
}

// Set the new incremented value
if err := params.SetValues(params.Context, map[string]interface{}{key: fmt.Sprintf("%g", newValue)}); err != nil {
return nil, err
}

// Prepare response with the actual new value in bulk string format
response := fmt.Sprintf("$%d\r\n%g\r\n", len(fmt.Sprintf("%g", newValue)), newValue)
return []byte(response), nil
}
func handleDecrBy(params internal.HandlerFuncParams) ([]byte, error) {
// Extract key from command
keys, err := decrByKeyFunc(params.Command)
Expand Down Expand Up @@ -829,6 +887,17 @@ An error is returned if the key contains a value of the wrong type or contains a
KeyExtractionFunc: incrByKeyFunc,
HandlerFunc: handleIncrBy,
},
{
Command: "incrbyfloat",
Module: constants.GenericModule,
Categories: []string{constants.WriteCategory, constants.FastCategory},
Description: `(INCRBYFLOAT key increment)
Increments the number stored at key by increment. 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 float.`,
Sync: true,
KeyExtractionFunc: incrByFloatKeyFunc,
HandlerFunc: handleIncrByFloat,
},
{
Command: "decrby",
Module: constants.GenericModule,
Expand All @@ -845,7 +914,7 @@ If the key's value is not of the correct type or cannot be represented as an int
Command: "rename",
Module: constants.GenericModule,
Categories: []string{constants.KeyspaceCategory, constants.WriteCategory, constants.FastCategory},
Description: `(RENAME key newkey)
Description: `(RENAME key newkey)
Renames key to newkey. If newkey already exists, it is overwritten. If key does not exist, an error is returned.`,
Sync: true,
KeyExtractionFunc: renameKeyFunc,
Expand Down Expand Up @@ -878,7 +947,7 @@ Renames key to newkey. If newkey already exists, it is overwritten. If key does
constants.SlowCategory,
constants.DangerousCategory,
},
Description: `(FLUSHDB)
Description: `(FLUSHDB)
Delete all the keys in the currently selected database. This command is always synchronous.`,
Sync: true,
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
Expand Down
Loading
Loading