diff --git a/docs/src/content/docs/commands/APPEND.md b/docs/src/content/docs/commands/APPEND.md index 8ada6fd98..2f5cedcf0 100644 --- a/docs/src/content/docs/commands/APPEND.md +++ b/docs/src/content/docs/commands/APPEND.md @@ -3,7 +3,7 @@ title: APPEND description: The `APPEND` command in DiceDB is used to either set the value of a key or append a value to an existing key. This command allows for both creating and updating key-value pairs. --- -The `APPEND` command in DiceDB is used to either set the value of a key or append a value to an existing key. This command allows for both creating and updating key-value pairs. +The `APPEND` command in DiceDB is used to either set the value of a key or append a value to an existing key and returns the length of the value stored at the specified key after appending. This command allows for both creating and updating key-value pairs. ## Syntax @@ -56,8 +56,34 @@ Appending to key `foo` that contains `bar` with `baz` ```bash 127.0.0.1:7379> SET foo bar +OK 127.0.0.1:7379> APPEND foo baz (integer) 6 +127.0.0.1:7379> GET foo +"barbaz" +``` + +Appending "1" to key `bmkey` that contains a bitmap equivalent of `42` + +```bash +127.0.0.1:7379> SETBIT bmkey 2 1 +(integer) 0 +127.0.0.1:7379> SETBIT bmkey 3 1 +(integer) 0 +127.0.0.1:7379> SETBIT bmkey 5 1 +(integer) 0 +127.0.0.1:7379> SETBIT bmkey 10 1 +(integer) 0 +127.0.0.1:7379> SETBIT bmkey 11 1 +(integer) 0 +127.0.0.1:7379> SETBIT bmkey 14 1 +(integer) 0 +127.0.0.1:7379> GET bmkey +"42" +127.0.0.1:7379> APPEND bmkey 1 +(integer) 3 +127.0.0.1:7379> GET bmkey +"421" ``` ### Invalid usage diff --git a/integration_tests/commands/http/append_test.go b/integration_tests/commands/http/append_test.go index 336a37850..3e63f9c1e 100644 --- a/integration_tests/commands/http/append_test.go +++ b/integration_tests/commands/http/append_test.go @@ -101,6 +101,23 @@ func TestAPPEND(t *testing.T) { {Command: "del", Body: map[string]interface{}{"key": "myzset"}}, }, }, + { + name: "APPEND to key created using SETBIT", + commands: []HTTPCommand{ + {Command: "SETBIT", Body: map[string]interface{}{"key": "bitkey", "values": []string{"2", "1"}}}, + {Command: "SETBIT", Body: map[string]interface{}{"key": "bitkey", "values": []string{"3", "1"}}}, + {Command: "SETBIT", Body: map[string]interface{}{"key": "bitkey", "values": []string{"5", "1"}}}, + {Command: "SETBIT", Body: map[string]interface{}{"key": "bitkey", "values": []string{"10", "1"}}}, + {Command: "SETBIT", Body: map[string]interface{}{"key": "bitkey", "values": []string{"11", "1"}}}, + {Command: "SETBIT", Body: map[string]interface{}{"key": "bitkey", "values": []string{"14", "1"}}}, + {Command: "APPEND", Body: map[string]interface{}{"key": "bitkey", "value": "1"}}, + {Command: "GET", Body: map[string]interface{}{"key": "bitkey"}}, + }, + expected: []interface{}{float64(0), float64(0), float64(0), float64(0), float64(0), float64(0), float64(3), "421"}, + cleanup: []HTTPCommand{ + {Command: "del", Body: map[string]interface{}{"key": "bitkey"}}, + }, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/integration_tests/commands/resp/append_test.go b/integration_tests/commands/resp/append_test.go index 6d2481c7d..2d6abb2e4 100644 --- a/integration_tests/commands/resp/append_test.go +++ b/integration_tests/commands/resp/append_test.go @@ -65,6 +65,12 @@ func TestAPPEND(t *testing.T) { expected: []interface{}{int64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"}, cleanup: []string{"del key"}, }, + { + name: "APPEND to key created using SETBIT", + commands: []string{"SETBIT bitkey 2 1", "SETBIT bitkey 3 1", "SETBIT bitkey 5 1", "SETBIT bitkey 10 1", "SETBIT bitkey 11 1", "SETBIT bitkey 14 1", "APPEND bitkey 1", "GET bitkey"}, + expected: []interface{}{int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), int64(3), "421"}, + cleanup: []string{"del bitkey"}, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/integration_tests/commands/websocket/append_test.go b/integration_tests/commands/websocket/append_test.go index 989463d96..63ec1b5f9 100644 --- a/integration_tests/commands/websocket/append_test.go +++ b/integration_tests/commands/websocket/append_test.go @@ -51,6 +51,12 @@ func TestAppend(t *testing.T) { expected: []interface{}{float64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"}, cleanupKey: "key", }, + { + name: "APPEND to key created using SETBIT", + commands: []string{"SETBIT bitkey 2 1", "SETBIT bitkey 3 1", "SETBIT bitkey 5 1", "SETBIT bitkey 10 1", "SETBIT bitkey 11 1", "SETBIT bitkey 14 1", "APPEND bitkey 1", "GET bitkey"}, + expected: []interface{}{float64(0), float64(0), float64(0), float64(0), float64(0), float64(0), float64(3), "421"}, + cleanupKey: "bitkey", + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/internal/eval/eval_test.go b/internal/eval/eval_test.go index e301ce601..c8dc7c7b4 100644 --- a/internal/eval/eval_test.go +++ b/internal/eval/eval_test.go @@ -7031,17 +7031,22 @@ func testEvalAPPEND(t *testing.T, store *dstore.Store) { input: []string{"hashKey", "val"}, migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrWrongTypeOperation}, }, - "append to key created using SETBIT": { + "append to key containing byte array": { 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 + initialByteArray := NewByteArray(2) // Initialize with 2 byte + initialByteArray.SetBit(2, true) // Set the third bit to 1 + initialByteArray.SetBit(3, true) // Set the fourth bit to 1 + initialByteArray.SetBit(5, true) // Set the sixth bit to 1 + initialByteArray.SetBit(10, true) // Set the eleventh bit to 1 + initialByteArray.SetBit(11, true) // Set the twelfth bit to 1 + initialByteArray.SetBit(14, true) // Set the fifteenth bit to 1 obj := store.NewObj(initialByteArray, -1, object.ObjTypeByteArray, object.ObjEncodingByteArray) store.Put(key, obj) }, - input: []string{"bitKey", "val"}, - migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrWrongTypeOperation}, + input: []string{"bitKey", "1"}, + migratedOutput: EvalResponse{Result: 3, Error: nil}, }, "append value with leading zeros": { setup: func() { diff --git a/internal/eval/store_eval.go b/internal/eval/store_eval.go index b316af31a..bcbda6248 100644 --- a/internal/eval/store_eval.go +++ b/internal/eval/store_eval.go @@ -1115,25 +1115,33 @@ func evalAPPEND(args []string, store *dstore.Store) *EvalResponse { Error: nil, } } - // Key exists path - if _, ok := obj.Value.(*sortedset.Set); ok { - return &EvalResponse{ - Result: nil, - Error: diceerrors.ErrWrongTypeOperation, - } - } - _, currentEnc := object.ExtractTypeEncoding(obj) var currentValueStr string - switch currentEnc { + switch currentType, currentEnc := object.ExtractTypeEncoding(obj); 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) + // If the encoding and type is a string, retrieve the string value for concatenation + if currentType == object.ObjTypeString { + currentValueStr = obj.Value.(string) + } else { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + case object.ObjEncodingByteArray: + if val, ok := obj.Value.(*ByteArray); ok { + currentValueStr = string(val.data) + } else { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } default: - // If the encoding is neither integer nor string, return a "wrong type" error + // If the encoding is neither integer, string, nor byte array, return a "wrong type" error return &EvalResponse{ Result: nil, Error: diceerrors.ErrWrongTypeOperation, @@ -1401,7 +1409,6 @@ func jsonGETHelper(store *dstore.Store, path, key string) *EvalResponse { // If path is root, return the entire JSON if path == defaultRootPath { resultBytes, err := sonic.Marshal(jsonData) - fmt.Println(string(resultBytes)) if err != nil { return &EvalResponse{ Result: nil, @@ -5212,7 +5219,6 @@ func evalSETBIT(args []string, store *dstore.Store) *EvalResponse { // resize as per the offset byteArray = byteArray.IncreaseSize(int(requiredByteArraySize)) } - resp := byteArray.GetBit(int(offset)) byteArray.SetBit(int(offset), value)