Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#639: Feature: BITFIELD command with all subcommands #723

Merged
merged 13 commits into from
Oct 4, 2024
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
255 changes: 255 additions & 0 deletions integration_tests/commands/async/bitfield_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
package async

import (
"testing"
"time"

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

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

defer FireCommand(conn, "FLUSHDB") // clean up after all test cases
JyotinderSingh marked this conversation as resolved.
Show resolved Hide resolved
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"
)

var (
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