Skip to content

Commit

Permalink
DiceDB#412: Bitmap commands: Fixed deviation from redis implementation (
Browse files Browse the repository at this point in the history
DiceDB#439)

Co-authored-by: ayadav16 <ayadav7@binghamton.edu>
apoorvyadav1111 and ayadav16 authored Sep 9, 2024
1 parent 83392ce commit 7ec2ad0
Showing 2 changed files with 277 additions and 67 deletions.
157 changes: 90 additions & 67 deletions internal/eval/eval.go
Original file line number Diff line number Diff line change
@@ -1133,7 +1133,7 @@ func evalSETBIT(args []string, store *dstore.Store) []byte {
}

obj := store.Get(key)
requiredByteArraySize := offset/8 + 1
requiredByteArraySize := offset>>3 + 1

if obj == nil {
obj = store.NewObj(NewByteArray(int(requiredByteArraySize)), -1, dstore.ObjTypeByteArray, dstore.ObjEncodingByteArray)
@@ -1170,12 +1170,6 @@ func evalSETBIT(args []string, store *dstore.Store) []byte {
resp := byteArray.GetBit(int(offset))
byteArray.SetBit(int(offset), value)

// if earlier bit was 1 and the new bit is 0
// propability is that, we can remove some space from the byte array
if resp && !value {
byteArray.ResizeIfNecessary()
}

// We are returning newObject here so it is thread-safe
// Old will be removed by GC
newObj, err := ByteSliceToObj(store, obj, byteArray.data, oType, oEnc)
@@ -1220,20 +1214,12 @@ func evalGETBIT(args []string, store *dstore.Store) []byte {
if obj == nil {
return clientio.Encode(0, true)
}
// if object is a set type, return error
if dstore.AssertType(obj.TypeEncoding, dstore.ObjTypeSet) == nil {
return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr)
}

requiredByteArraySize := offset/8 + 1

// handle the case when it is string
if dstore.AssertType(obj.TypeEncoding, dstore.ObjTypeString) == nil {
return diceerrors.NewErrWithMessage("value is not a valid byte array")
}

// handle the case when it is byte array
if dstore.AssertType(obj.TypeEncoding, dstore.ObjTypeByteArray) == nil {
requiredByteArraySize := offset>>3 + 1
switch oType, _ := dstore.ExtractTypeEncoding(obj); oType {
case dstore.ObjTypeSet:
return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr)
case dstore.ObjTypeByteArray:
byteArray := obj.Value.(*ByteArray)
byteArrayLength := byteArray.Length

@@ -1246,9 +1232,22 @@ func evalGETBIT(args []string, store *dstore.Store) []byte {
return clientio.Encode(1, true)
}
return clientio.Encode(0, true)
case dstore.ObjTypeString, dstore.ObjTypeInt:
byteArray, err := NewByteArrayFromObj(obj)
if err != nil {
return diceerrors.NewErrWithMessage(diceerrors.WrongTypeErr)
}
if requiredByteArraySize > byteArray.Length {
return clientio.Encode(0, true)
}
value := byteArray.GetBit(int(offset))
if value {
return clientio.Encode(1, true)
}
return clientio.Encode(0, true)
default:
return clientio.Encode(0, true)
}

return clientio.Encode(0, true)
}

func evalBITCOUNT(args []string, store *dstore.Store) []byte {
@@ -1287,6 +1286,11 @@ func evalBITCOUNT(args []string, store *dstore.Store) []byte {
valueLength = int64(len(value))
}

if dstore.AssertType(obj.TypeEncoding, dstore.ObjTypeInt) == nil {
value = []byte(strconv.FormatInt(valueInterface.(int64), 10))
valueLength = int64(len(value))
}

// defining constants of the function
start := int64(0)
end := valueLength - 1
@@ -1338,8 +1342,7 @@ func evalBITCOUNT(args []string, store *dstore.Store) []byte {
return clientio.Encode(bitCount, true)
}
startBitRange := start / 8
endBitRange := end / 8

endBitRange := min(end/8, valueLength-1)
for i := startBitRange; i <= endBitRange; i++ {
if i == startBitRange {
considerBits := start % 8
@@ -1371,46 +1374,67 @@ func evalBITOP(args []string, store *dstore.Store) []byte {
if !(operation == AND || operation == OR || operation == XOR || operation == NOT) {
return diceerrors.NewErrWithMessage(diceerrors.SyntaxErr)
}
// if operation is not, then keys length should be only 1
if operation == NOT && len(keys) != 1 {
return diceerrors.NewErrWithMessage("BITOP NOT must be called with a single source key.")
}

if operation == NOT {
obj := store.Get(keys[0])
if len(keys) != 1 {
return diceerrors.NewErrWithMessage("BITOP NOT must be called with a single source key.")
}
key := keys[0]
obj := store.Get(key)
if obj == nil {
return clientio.Encode(0, true)
}

var value []byte
if dstore.AssertType(obj.TypeEncoding, dstore.ObjTypeByteArray) == nil {

switch oType, _ := dstore.ExtractTypeEncoding(obj); oType {
case dstore.ObjTypeByteArray:
byteArray := obj.Value.(*ByteArray)
byteArrayObject := *byteArray
value = byteArrayObject.data
} else {
return diceerrors.NewErrWithMessage("value is not a valid byte array")
}

// perform the operation
result := make([]byte, len(value))
for i := 0; i < len(value); i++ {
result[i] = ^value[i]
}
// perform the operation
result := make([]byte, len(value))
for i := 0; i < len(value); i++ {
result[i] = ^value[i]
}

// initialize result with byteArray
operationResult := NewByteArray(len(result))
operationResult.data = result
operationResult.Length = int64(len(result))
// initialize result with byteArray
operationResult := NewByteArray(len(result))
operationResult.data = result
operationResult.Length = int64(len(result))

// resize the byte array if necessary
operationResult.ResizeIfNecessary()
// resize the byte array if necessary
operationResult.ResizeIfNecessary()

// create object related to result
obj = store.NewObj(operationResult, -1, dstore.ObjTypeByteArray, dstore.ObjEncodingByteArray)
// create object related to result
obj = store.NewObj(operationResult, -1, dstore.ObjTypeByteArray, dstore.ObjEncodingByteArray)

// store the result in destKey
store.Put(destKey, obj)
return clientio.Encode(len(value), true)
// store the result in destKey
store.Put(destKey, obj)
return clientio.Encode(len(value), true)
case dstore.ObjTypeString, dstore.ObjTypeInt:
if oType == dstore.ObjTypeString {
value = []byte(obj.Value.(string))
} else {
value = []byte(strconv.FormatInt(obj.Value.(int64), 10))
}
// perform the operation
result := make([]byte, len(value))
for i := 0; i < len(value); i++ {
result[i] = ^value[i]
}
resOType, resOEnc := deduceTypeEncoding(string(result))
var storedValue interface{}
if resOType == dstore.ObjTypeInt {
storedValue, _ = strconv.ParseInt(string(result), 10, 64)
} else {
storedValue = string(result)
}
store.Put(destKey, store.NewObj(storedValue, -1, resOType, resOEnc))
return clientio.Encode(len(value), true)
default:
return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr)
}
}
// if operation is AND, OR, XOR
values := make([][]byte, len(keys))
@@ -1422,16 +1446,22 @@ func evalBITOP(args []string, store *dstore.Store) []byte {
values[i] = make([]byte, 0)
} else {
// handle the case when it is byte array
if dstore.AssertType(obj.TypeEncoding, dstore.ObjTypeByteArray) == nil {
switch oType, _ := dstore.ExtractTypeEncoding(obj); oType {
case dstore.ObjTypeByteArray:
byteArray := obj.Value.(*ByteArray)
byteArrayObject := *byteArray
values[i] = byteArrayObject.data
} else {
case dstore.ObjTypeString:
value := obj.Value.(string)
values[i] = []byte(value)
case dstore.ObjTypeInt:
value := strconv.FormatInt(obj.Value.(int64), 10)
values[i] = []byte(value)
default:
return diceerrors.NewErrWithMessage("value is not a valid byte array")
}
}
}

// get the length of the largest value
maxLength := 0
minLength := len(values[0])
@@ -1441,22 +1471,18 @@ func evalBITOP(args []string, store *dstore.Store) []byte {
maxLength = len(value)
maxKeyIterator = keyIterator
}
if len(value) < minLength {
minLength = len(value)
}
minLength = min(minLength, len(value))
}

result := make([]byte, maxLength)
if operation == AND {
for i := 0; i < maxLength; i++ {
result[i] = 0
if i < minLength {
result[i] = values[maxKeyIterator][i]
} else {
result[i] = 0
}
}
}
if operation == XOR || operation == OR {
} else {
for i := 0; i < maxLength; i++ {
result[i] = 0x00
}
@@ -1465,24 +1491,21 @@ func evalBITOP(args []string, store *dstore.Store) []byte {
// perform the operation
for _, value := range values {
for i := 0; i < len(value); i++ {
if operation == AND {
switch operation {
case AND:
result[i] &= value[i]
} else if operation == OR {
case OR:
result[i] |= value[i]
} else if operation == XOR {
case XOR:
result[i] ^= value[i]
}
}
}

// initialize result with byteArray
operationResult := NewByteArray(len(result))
operationResult.data = result
operationResult.Length = int64(len(result))

// resize the byte array if necessary
operationResult.ResizeIfNecessary()

// create object related to result
operationResultObject := store.NewObj(operationResult, -1, dstore.ObjTypeByteArray, dstore.ObjEncodingByteArray)

187 changes: 187 additions & 0 deletions tests/bit_ops_string_int_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package tests

import (
"fmt"
"math/rand"
"testing"

"gotest.tools/v3/assert"
)

func TestBitOpsString(t *testing.T) {
// test code

conn := getLocalConnection()
defer conn.Close()
// foobar in bits is 01100110 01101111 01101111 01100010 01100001 01110010
fooBarBits := "011001100110111101101111011000100110000101110010"
// randomly get 8 bits for testing
testOffets := make([]int, 8)

for i := 0; i < 8; i++ {
testOffets[i] = rand.Intn(len(fooBarBits))
}

getBitTestCommands := make([]string, 8+1)
getBitTestExpected := make([]interface{}, 8+1)

getBitTestCommands[0] = "SET foo foobar"
getBitTestExpected[0] = "OK"

for i := 1; i < 8+1; i++ {
getBitTestCommands[i] = fmt.Sprintf("GETBIT foo %d", testOffets[i-1])
getBitTestExpected[i] = int64(fooBarBits[testOffets[i-1]] - '0')
}

testCases := []struct {
name string
cmds []string
expected []interface{}
assert_type []string
}{
{
name: "Getbit of a key containing a string",
cmds: getBitTestCommands,
expected: getBitTestExpected,
assert_type: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
},
{
name: "Getbit of a key containing an integer",
cmds: []string{"SET foo 10", "GETBIT foo 0", "GETBIT foo 1", "GETBIT foo 2", "GETBIT foo 3", "GETBIT foo 4", "GETBIT foo 5", "GETBIT foo 6", "GETBIT foo 7"},
expected: []interface{}{"OK", int64(0), int64(0), int64(1), int64(1), int64(0), int64(0), int64(0), int64(1)},
assert_type: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
}, {
name: "Getbit of a key containing an integer 2nd byte",
cmds: []string{"SET foo 10", "GETBIT foo 8", "GETBIT foo 9", "GETBIT foo 10", "GETBIT foo 11", "GETBIT foo 12", "GETBIT foo 13", "GETBIT foo 14", "GETBIT foo 15"},
expected: []interface{}{"OK", int64(0), int64(0), int64(1), int64(1), int64(0), int64(0), int64(0), int64(0)},
assert_type: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
},
{
name: "Getbit of a key with an offset greater than the length of the string in bits",
cmds: []string{"SET foo foobar", "GETBIT foo 100", "GETBIT foo 48", "GETBIT foo 47"},
expected: []interface{}{"OK", int64(0), int64(0), int64(0)},
assert_type: []string{"equal", "equal", "equal", "equal"},
},
{
name: "Bitcount of a key containing a string",
cmds: []string{"SET foo foobar", "BITCOUNT foo 0 -1", "BITCOUNT foo", "BITCOUNT foo 0 0", "BITCOUNT foo 1 1", "BITCOUNT foo 1 1 Byte", "BITCOUNT foo 5 30 BIT"},
expected: []interface{}{"OK", int64(26), int64(26), int64(4), int64(6), int64(6), int64(17)},
assert_type: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal"},
},
{
name: "Bitcount of a key containing an integer",
cmds: []string{"SET foo 10", "BITCOUNT foo 0 -1", "BITCOUNT foo", "BITCOUNT foo 0 0", "BITCOUNT foo 1 1", "BITCOUNT foo 1 1 Byte", "BITCOUNT foo 5 30 BIT"},
expected: []interface{}{"OK", int64(5), int64(5), int64(3), int64(2), int64(2), int64(3)},
assert_type: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal"},
},
{
name: "Setbit of a key containing a string",
cmds: []string{"SET foo foobar", "setbit foo 7 1", "get foo", "setbit foo 49 1", "setbit foo 50 1", "get foo", "setbit foo 49 0", "get foo"},
expected: []interface{}{"OK", int64(0), "goobar", int64(0), int64(0), "goobar`", int64(1), "goobar "},
assert_type: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
},
{
name: "Setbit of a key must not change the expiry of the key if expiry is set",
cmds: []string{"SET foo foobar", "EXPIRE foo 100", "TTL foo", "SETBIT foo 7 1", "TTL foo"},
expected: []interface{}{"OK", int64(1), int64(100), int64(0), int64(100)},
assert_type: []string{"equal", "equal", "less", "equal", "less"},
},
{
name: "Setbit of a key must not add expiry to the key if expiry is not set",
cmds: []string{"SET foo foobar", "TTL foo", "SETBIT foo 7 1", "TTL foo"},
expected: []interface{}{"OK", int64(-1), int64(0), int64(-1)},
assert_type: []string{"equal", "equal", "equal", "equal"},
},
{
name: "Bitop not of a key containing a string",
cmds: []string{"SET foo foobar", "BITOP NOT baz foo", "GET baz", "BITOP NOT bazz baz", "GET bazz"},
expected: []interface{}{"OK", int64(6), "\x99\x90\x90\x9d\x9e\x8d", int64(6), "foobar"},
assert_type: []string{"equal", "equal", "equal", "equal", "equal"},
},
{
name: "Bitop not of a key containing an integer",
cmds: []string{"SET foo 10", "BITOP NOT baz foo", "GET baz", "BITOP NOT bazz baz", "GET bazz"},
expected: []interface{}{"OK", int64(2), "\xce\xcf", int64(2), int64(10)},
assert_type: []string{"equal", "equal", "equal", "equal", "equal"},
},
{
name: "Get a string created with setbit",
cmds: []string{"SETBIT foo 1 1", "SETBIT foo 3 1", "GET foo"},
expected: []interface{}{int64(0), int64(0), "P"},
assert_type: []string{"equal", "equal", "equal"},
},
{
name: "Bitop and of keys containing a string and get the destkey",
cmds: []string{"SET foo foobar", "SET baz abcdef", "BITOP AND bazz foo baz", "GET bazz"},
expected: []interface{}{"OK", "OK", int64(6), "`bc`ab"},
assert_type: []string{"equal", "equal", "equal", "equal"},
},
{
name: "BITOP AND of keys containing integers and get the destkey",
cmds: []string{"SET foo 10", "SET baz 5", "BITOP AND bazz foo baz", "GET bazz"},
expected: []interface{}{"OK", "OK", int64(2), "1\x00"},
assert_type: []string{"equal", "equal", "equal", "equal"},
},
{
name: "Bitop or of keys containing a string, a bytearray and get the destkey",
cmds: []string{"MSET foo foobar baz abcdef", "SETBIT bazz 8 1", "BITOP and bazzz foo baz bazz", "GET bazzz"},
expected: []interface{}{"OK", int64(0), int64(6), "\x00\x00\x00\x00\x00\x00"},
assert_type: []string{"equal", "equal", "equal", "equal"},
},
{
name: "BITOP OR of keys containing strings and get the destkey",
cmds: []string{"MSET foo foobar baz abcdef", "BITOP OR bazz foo baz", "GET bazz"},
expected: []interface{}{"OK", int64(6), "goofev"},
assert_type: []string{"equal", "equal", "equal"},
},
{
name: "BITOP OR of keys containing integers and get the destkey",
cmds: []string{"SET foo 10", "SET baz 5", "BITOP OR bazz foo baz", "GET bazz"},
expected: []interface{}{"OK", "OK", int64(2), "50"},
assert_type: []string{"equal", "equal", "equal", "equal"},
},
{
name: "BITOP OR of keys containing strings and a bytearray and get the destkey",
cmds: []string{"MSET foo foobar baz abcdef", "SETBIT bazz 8 1", "BITOP OR bazzz foo baz bazz", "GET bazzz", "SETBIT bazz 8 0", "SETBIT bazz 49 1", "BITOP OR bazzz foo baz bazz", "GET bazzz"},
expected: []interface{}{"OK", int64(0), int64(6), "g\xefofev", int64(1), int64(0), int64(7), "goofev@"},
assert_type: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
},
{
name: "BITOP XOR of keys containing strings and get the destkey",
cmds: []string{"MSET foo foobar baz abcdef", "BITOP XOR bazz foo baz", "GET bazz"},
expected: []interface{}{"OK", int64(6), "\a\r\x0c\x06\x04\x14"},
assert_type: []string{"equal", "equal", "equal"},
},
{
name: "BITOP XOR of keys containing strings and a bytearray and get the destkey",
cmds: []string{"MSET foo foobar baz abcdef", "SETBIT bazz 8 1", "BITOP XOR bazzz foo baz bazz", "GET bazzz", "SETBIT bazz 8 0", "SETBIT bazz 49 1", "BITOP XOR bazzz foo baz bazz", "GET bazzz", "Setbit bazz 49 0", "bitop xor bazzz foo baz bazz", "get bazzz"},
expected: []interface{}{"OK", int64(0), int64(6), "\a\x8d\x0c\x06\x04\x14", int64(1), int64(0), int64(7), "\a\r\x0c\x06\x04\x14@", int64(1), int64(7), "\a\r\x0c\x06\x04\x14\x00"},
assert_type: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
},
{
name: "BITOP XOR of keys containing integers and get the destkey",
cmds: []string{"SET foo 10", "SET baz 5", "BITOP XOR bazz foo baz", "GET bazz"},
expected: []interface{}{"OK", "OK", int64(2), "\x040"},
assert_type: []string{"equal", "equal", "equal", "equal"},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Delete the key before running the test
fireCommand(conn, "DEL foo")
fireCommand(conn, "DEL baz")
fireCommand(conn, "DEL bazz")
fireCommand(conn, "DEL bazzz")
for i := 0; i < len(tc.cmds); i++ {
res := fireCommand(conn, tc.cmds[i])
switch tc.assert_type[i] {
case "equal":
assert.Equal(t, res, tc.expected[i])
case "less":
assert.Assert(t, res.(int64) <= tc.expected[i].(int64), "CMD: %s Expected %d to be less than or equal to %d", tc.cmds[i], res, tc.expected[i])
}
}
})
}
}

0 comments on commit 7ec2ad0

Please sign in to comment.