Skip to content

Commit

Permalink
#638: Adds APPEND Command (#759)
Browse files Browse the repository at this point in the history
  • Loading branch information
arbha1erao authored Oct 2, 2024
1 parent b77ab8c commit 5ae8e58
Show file tree
Hide file tree
Showing 4 changed files with 300 additions and 0 deletions.
75 changes: 75 additions & 0 deletions integration_tests/commands/async/append_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package async

import (
"testing"

"gotest.tools/v3/assert"
)

func TestAPPEND(t *testing.T) {
conn := getLocalConnection()
defer conn.Close()

FireCommand(conn, "DEL key listKey bitKey hashKey setKey")
defer FireCommand(conn, "DEL key listKey bitKey hashKey setKey")

setErrorMsg := "WRONGTYPE Operation against a key holding the wrong kind of value"
testCases := []struct {
name string
commands []string
expected []interface{}
}{
{
name: "APPEND After SET and DEL",
commands: []string{
"SET key value",
"APPEND key value",
"GET key",
"APPEND key 100",
"GET key",
"DEL key",
"APPEND key value",
"GET key",
},
expected: []interface{}{"OK", int64(10), "valuevalue", int64(13), "valuevalue100", int64(1), int64(5), "value"},
},
{
name: "APPEND to Integer Values",
commands: []string{
"DEL key",
"APPEND key 1",
"APPEND key 2",
"GET key",
"SET key 1",
"APPEND key 2",
"GET key",
},
expected: []interface{}{int64(0), int64(1), int64(2), "12", "OK", int64(2), "12"},
},
{
name: "APPEND with Various Data Types",
commands: []string{
"LPUSH listKey lValue", // Add element to a list
"SETBIT bitKey 0 1", // Set a bit in a bitmap
"HSET hashKey hKey hValue", // Set a field in a hash
"SADD setKey sValue", // Add element to a set
"APPEND listKey value", // Attempt to append to a list
"APPEND bitKey value", // Attempt to append to a bitmap
"APPEND hashKey value", // Attempt to append to a hash
"APPEND setKey value", // Attempt to append to a set
},
expected: []interface{}{"OK", int64(0), int64(1), int64(1), setErrorMsg, setErrorMsg, setErrorMsg, setErrorMsg},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
FireCommand(conn, "DEL key")

for i, cmd := range tc.commands {
result := FireCommand(conn, cmd)
assert.DeepEqual(t, tc.expected[i], result)
}
})
}
}
7 changes: 7 additions & 0 deletions internal/eval/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,12 @@ var (
Arity: -2,
KeySpecs: KeySpecs{BeginIndex: 1},
}
appendCmdMeta = DiceCmdMeta{
Name: "APPEND",
Info: `Appends a string to the value of a key. Creates the key if it doesn't exist.`,
Eval: evalAPPEND,
Arity: 3,
}
zaddCmdMeta = DiceCmdMeta{
Name: "ZADD",
Info: `ZADD key [NX|XX] [CH] [INCR] score member [score member ...]
Expand Down Expand Up @@ -1119,6 +1125,7 @@ func init() {
DiceCmds["HRANDFIELD"] = hrandfieldCmdMeta
DiceCmds["HDEL"] = hdelCmdMeta
DiceCmds["HVALS"] = hValsCmdMeta
DiceCmds["APPEND"] = appendCmdMeta
DiceCmds["ZADD"] = zaddCmdMeta
DiceCmds["ZRANGE"] = zrangeCmdMeta
DiceCmds["HINCRBYFLOAT"] = hincrbyFloatCmdMeta
Expand Down
53 changes: 53 additions & 0 deletions internal/eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -4406,6 +4406,59 @@ func selectRandomFields(hashMap HashMap, count int, withValues bool) []byte {
return clientio.Encode(results, false)
}

// evalAPPEND takes two arguments: the key and the value to append to the key's current value.
// If the key does not exist, it creates a new key with the given value (so APPEND will be similar to SET in this special case)
// If key already exists and is a string (or integers stored as strings), this command appends the value at the end of the string
func evalAPPEND(args []string, store *dstore.Store) []byte {
if len(args) != 2 {
return diceerrors.NewErrArity("APPEND")
}

key, value := args[0], args[1]
obj := store.Get(key)

if obj == nil {
// Key does not exist path
oType, oEnc := deduceTypeEncoding(value)

var storedValue interface{}
// Store the value with the appropriate encoding based on the type
switch oEnc {
case object.ObjEncodingInt:
storedValue, _ = strconv.ParseInt(value, 10, 64)
case object.ObjEncodingEmbStr, object.ObjEncodingRaw:
storedValue = value
default:
return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr)
}

store.Put(key, store.NewObj(storedValue, -1, oType, oEnc))

return clientio.Encode(len(value), false)
}
// Key exists path
_, currentEnc := object.ExtractTypeEncoding(obj)

var currentValueStr string
switch currentEnc {
case object.ObjEncodingInt:
// If the encoding is an integer, convert the current value to a string for concatenation
currentValueStr = strconv.FormatInt(obj.Value.(int64), 10)
case object.ObjEncodingEmbStr, object.ObjEncodingRaw:
// If the encoding is a string, retrieve the string value for concatenation
currentValueStr = obj.Value.(string)
default:
// If the encoding is neither integer nor string, return a "wrong type" error
return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr)
}

newValue := currentValueStr + value

store.Put(key, store.NewObj(newValue, -1, object.ObjTypeString, object.ObjEncodingRaw))

return clientio.Encode(len(newValue), false)
}

func evalJSONRESP(args []string, store *dstore.Store) []byte {
if len(args) < 1 {
return diceerrors.NewErrArity("json.resp")
Expand Down
165 changes: 165 additions & 0 deletions internal/eval/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ func TestEval(t *testing.T) {
testEvalFLUSHDB(t, store)
testEvalINCRBYFLOAT(t, store)
testEvalBITOP(t, store)
testEvalAPPEND(t, store)
testEvalHRANDFIELD(t, store)
testEvalZADD(t, store)
testEvalZRANGE(t, store)
Expand Down Expand Up @@ -4535,6 +4536,170 @@ func testEvalHRANDFIELD(t *testing.T, store *dstore.Store) {
runEvalTests(t, tests, evalHRANDFIELD, store)
}

func testEvalAPPEND(t *testing.T, store *dstore.Store) {
tests := map[string]evalTestCase{
"nil value": {
setup: func() {},
input: nil,
output: []byte("-ERR wrong number of arguments for 'append' command\r\n"),
},
"append invalid number of arguments": {
setup: func() {
store.Del("key")
},
input: []string{"key", "val", "val2"},
output: []byte("-ERR wrong number of arguments for 'append' command\r\n"),
},
"append to non-existing key": {
setup: func() {
store.Del("key")
},
input: []string{"key", "val"},
output: clientio.Encode(3, false),
},
"append string value to existing key having string value": {
setup: func() {
key := "key"
value := "val"
obj := store.NewObj(value, -1, object.ObjTypeString, object.ObjEncodingRaw)
store.Put(key, obj)
},
input: []string{"key", "val"},
output: clientio.Encode(6, false),
},
"append integer value to non existing key": {
setup: func() {
store.Del("key")
},
input: []string{"key", "123"},
output: clientio.Encode(3, false),
validator: func(output []byte) {
obj := store.Get("key")
_, enc := object.ExtractTypeEncoding(obj)
if enc != object.ObjEncodingInt {
t.Errorf("unexpected encoding")
}
},
},
"append string value to existing key having integer value": {
setup: func() {
key := "key"
value := "123"
storedValue, _ := strconv.ParseInt(value, 10, 64)
obj := store.NewObj(storedValue, -1, object.ObjTypeInt, object.ObjEncodingInt)
store.Put(key, obj)
},
input: []string{"key", "val"},
output: clientio.Encode(6, false),
},
"append empty string to non-existing key": {
setup: func() {
store.Del("key")
},
input: []string{"key", ""},
output: clientio.Encode(0, false),
},
"append empty string to existing key having empty string": {
setup: func() {
key := "key"
value := ""
obj := store.NewObj(value, -1, object.ObjTypeString, object.ObjEncodingRaw)
store.Put(key, obj)
},
input: []string{"key", ""},
output: clientio.Encode(0, false),
},
"append empty string to existing key": {
setup: func() {
key := "key"
value := "val"
obj := store.NewObj(value, -1, object.ObjTypeString, object.ObjEncodingRaw)
store.Put(key, obj)
},
input: []string{"key", ""},
output: clientio.Encode(3, false),
},
"append modifies the encoding from int to raw": {
setup: func() {
store.Del("key")
storedValue, _ := strconv.ParseInt("1", 10, 64)
obj := store.NewObj(storedValue, -1, object.ObjTypeInt, object.ObjEncodingInt)
store.Put("key", obj)
},
input: []string{"key", "2"},
output: clientio.Encode(2, false),
validator: func(output []byte) {
obj := store.Get("key")
_, enc := object.ExtractTypeEncoding(obj)
if enc != object.ObjEncodingRaw {
t.Errorf("unexpected encoding")
}
},
},
"append to key created using LPUSH": {
setup: func() {
key := "listKey"
value := "val"
// Create a new list object
obj := store.NewObj(NewDeque(), -1, object.ObjTypeByteList, object.ObjEncodingDeque)
store.Put(key, obj)
obj.Value.(*Deque).LPush(value)
},
input: []string{"listKey", "val"},
output: diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr),
},
"append to key created using SADD": {
setup: func() {
key := "setKey"
// Create a new set object
initialValues := map[string]struct{}{
"existingVal": {},
"anotherVal": {},
}
obj := store.NewObj(initialValues, -1, object.ObjTypeSet, object.ObjEncodingSetStr)
store.Put(key, obj)
},
input: []string{"setKey", "val"},
output: diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr),
},
"append to key created using HSET": {
setup: func() {
key := "hashKey"
// Create a new hash map object
initialValues := HashMap{
"field1": "value1",
"field2": "value2",
}
obj := store.NewObj(initialValues, -1, object.ObjTypeHashMap, object.ObjEncodingHashMap)
store.Put(key, obj)
},
input: []string{"hashKey", "val"},
output: diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr),
},
"append to key created using SETBIT": {
setup: func() {
key := "bitKey"
// Create a new byte array object
initialByteArray := NewByteArray(1) // Initialize with 1 byte
initialByteArray.SetBit(0, true) // Set the first bit to 1
obj := store.NewObj(initialByteArray, -1, object.ObjTypeByteArray, object.ObjEncodingByteArray)
store.Put(key, obj)
},
input: []string{"bitKey", "val"},
output: diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr),
},
}

runEvalTests(t, tests, evalAPPEND, store)
}

func BenchmarkEvalAPPEND(b *testing.B) {
store := dstore.NewStore(nil)
for i := 0; i < b.N; i++ {
evalAPPEND([]string{"key", fmt.Sprintf("val_%d", i)}, store)
}
}

func testEvalJSONRESP(t *testing.T, store *dstore.Store) {
tests := map[string]evalTestCase{
"wrong number of args passed": {
Expand Down

0 comments on commit 5ae8e58

Please sign in to comment.