Repository: DiceDB/dice (Go)

Issue:
Inconsistent `BITOP`: BITOP with non string source key
## Steps to reproduce

Run the commands mentioned in the test on Line 245 in the file https://github.com/AshwinKul28/dice-tests/blob/main/tcltests/unit/bitpos.tcl#L245.

## Expected output

The expected output when the above set of commands when run on Redis

```
'WRONGTYPE*'
```

## Observed output

The observed output when the above set of commands when run on DiceDB

```
'ERR value is not a valid byte array'
```

The steps to run the test cases are mentioned in the README of the [dice-tests repository](https://github.com/AshwinKul28/dice-tests).

## Expectations for resolution

This issue will be considered resolved when the following things are done:

1. Changes in the [`dice`](https://github.com/dicedb/dice) repository code to meet the expected behavior.
1. Successful run of the tcl test behavior.

You can find the tests under the `tests` directory of the [`dice`](https://github.com/dicedb/dice) repository and the steps to run are in the [README file](https://github.com/dicedb/dice). Refer to the following links to set up DiceDB and Redis 7.2.5 locally

- [Setup DiceDB locally](https://github.com/dicedb/dice)
- [Setup Redis 7.2.5 locally](https://gist.github.com/arpitbbhayani/94aedf279349303ed7394197976b6843)
- [Setup DiceDB CLI](https://github.com/dicedb/dice)



Discussion:
Hi @lucifercr07 , can you assign this task to me?
Hi @lucifercr07 
Can I take this up ? Would love to work on it.
Thanks
@Nijin-P-S @jainbhavya53 Assigned other issues to both of you. Let's have a single issue on your plate at a time. Once that's completed you can pick other opened issues. 
Hi @lucifercr07 Can I take this up ? 
@karandixit10 assigned, thanks for contributing.
Here when ran the test the error message should be of type WrongTypeErr but it is being handled wrong here at Line no. 2146 in this file https://github.com/DiceDB/dice/blob/master/internal/eval/eval.go#L2146

It can be changed to `return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr)`

Hi @lucifercr07 If this is correct I can go for it.

@lucifercr07 Can you confirm this, as it is my first issue I am getting familiar with the codebase.
@karandixit10 please raise a PR, will have a look.



Patch:
diff --git a/internal/eval/eval.go b/internal/eval/eval.go
index 9c6ed376c..a84d03373 100644
--- a/internal/eval/eval.go
+++ b/internal/eval/eval.go
@@ -2238,7 +2238,7 @@ func evalBITOP(args []string, store *dstore.Store) []byte {
 				value := strconv.FormatInt(obj.Value.(int64), 10)
 				values[i] = []byte(value)
 			default:
-				return diceerrors.NewErrWithMessage("value is not a valid byte array")
+				return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr)
 			}
 		}
 	}


Test patch:
diff --git a/internal/eval/eval_test.go b/internal/eval/eval_test.go
index d8fd17242..b7255802e 100644
--- a/internal/eval/eval_test.go
+++ b/internal/eval/eval_test.go
@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"errors"
 	"fmt"
-	"github.com/dicedb/dice/internal/server/utils"
 	"reflect"
 	"strconv"
 	"strings"
@@ -12,6 +11,7 @@ import (
 	"time"
 
 	"github.com/bytedance/sonic"
+	"github.com/dicedb/dice/internal/server/utils"
 	"github.com/ohler55/ojg/jp"
 
 	"github.com/axiomhq/hyperloglog"
@@ -88,6 +88,7 @@ func TestEval(t *testing.T) {
 	testEvalSETEX(t, store)
 	testEvalFLUSHDB(t, store)
 	testEvalINCRBYFLOAT(t, store)
+	testEvalBITOP(t, store)
 }
 
 func testEvalPING(t *testing.T, store *dstore.Store) {
@@ -3487,3 +3488,124 @@ func BenchmarkEvalINCRBYFLOAT(b *testing.B) {
 		})
 	}
 }
+
+func testEvalBITOP(t *testing.T, store *dstore.Store) {
+	tests := map[string]evalTestCase{
+		"BITOP NOT (empty string)": {
+			setup: func() {
+				store.Put("s{t}", store.NewObj(&ByteArray{data: []byte("")}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray))
+			},
+			input:  []string{"NOT", "dest{t}", "s{t}"},
+			output: clientio.Encode(0, true),
+			validator: func(output []byte) {
+				expectedResult := []byte{}
+				assert.DeepEqual(t, expectedResult, store.Get("dest{t}").Value.(*ByteArray).data)
+			},
+		},
+		"BITOP NOT (known string)": {
+			setup: func() {
+				store.Put("s{t}", store.NewObj(&ByteArray{data: []byte{0xaa, 0x00, 0xff, 0x55}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray))
+			},
+			input:  []string{"NOT", "dest{t}", "s{t}"},
+			output: clientio.Encode(4, true),
+			validator: func(output []byte) {
+				expectedResult := []byte{0x55, 0xff, 0x00, 0xaa}
+				assert.DeepEqual(t, expectedResult, store.Get("dest{t}").Value.(*ByteArray).data)
+			},
+		},
+		"BITOP where dest and target are the same key": {
+			setup: func() {
+				store.Put("s", store.NewObj(&ByteArray{data: []byte{0xaa, 0x00, 0xff, 0x55}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray))
+			},
+			input:  []string{"NOT", "s", "s"},
+			output: clientio.Encode(4, true),
+			validator: func(output []byte) {
+				expectedResult := []byte{0x55, 0xff, 0x00, 0xaa}
+				assert.DeepEqual(t, expectedResult, store.Get("s").Value.(*ByteArray).data)
+			},
+		},
+		"BITOP AND|OR|XOR don't change the string with single input key": {
+			setup: func() {
+				store.Put("a{t}", store.NewObj(&ByteArray{data: []byte{0x01, 0x02, 0xff}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray))
+			},
+			input:  []string{"AND", "res1{t}", "a{t}"},
+			output: clientio.Encode(3, true),
+			validator: func(output []byte) {
+				expectedResult := []byte{0x01, 0x02, 0xff}
+				assert.DeepEqual(t, expectedResult, store.Get("res1{t}").Value.(*ByteArray).data)
+			},
+		},
+		"BITOP missing key is considered a stream of zero": {
+			setup: func() {
+				store.Put("a{t}", store.NewObj(&ByteArray{data: []byte{0x01, 0x02, 0xff}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray))
+			},
+			input:  []string{"AND", "res1{t}", "no-such-key{t}", "a{t}"},
+			output: clientio.Encode(3, true),
+			validator: func(output []byte) {
+				expectedResult := []byte{0x00, 0x00, 0x00}
+				assert.DeepEqual(t, expectedResult, store.Get("res1{t}").Value.(*ByteArray).data)
+			},
+		},
+		"BITOP shorter keys are zero-padded to the key with max length": {
+			setup: func() {
+				store.Put("a{t}", store.NewObj(&ByteArray{data: []byte{0x01, 0x02, 0xff, 0xff}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray))
+				store.Put("b{t}", store.NewObj(&ByteArray{data: []byte{0x01, 0x02, 0xff}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray))
+			},
+			input:  []string{"AND", "res1{t}", "a{t}", "b{t}"},
+			output: clientio.Encode(4, true),
+			validator: func(output []byte) {
+				expectedResult := []byte{0x01, 0x02, 0xff, 0x00}
+				assert.DeepEqual(t, expectedResult, store.Get("res1{t}").Value.(*ByteArray).data)
+			},
+		},
+		"BITOP with non string source key": {
+			setup: func() {
+				store.Put("a{t}", store.NewObj("1", maxExDuration, object.ObjTypeString, object.ObjEncodingRaw))
+				store.Put("b{t}", store.NewObj("2", maxExDuration, object.ObjTypeString, object.ObjEncodingRaw))
+				store.Put("c{t}", store.NewObj([]byte("foo"), maxExDuration, object.ObjTypeByteList, object.ObjEncodingRaw))
+			},
+			input:  []string{"XOR", "dest{t}", "a{t}", "b{t}", "c{t}", "d{t}"},
+			output: []byte("-WRONGTYPE Operation against a key holding the wrong kind of value\r\n"),
+		},
+		"BITOP with empty string after non empty string": {
+			setup: func() {
+				store.Put("a{t}", store.NewObj(&ByteArray{data: []byte("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")}, -1, object.ObjTypeByteArray, object.ObjEncodingByteArray))
+			},
+			input:  []string{"OR", "x{t}", "a{t}", "b{t}"},
+			output: clientio.Encode(32, true),
+		},
+	}
+
+	runEvalTests(t, tests, evalBITOP, store)
+}
+
+func BenchmarkEvalBITOP(b *testing.B) {
+	store := dstore.NewStore(nil)
+
+	// Setup initial data for benchmarking
+	store.Put("key1", store.NewObj(&ByteArray{data: []byte{0x01, 0x02, 0xff}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray))
+	store.Put("key2", store.NewObj(&ByteArray{data: []byte{0x01, 0x02, 0xff}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray))
+
+	// Define different operations to benchmark
+	operations := []struct {
+		name string
+		op   string
+	}{
+		{"AND", "AND"},
+		{"OR", "OR"},
+		{"XOR", "XOR"},
+		{"NOT", "NOT"},
+	}
+
+	for _, operation := range operations {
+		b.Run(fmt.Sprintf("BITOP_%s", operation.name), func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				if operation.op == "NOT" {
+					evalBITOP([]string{operation.op, "dest", "key1"}, store)
+				} else {
+					evalBITOP([]string{operation.op, "dest", "key1", "key2"}, store)
+				}
+			}
+		})
+	}
+}