Skip to content

Commit

Permalink
#639: Feature: BITFIELD command with all subcommands (#723)
Browse files Browse the repository at this point in the history
Co-authored-by: A Yadav <[email protected]>
Co-authored-by: Jyotinder Singh <[email protected]>
  • Loading branch information
3 people authored Oct 4, 2024
1 parent 03c4b66 commit 1635582
Show file tree
Hide file tree
Showing 8 changed files with 641 additions and 3 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ dicedb
venv
__pycache__
.idea/
./dice
*.rdb
dice

# build output
Expand Down
256 changes: 256 additions & 0 deletions integration_tests/commands/async/bitfield_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
package async

import (
"testing"
"time"

testifyAssert "github.com/stretchr/testify/assert"
)

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

FireCommand(conn, "FLUSHDB")
defer FireCommand(conn, "FLUSHDB") // clean up after all test cases
syntaxErrMsg := "ERR syntax error"
bitFieldTypeErrMsg := "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is."
integerErrMsg := "ERR value is not an integer or out of range"
overflowErrMsg := "ERR Invalid OVERFLOW type specified"

testCases := []struct {
Name string
Commands []string
Expected []interface{}
Delay []time.Duration
CleanUp []string
}{
{
Name: "BITFIELD Arity Check",
Commands: []string{"bitfield"},
Expected: []interface{}{"ERR wrong number of arguments for 'bitfield' command"},
Delay: []time.Duration{0},
CleanUp: []string{},
},
{
Name: "BITFIELD on unsupported type of SET",
Commands: []string{"SADD bits a b c", "bitfield bits"},
Expected: []interface{}{int64(3), "WRONGTYPE Operation against a key holding the wrong kind of value"},
Delay: []time.Duration{0, 0},
CleanUp: []string{"DEL bits"},
},
{
Name: "BITFIELD on unsupported type of JSON",
Commands: []string{"json.set bits $ 1", "bitfield bits"},
Expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
Delay: []time.Duration{0, 0},
CleanUp: []string{"DEL bits"},
},
{
Name: "BITFIELD on unsupported type of HSET",
Commands: []string{"HSET bits a 1", "bitfield bits"},
Expected: []interface{}{int64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"},
Delay: []time.Duration{0, 0},
CleanUp: []string{"DEL bits"},
},
{
Name: "BITFIELD with syntax errors",
Commands: []string{
"bitfield bits set u8 0 255 incrby u8 0 100 get u8",
"bitfield bits set a8 0 255 incrby u8 0 100 get u8",
"bitfield bits set u8 a 255 incrby u8 0 100 get u8",
"bitfield bits set u8 0 255 incrby u8 0 100 overflow wraap",
"bitfield bits set u8 0 incrby u8 0 100 get u8 288",
},
Expected: []interface{}{
syntaxErrMsg,
bitFieldTypeErrMsg,
"ERR bit offset is not an integer or out of range",
overflowErrMsg,
integerErrMsg,
},
Delay: []time.Duration{0, 0, 0, 0, 0},
CleanUp: []string{"Del bits"},
},
{
Name: "BITFIELD signed SET and GET basics",
Commands: []string{"bitfield bits set i8 0 -100", "bitfield bits set i8 0 101", "bitfield bits get i8 0"},
Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(-100)}, []interface{}{int64(101)}},
Delay: []time.Duration{0, 0, 0},
CleanUp: []string{"DEL bits"},
},
{
Name: "BITFIELD unsigned SET and GET basics",
Commands: []string{"bitfield bits set u8 0 255", "bitfield bits set u8 0 100", "bitfield bits get u8 0"},
Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(255)}, []interface{}{int64(100)}},
Delay: []time.Duration{0, 0, 0},
CleanUp: []string{"DEL bits"},
},
{
Name: "BITFIELD signed SET and GET together",
Commands: []string{"bitfield bits set i8 0 255 set i8 0 100 get i8 0"},
Expected: []interface{}{[]interface{}{int64(0), int64(-1), int64(100)}},
Delay: []time.Duration{0},
CleanUp: []string{"DEL bits"},
},
{
Name: "BITFIELD unsigned with SET, GET and INCRBY arguments",
Commands: []string{"bitfield bits set u8 0 255 incrby u8 0 100 get u8 0"},
Expected: []interface{}{[]interface{}{int64(0), int64(99), int64(99)}},
Delay: []time.Duration{0},
CleanUp: []string{"DEL bits"},
},
{
Name: "BITFIELD with only key as argument",
Commands: []string{"bitfield bits"},
Expected: []interface{}{[]interface{}{}},
Delay: []time.Duration{0},
CleanUp: []string{"DEL bits"},
},
{
Name: "BITFIELD #<idx> form",
Commands: []string{
"bitfield bits set u8 #0 65",
"bitfield bits set u8 #1 66",
"bitfield bits set u8 #2 67",
"get bits",
},
Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(0)}, []interface{}{int64(0)}, "ABC"},
Delay: []time.Duration{0, 0, 0, 0},
CleanUp: []string{"DEL bits"},
},
{
Name: "BITFIELD basic INCRBY form",
Commands: []string{
"bitfield bits set u8 #0 10",
"bitfield bits incrby u8 #0 100",
"bitfield bits incrby u8 #0 100",
},
Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(110)}, []interface{}{int64(210)}},
Delay: []time.Duration{0, 0, 0},
CleanUp: []string{"DEL bits"},
},
{
Name: "BITFIELD chaining of multiple commands",
Commands: []string{
"bitfield bits set u8 #0 10",
"bitfield bits incrby u8 #0 100 incrby u8 #0 100",
},
Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(110), int64(210)}},
Delay: []time.Duration{0, 0},
CleanUp: []string{"DEL bits"},
},
{
Name: "BITFIELD unsigned overflow wrap",
Commands: []string{
"bitfield bits set u8 #0 100",
"bitfield bits overflow wrap incrby u8 #0 257",
"bitfield bits get u8 #0",
"bitfield bits overflow wrap incrby u8 #0 255",
"bitfield bits get u8 #0",
},
Expected: []interface{}{
[]interface{}{int64(0)},
[]interface{}{int64(101)},
[]interface{}{int64(101)},
[]interface{}{int64(100)},
[]interface{}{int64(100)},
},
Delay: []time.Duration{0, 0, 0, 0, 0},
CleanUp: []string{"DEL bits"},
},
{
Name: "BITFIELD unsigned overflow sat",
Commands: []string{
"bitfield bits set u8 #0 100",
"bitfield bits overflow sat incrby u8 #0 257",
"bitfield bits get u8 #0",
"bitfield bits overflow sat incrby u8 #0 -255",
"bitfield bits get u8 #0",
},
Expected: []interface{}{
[]interface{}{int64(0)},
[]interface{}{int64(255)},
[]interface{}{int64(255)},
[]interface{}{int64(0)},
[]interface{}{int64(0)},
},
Delay: []time.Duration{0, 0, 0, 0, 0},
CleanUp: []string{"DEL bits"},
},
{
Name: "BITFIELD signed overflow wrap",
Commands: []string{
"bitfield bits set i8 #0 100",
"bitfield bits overflow wrap incrby i8 #0 257",
"bitfield bits get i8 #0",
"bitfield bits overflow wrap incrby i8 #0 255",
"bitfield bits get i8 #0",
},
Expected: []interface{}{
[]interface{}{int64(0)},
[]interface{}{int64(101)},
[]interface{}{int64(101)},
[]interface{}{int64(100)},
[]interface{}{int64(100)},
},
Delay: []time.Duration{0, 0, 0, 0, 0},
CleanUp: []string{"DEL bits"},
},
{
Name: "BITFIELD signed overflow sat",
Commands: []string{
"bitfield bits set u8 #0 100",
"bitfield bits overflow sat incrby i8 #0 257",
"bitfield bits get i8 #0",
"bitfield bits overflow sat incrby i8 #0 -255",
"bitfield bits get i8 #0",
},
Expected: []interface{}{
[]interface{}{int64(0)},
[]interface{}{int64(127)},
[]interface{}{int64(127)},
[]interface{}{int64(-128)},
[]interface{}{int64(-128)},
},
Delay: []time.Duration{0, 0, 0, 0, 0},
CleanUp: []string{"DEL bits"},
},
{
Name: "BITFIELD regression 1",
Commands: []string{"set bits 1", "bitfield bits get u1 0"},
Expected: []interface{}{"OK", []interface{}{int64(0)}},
Delay: []time.Duration{0, 0},
CleanUp: []string{"DEL bits"},
},
{
Name: "BITFIELD regression 2",
Commands: []string{
"bitfield mystring set i8 0 10",
"bitfield mystring set i8 64 10",
"bitfield mystring incrby i8 10 99900",
},
Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(0)}, []interface{}{int64(60)}},
Delay: []time.Duration{0, 0, 0},
CleanUp: []string{"DEL mystring"},
},
}

for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {

for i := 0; i < len(tc.Commands); i++ {
if tc.Delay[i] > 0 {
time.Sleep(tc.Delay[i])
}
result := FireCommand(conn, tc.Commands[i])
expected := tc.Expected[i]
testifyAssert.Equal(t, expected, result)
}

for _, cmd := range tc.CleanUp {
FireCommand(conn, cmd)
}
})
}
}
3 changes: 3 additions & 0 deletions internal/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ const (
InternalServerError = "-ERR: Internal server error, unable to process command"
InvalidFloatErr = "-ERR value is not a valid float"
InvalidIntErr = "-ERR value is not a valid integer"
InvalidBitfieldType = "-ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is."
BitfieldOffsetErr = "-ERR bit offset is not an integer or out of range"
OverflowTypeErr = "-ERR Invalid OVERFLOW type specified"
)

type DiceError struct {
Expand Down
85 changes: 85 additions & 0 deletions internal/eval/bytearray.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,91 @@ func (b *ByteArray) DeepCopy() *ByteArray {
return copyArray
}

func (b *ByteArray) getBits(offset, width int, signed bool) int64 {
extraBits := 0
if offset+width > int(b.Length)*8 {
// If bits exceed the current data size, we will pad the result with zeros for the missing bits.
extraBits = offset + width - int(b.Length)*8
}
var value int64
for i := 0; i < width-extraBits; i++ {
value <<= 1
byteIndex := (offset + i) / 8
bitIndex := 7 - ((offset + i) % 8)
if b.data[byteIndex]&(1<<bitIndex) != 0 {
value |= 1 << 0
}
}
value <<= int64(extraBits)
if signed && (value&(1<<(width-1)) != 0) {
value -= 1 << width
}
return value
}

func (b *ByteArray) setBits(offset, width int, value int64) {
if offset+width > int(b.Length)*8 {
newSize := (offset + width + 7) / 8
b.IncreaseSize(newSize)
}
for i := 0; i < width; i++ {
byteIndex := (offset + i) / 8
bitIndex := (offset + i) % 8
if value&(1<<i) != 0 {
b.data[byteIndex] |= 1 << bitIndex
} else {
b.data[byteIndex] &^= 1 << bitIndex
}
}
}

// Increment value at a specific bitfield and handle overflow.
func (b *ByteArray) incrByBits(offset, width int, increment int64, overflow string, signed bool) (int64, error) {
if offset+width > int(b.Length)*8 {
newSize := (offset + width + 7) / 8
b.IncreaseSize(newSize)
}

value := b.getBits(offset, width, signed)
newValue := value + increment

var maxVal, minVal int64
if signed {
maxVal = int64(1<<(width-1) - 1)
minVal = int64(-1 << (width - 1))
} else {
maxVal = int64(1<<width - 1)
minVal = 0
}

switch overflow {
case WRAP:
if signed {
rangeSize := maxVal - minVal + 1
newValue = ((newValue-minVal)%rangeSize+rangeSize)%rangeSize + minVal
} else {
newValue %= maxVal + 1
}
case SAT:
// Handle saturation
if newValue > maxVal {
newValue = maxVal
} else if newValue < minVal {
newValue = minVal
}
case FAIL:
// Handle failure on overflow
if newValue > maxVal || newValue < minVal {
return value, errors.New("overflow detected")
}
default:
return value, errors.New("invalid overflow type")
}

b.setBits(offset, width, newValue)
return newValue, nil
}

// population counting, counts the number of set bits in a byte
// Using: https://en.wikipedia.org/wiki/Hamming_weight
func popcount(x byte) byte {
Expand Down
Loading

0 comments on commit 1635582

Please sign in to comment.