diff --git a/docs/src/content/docs/commands/GETRANGE.MD b/docs/src/content/docs/commands/GETRANGE.MD index 390cfc368..c58ff9e56 100644 --- a/docs/src/content/docs/commands/GETRANGE.MD +++ b/docs/src/content/docs/commands/GETRANGE.MD @@ -89,6 +89,25 @@ OK "apple" ``` +`GETRANGE` returns string representation of byte array stored in bitmap + +```bash +127.0.0.1:7379> SETBIT bitmapkey 2 1 +(integer) 0 +127.0.0.1:7379> SETBIT bitmapkey 3 1 +(integer) 0 +127.0.0.1:7379> SETBIT bitmapkey 5 1 +(integer) 0 +127.0.0.1:7379> SETBIT bitmapkey 10 1 +(integer) 0 +127.0.0.1:7379> SETBIT bitmapkey 11 1 +(integer) 0 +127.0.0.1:7379> SETBIT bitmapkey 14 1 +(integer) 0 +127.0.0.1:7379> GETRANGE bitmapkey 0 -1 +"42" +``` + ### Invalid usage Trying to use `GETRANGE` without giving the value diff --git a/integration_tests/commands/http/getrange_test.go b/integration_tests/commands/http/getrange_test.go index 5de0e3444..e4c7c941e 100644 --- a/integration_tests/commands/http/getrange_test.go +++ b/integration_tests/commands/http/getrange_test.go @@ -1,14 +1,39 @@ package http import ( + "fmt" "testing" "github.com/stretchr/testify/assert" ) +func generateByteArrayForGetrangeTestCase() ([]HTTPCommand, []interface{}) { + var cmds []HTTPCommand + var exp []interface{} + + str := "helloworld" + var binaryStr string + + for _, c := range str { + binaryStr += fmt.Sprintf("%08b", c) + } + + for idx, bit := range binaryStr { + if bit == '1' { + cmds = append(cmds, HTTPCommand{Command: "SETBIT", Body: map[string]interface{}{"key": "byteArrayKey", "values": []interface{}{idx, 1}}}) + exp = append(exp, float64(0)) + } + } + + cmds = append(cmds, HTTPCommand{Command: "GETRANGE", Body: map[string]interface{}{"key": "byteArrayKey", "values": []interface{}{0, 4}}}) + exp = append(exp, "hello") + + return cmds, exp +} + func TestGETRANGE(t *testing.T) { exec := NewHTTPCommandExecutor() - + byteArrayCmds, byteArrayExp := generateByteArrayForGetrangeTestCase() testCases := []struct { name string commands []HTTPCommand @@ -69,6 +94,14 @@ func TestGETRANGE(t *testing.T) { {Command: "del", Body: map[string]interface{}{"key": "test5"}}, }, }, + { + name: "GETRANGE against byte array", + commands: byteArrayCmds, + expected: byteArrayExp, + cleanup: []HTTPCommand{ + {Command: "del", Body: map[string]interface{}{"key": "byteArrayKey"}}, + }, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/integration_tests/commands/resp/getrange_test.go b/integration_tests/commands/resp/getrange_test.go index 14f068b3e..343654f6d 100644 --- a/integration_tests/commands/resp/getrange_test.go +++ b/integration_tests/commands/resp/getrange_test.go @@ -1,11 +1,37 @@ package resp import ( + "fmt" + "strconv" "testing" "github.com/stretchr/testify/assert" ) +func generateByteArrayForGetrangeTestCase() ([]string, []interface{}) { + var cmds []string + var exp []interface{} + + str := "helloworld" + var binaryStr string + + for _, c := range str { + binaryStr += fmt.Sprintf("%08b", c) + } + + for idx, bit := range binaryStr { + if bit == '1' { + cmds = append(cmds, string("SETBIT byteArrayKey "+strconv.Itoa(idx)+" 1")) + exp = append(exp, int64(0)) + } + } + + cmds = append(cmds, "GETRANGE byteArrayKey 0 4") + exp = append(exp, "hello") + + return cmds, exp +} + func TestGETRANGE(t *testing.T) { conn := getLocalConnection() defer conn.Close() @@ -13,6 +39,8 @@ func TestGETRANGE(t *testing.T) { FireCommand(conn, "FLUSHDB") defer FireCommand(conn, "FLUSHDB") + byteArrayCmds, byteArrayExp := generateByteArrayForGetrangeTestCase() + testCases := []struct { name string commands []string @@ -55,6 +83,12 @@ func TestGETRANGE(t *testing.T) { expected: []interface{}{"OK", ""}, cleanup: []string{"del test6"}, }, + { + name: "GETRANGE against byte array", + commands: byteArrayCmds, + expected: byteArrayExp, + cleanup: []string{"del byteArrayKey"}, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/integration_tests/commands/websocket/getrange_test.go b/integration_tests/commands/websocket/getrange_test.go index 756189617..136d0d0cc 100644 --- a/integration_tests/commands/websocket/getrange_test.go +++ b/integration_tests/commands/websocket/getrange_test.go @@ -1,14 +1,40 @@ package websocket import ( + "fmt" + "strconv" "testing" "github.com/stretchr/testify/assert" ) +func generateByteArrayForGetrangeTestCase() ([]string, []interface{}) { + var cmds []string + var exp []interface{} + + str := "helloworld" + var binaryStr string + + for _, c := range str { + binaryStr += fmt.Sprintf("%08b", c) + } + + for idx, bit := range binaryStr { + if bit == '1' { + cmds = append(cmds, string("SETBIT byteArrayKey "+strconv.Itoa(idx)+" 1")) + exp = append(exp, float64(0)) + } + } + + cmds = append(cmds, "GETRANGE byteArrayKey 0 4") + exp = append(exp, "hello") + + return cmds, exp +} + func TestGETRANGE(t *testing.T) { exec := NewWebsocketCommandExecutor() - + byteArrayCmds, byteArrayExp := generateByteArrayForGetrangeTestCase() testCases := []struct { name string commands []string @@ -51,6 +77,12 @@ func TestGETRANGE(t *testing.T) { expected: []interface{}{"OK", ""}, cleanupKey: "test6", }, + { + name: "GETRANGE against byte array", + commands: byteArrayCmds, + expected: byteArrayExp, + cleanupKey: "byteArrayKey", + }, } 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 7ff45c31c..bff4c7d52 100644 --- a/internal/eval/eval_test.go +++ b/internal/eval/eval_test.go @@ -5480,6 +5480,30 @@ func testEvalGETRANGE(t *testing.T, store *dstore.Store) { Error: nil, }, }, + "GETRANGE against byte array with valid range: 0 4": { + setup: func() { + key := "BYTEARRAY_KEY" + store.Put(key, store.NewObj(&ByteArray{data: []byte{0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray)) + }, + input: []string{"BYTEARRAY_KEY", "0", "4"}, + migratedOutput: EvalResponse{Result: "hello", Error: nil}, + }, + "GETRANGE against byte array with valid range: 6 -1": { + setup: func() { + key := "BYTEARRAY_KEY" + store.Put(key, store.NewObj(&ByteArray{data: []byte{0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray)) + }, + input: []string{"BYTEARRAY_KEY", "6", "-1"}, + migratedOutput: EvalResponse{Result: "world", Error: nil}, + }, + "GETRANGE against byte array with invalid range: 20 30": { + setup: func() { + key := "BYTEARRAY_KEY" + store.Put(key, store.NewObj(&ByteArray{data: []byte{0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray)) + }, + input: []string{"BYTEARRAY_KEY", "20", "30"}, + migratedOutput: EvalResponse{Result: "", Error: nil}, + }, } runMigratedEvalTests(t, tests, evalGETRANGE, store) diff --git a/internal/eval/store_eval.go b/internal/eval/store_eval.go index 746c5c84a..f5d294c7c 100644 --- a/internal/eval/store_eval.go +++ b/internal/eval/store_eval.go @@ -647,6 +647,15 @@ func evalGETRANGE(args []string, store *dstore.Store) *EvalResponse { } case object.ObjEncodingInt: str = strconv.FormatInt(obj.Value.(int64), 10) + case object.ObjEncodingByteArray: + if val, ok := obj.Value.(*ByteArray); ok { + str = string(val.data) + } else { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } default: return &EvalResponse{ Result: nil,