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

Enhancement/62 append command #84

Merged
merged 11 commits into from
Jul 2, 2024
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)
}
})
}
}
40 changes: 40 additions & 0 deletions internal/modules/string/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package str
import (
"errors"
"fmt"

"github.com/echovault/echovault/internal"
"github.com/echovault/echovault/internal/constants"
)
Expand Down Expand Up @@ -166,6 +167,36 @@ func handleSubStr(params internal.HandlerFuncParams) ([]byte, error) {
return []byte(fmt.Sprintf("$%d\r\n%s\r\n", len(str), str)), nil
}

func handleAppend(params internal.HandlerFuncParams) ([]byte, error) {
keys, err := appendKeyFunc(params.Command)
if err != nil {
return nil, err
}

key := keys.WriteKeys[0]
keyExists := params.KeysExist(params.Context, keys.WriteKeys)[key]
value := params.Command[2]
if !keyExists {
if err = params.SetValues(params.Context, map[string]interface{}{
key: internal.AdaptType(value),
}); err != nil {
return nil, err
}
return []byte(fmt.Sprintf(":%d\r\n", len(value))), nil
}
currentValue, ok := params.GetValues(params.Context, []string{key})[key].(string)
if !ok {
return nil, fmt.Errorf("Value at key %s is not a string", key)
}
newValue := fmt.Sprintf("%v%s", currentValue, value)
if err = params.SetValues(params.Context, map[string]interface{}{
key: internal.AdaptType(newValue),
}); err != nil {
return nil, err
}
return []byte(fmt.Sprintf(":%d\r\n", len(newValue))), nil
}

func Commands() []internal.Command {
return []internal.Command{
{
Expand Down Expand Up @@ -205,5 +236,14 @@ Overwrites part of a string value with another by offset. Creates the key if it
KeyExtractionFunc: subStrKeyFunc,
HandlerFunc: handleSubStr,
},
{
Command: "append",
Module: constants.StringModule,
Categories: []string{constants.StringCategory, constants.WriteCategory, constants.SlowCategory},
Description: `(APPEND key value) If key already exists and is a string, this command appends the value at the end of the string. If key does not exist it is created and set as an empty string, so APPEND will be similar to [SET] in this special case.`,
Sync: true,
KeyExtractionFunc: appendKeyFunc,
HandlerFunc: handleAppend,
},
}
}
109 changes: 106 additions & 3 deletions internal/modules/string/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ package str_test

import (
"errors"
"strconv"
"strings"
"testing"

"github.com/echovault/echovault/echovault"
"github.com/echovault/echovault/internal"
"github.com/echovault/echovault/internal/config"
"github.com/echovault/echovault/internal/constants"
"github.com/tidwall/resp"
"strconv"
"strings"
"testing"
)

func Test_String(t *testing.T) {
Expand Down Expand Up @@ -450,4 +451,106 @@ func Test_String(t *testing.T) {
})
}
})

t.Run("Test_HandleAppend", 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
presetValue interface{}
command []string
expectedResponse int
expectedError error
}{
{
name: "Test APPEND with no preset value",
key: "AppendKey1",
command: []string{"APPEND", "AppendKey1", "Hello"},
expectedResponse: 5,
expectedError: nil,
},
{
name: "Test APPEND with preset value",
key: "AppendKey2",
presetValue: "Hello ",
command: []string{"APPEND", "AppendKey2", "World"},
expectedResponse: 11,
expectedError: nil,
},
{
name: "Test APPEND with integer preset value",
key: "AppendKey4",
presetValue: 10,
command: []string{"APPEND", "AppendKey4", "World"},
expectedResponse: 0,
expectedError: errors.New("Value at key AppendKey4 is not a string"),
},
{
name: "Command too short",
command: []string{"APPEND", "AppendKey5"},
expectedError: errors.New(constants.WrongArgsResponse),
},
{
name: "Command too long",
command: []string{"APPEND", "AppendKey5", "new value", "extra value"},
expectedError: errors.New(constants.WrongArgsResponse),
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.presetValue != "" {
if err = client.WriteArray([]resp.Value{
resp.StringValue("SET"),
resp.StringValue(test.key),
resp.AnyValue(test.presetValue),
}); 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())
}
}

command := make([]resp.Value, len(test.command))
for i, c := range test.command {
command[i] = resp.StringValue(c)
}

if err = client.WriteArray(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 res.Integer() != test.expectedResponse {
t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer())
}
})
}
})
}
12 changes: 12 additions & 0 deletions internal/modules/string/key_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package str

import (
"errors"

"github.com/echovault/echovault/internal"
"github.com/echovault/echovault/internal/constants"
)
Expand Down Expand Up @@ -52,3 +53,14 @@ func subStrKeyFunc(cmd []string) (internal.KeyExtractionFuncResult, error) {
WriteKeys: make([]string, 0),
}, nil
}

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