diff --git a/.gitignore b/.gitignore index fb0f6a316..e7a3fff46 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ dicedb venv __pycache__ .idea/ +./dice +*.rdb dice # build output diff --git a/integration_tests/commands/async/bitfield_test.go b/integration_tests/commands/async/bitfield_test.go new file mode 100644 index 000000000..10df4e548 --- /dev/null +++ b/integration_tests/commands/async/bitfield_test.go @@ -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 # 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) + } + }) + } +} diff --git a/internal/errors/errors.go b/internal/errors/errors.go index 17f0b7681..f182e4242 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -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 { diff --git a/internal/eval/bytearray.go b/internal/eval/bytearray.go index 6cdb98e0f..ff612364b 100644 --- a/internal/eval/bytearray.go +++ b/internal/eval/bytearray.go @@ -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< 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< 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< 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 { diff --git a/internal/eval/commands.go b/internal/eval/commands.go index bb2d4a29d..410fa8d34 100644 --- a/internal/eval/commands.go +++ b/internal/eval/commands.go @@ -1004,6 +1004,30 @@ var ( Arity: -4, KeySpecs: KeySpecs{BeginIndex: 1}, } + bitfieldCmdMeta = DiceCmdMeta{ + Name: "BITFIELD", + Info: `The command treats a string as an array of bits as well as bytearray data structure, + and is capable of addressing specific integer fields of varying bit widths + and arbitrary non (necessary) aligned offset. + In practical terms using this command you can set, for example, + a signed 5 bits integer at bit offset 1234 to a specific value, + retrieve a 31 bit unsigned integer from offset 4567. + Similarly the command handles increments and decrements of the + specified integers, providing guaranteed and well specified overflow + and underflow behavior that the user can configure. + The following is the list of supported commands. + GET -- Returns the specified bit field. + SET -- Set the specified bit field + and returns its old value. + INCRBY -- Increments or decrements + (if a negative increment is given) the specified bit field and returns the new value. + There is another subcommand that only changes the behavior of successive + INCRBY and SET subcommands calls by setting the overflow behavior: + OVERFLOW [WRAP|SAT|FAIL]`, + Arity: -1, + KeySpecs: KeySpecs{BeginIndex: 1}, + Eval: evalBITFIELD, + } hincrbyFloatCmdMeta = DiceCmdMeta{ Name: "HINCRBYFLOAT", Info: `HINCRBYFLOAT increments the specified field of a hash stored at the key, @@ -1128,6 +1152,7 @@ func init() { DiceCmds["APPEND"] = appendCmdMeta DiceCmds["ZADD"] = zaddCmdMeta DiceCmds["ZRANGE"] = zrangeCmdMeta + DiceCmds["BITFIELD"] = bitfieldCmdMeta DiceCmds["HINCRBYFLOAT"] = hincrbyFloatCmdMeta DiceCmds["HEXISTS"] = hexistsCmdMeta } diff --git a/internal/eval/constants.go b/internal/eval/constants.go index c208a755b..a9dacabb8 100644 --- a/internal/eval/constants.go +++ b/internal/eval/constants.go @@ -30,4 +30,13 @@ const ( WithValues string = "WITHVALUES" WithScores string = "WITHSCORES" REV string = "REV" + GET string = "GET" + SET string = "SET" + INCRBY string = "INCRBY" + OVERFLOW string = "OVERFLOW" + WRAP string = "WRAP" + SAT string = "SAT" + FAIL string = "FAIL" + SIGNED string = "SIGNED" + UNSIGNED string = "UNSIGNED" ) diff --git a/internal/eval/eval.go b/internal/eval/eval.go index 2b334f9e1..30ddfeb08 100644 --- a/internal/eval/eval.go +++ b/internal/eval/eval.go @@ -4698,6 +4698,211 @@ func evalZRANGE(args []string, store *dstore.Store) []byte { return clientio.Encode(result, false) } +// parseEncodingAndOffet function parses offset and encoding type for bitfield commands +// as this part is common to all subcommands +func parseEncodingAndOffset(args []string) (eType, eVal, offset interface{}, err error) { + encodingRaw := args[0] + offsetRaw := args[1] + switch encodingRaw[0] { + case 'i': + eType = SIGNED + eVal, err = strconv.ParseInt(encodingRaw[1:], 10, 64) + if err != nil { + err = diceerrors.NewErr(diceerrors.InvalidBitfieldType) + return eType, eVal, offset, err + } + if eVal.(int64) <= 0 || eVal.(int64) > 64 { + err = diceerrors.NewErr(diceerrors.InvalidBitfieldType) + return eType, eVal, offset, err + } + case 'u': + eType = UNSIGNED + eVal, err = strconv.ParseInt(encodingRaw[1:], 10, 64) + if err != nil { + err = diceerrors.NewErr(diceerrors.InvalidBitfieldType) + return eType, eVal, offset, err + } + if eVal.(int64) <= 0 || eVal.(int64) >= 64 { + err = diceerrors.NewErr(diceerrors.InvalidBitfieldType) + return eType, eVal, offset, err + } + default: + err = diceerrors.NewErr(diceerrors.InvalidBitfieldType) + return eType, eVal, offset, err + } + + switch offsetRaw[0] { + case '#': + offset, err = strconv.ParseInt(offsetRaw[1:], 10, 64) + if err != nil { + err = diceerrors.NewErr(diceerrors.BitfieldOffsetErr) + return eType, eVal, offset, err + } + offset = offset.(int64) * eVal.(int64) + default: + offset, err = strconv.ParseInt(offsetRaw, 10, 64) + if err != nil { + err = diceerrors.NewErr(diceerrors.BitfieldOffsetErr) + return eType, eVal, offset, err + } + } + return eType, eVal, offset, err +} + +// evalBITFIELD evaluates BITFIELD operations on a key store string, int or bytearray types +// it returns an array of results depending on the subcommands +// it allows mutation using SET and INCRBY commands +// returns arity error, offset type error, overflow type error, encoding type error, integer error, syntax error +// GET -- Returns the specified bit field. +// SET -- Set the specified bit field +// and returns its old value. +// INCRBY -- Increments or decrements +// (if a negative increment is given) the specified bit field and returns the new value. +// There is another subcommand that only changes the behavior of successive +// INCRBY and SET subcommands calls by setting the overflow behavior: +// OVERFLOW [WRAP|SAT|FAIL]` +func evalBITFIELD(args []string, store *dstore.Store) []byte { + if len(args) < 1 { + return diceerrors.NewErrArity("BITFIELD") + } + + var overflowType string = WRAP // Default overflow type + + type BitFieldOp struct { + Kind string + EType string + EVal int64 + Offset int64 + Value int64 + } + var ops []BitFieldOp + + for i := 1; i < len(args); { + switch strings.ToUpper(args[i]) { + case GET: + if len(args) <= i+2 { + return diceerrors.NewErrWithMessage(diceerrors.SyntaxErr) + } + eType, eVal, offset, err := parseEncodingAndOffset(args[i+1 : i+3]) + if err != nil { + return diceerrors.NewErrWithFormattedMessage(err.Error()) + } + ops = append(ops, BitFieldOp{ + Kind: GET, + EType: eType.(string), + EVal: eVal.(int64), + Offset: offset.(int64), + Value: int64(-1), + }) + i += 3 + case SET: + if len(args) <= i+3 { + return diceerrors.NewErrWithMessage(diceerrors.SyntaxErr) + } + eType, eVal, offset, err := parseEncodingAndOffset(args[i+1 : i+3]) + if err != nil { + return diceerrors.NewErrWithFormattedMessage(err.Error()) + } + value, err1 := strconv.ParseInt(args[i+3], 10, 64) + if err1 != nil { + return diceerrors.NewErrWithMessage(diceerrors.IntOrOutOfRangeErr) + } + ops = append(ops, BitFieldOp{ + Kind: SET, + EType: eType.(string), + EVal: eVal.(int64), + Offset: offset.(int64), + Value: value, + }) + i += 4 + case INCRBY: + if len(args) <= i+3 { + return diceerrors.NewErrWithMessage(diceerrors.SyntaxErr) + } + eType, eVal, offset, err := parseEncodingAndOffset(args[i+1 : i+3]) + if err != nil { + return diceerrors.NewErrWithFormattedMessage(err.Error()) + } + value, err1 := strconv.ParseInt(args[i+3], 10, 64) + if err1 != nil { + return diceerrors.NewErrWithMessage(diceerrors.IntOrOutOfRangeErr) + } + ops = append(ops, BitFieldOp{ + Kind: INCRBY, + EType: eType.(string), + EVal: eVal.(int64), + Offset: offset.(int64), + Value: value, + }) + i += 4 + case OVERFLOW: + if len(args) <= i+1 { + return diceerrors.NewErrWithMessage(diceerrors.SyntaxErr) + } + switch strings.ToUpper(args[i+1]) { + case WRAP, FAIL, SAT: + overflowType = strings.ToUpper(args[i+1]) + default: + return diceerrors.NewErrWithFormattedMessage(diceerrors.OverflowTypeErr) + } + ops = append(ops, BitFieldOp{ + Kind: OVERFLOW, + EType: overflowType, + EVal: int64(-1), + Offset: int64(-1), + Value: int64(-1), + }) + i += 2 + default: + return diceerrors.NewErrWithMessage(diceerrors.SyntaxErr) + } + } + key := args[0] + obj := store.Get(key) + if obj == nil { + obj = store.NewObj(NewByteArray(1), -1, object.ObjTypeByteArray, object.ObjEncodingByteArray) + store.Put(args[0], obj) + } + var value *ByteArray + var err error + + switch oType, _ := object.ExtractTypeEncoding(obj); oType { + case object.ObjTypeByteArray: + value = obj.Value.(*ByteArray) + case object.ObjTypeString, object.ObjTypeInt: + value, err = NewByteArrayFromObj(obj) + if err != nil { + return diceerrors.NewErrWithMessage("value is not a valid byte array") + } + default: + return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr) + } + + var result []interface{} + for _, op := range ops { + switch op.Kind { + case GET: + res := value.getBits(int(op.Offset), int(op.EVal), op.EType == SIGNED) + result = append(result, res) + case SET: + prevValue := value.getBits(int(op.Offset), int(op.EVal), op.EType == SIGNED) + value.setBits(int(op.Offset), int(op.EVal), op.Value) + result = append(result, prevValue) + case INCRBY: + res, err := value.incrByBits(int(op.Offset), int(op.EVal), op.Value, overflowType, op.EType == SIGNED) + if err != nil { + result = append(result, nil) + } else { + result = append(result, res) + } + case OVERFLOW: + overflowType = op.EType + } + } + + return clientio.Encode(result, false) +} + func evalHINCRBYFLOAT(args []string, store *dstore.Store) []byte { if len(args) < 3 { return diceerrors.NewErrArity("HINCRBYFLOAT") diff --git a/internal/eval/eval_test.go b/internal/eval/eval_test.go index 198ebaf5e..a0b041123 100644 --- a/internal/eval/eval_test.go +++ b/internal/eval/eval_test.go @@ -12,14 +12,16 @@ import ( "testing" "time" - "github.com/axiomhq/hyperloglog" + "github.com/dicedb/dice/internal/server/utils" + "github.com/bytedance/sonic" + "github.com/ohler55/ojg/jp" + + "github.com/axiomhq/hyperloglog" "github.com/dicedb/dice/internal/clientio" diceerrors "github.com/dicedb/dice/internal/errors" "github.com/dicedb/dice/internal/object" - "github.com/dicedb/dice/internal/server/utils" dstore "github.com/dicedb/dice/internal/store" - "github.com/ohler55/ojg/jp" testifyAssert "github.com/stretchr/testify/assert" "gotest.tools/v3/assert" ) @@ -104,6 +106,7 @@ func TestEval(t *testing.T) { testEvalZADD(t, store) testEvalZRANGE(t, store) testEvalHVALS(t, store) + testEvalBitField(t, store) testEvalHINCRBYFLOAT(t, store) } @@ -4976,6 +4979,56 @@ func testEvalZRANGE(t *testing.T, store *dstore.Store) { runEvalTests(t, tests, evalZRANGE, store) } + +func testEvalBitField(t *testing.T, store *dstore.Store) { + testCases := map[string]evalTestCase{ + "BITFIELD signed SET": { + input: []string{"bits", "set", "i8", "0", "-100"}, + output: clientio.Encode([]int64{0}, false), + }, + "BITFIELD GET": { + setup: func() { + args := []string{"bits", "set", "u8", "0", "255"} + evalBITFIELD(args, store) + }, + input: []string{"bits", "get", "u8", "0"}, + output: clientio.Encode([]int64{255}, false), + }, + "BITFIELD INCRBY": { + setup: func() { + args := []string{"bits", "set", "u8", "0", "255"} + evalBITFIELD(args, store) + }, + input: []string{"bits", "incrby", "u8", "0", "100"}, + output: clientio.Encode([]int64{99}, false), + }, + "BITFIELD Arity": { + input: []string{}, + output: diceerrors.NewErrArity("BITFIELD"), + }, + "BITFIELD invalid combination of commands in a single operation": { + input: []string{"bits", "SET", "u8", "0", "255", "INCRBY", "u8", "0", "100", "GET", "u8"}, + output: []byte("-ERR syntax error\r\n"), + }, + "BITFIELD invalid bitfield type": { + input: []string{"bits", "SET", "a8", "0", "255", "INCRBY", "u8", "0", "100", "GET", "u8"}, + output: []byte("-ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is.\r\n"), + }, + "BITFIELD invalid bit offset": { + input: []string{"bits", "SET", "u8", "a", "255", "INCRBY", "u8", "0", "100", "GET", "u8"}, + output: []byte("-ERR bit offset is not an integer or out of range\r\n"), + }, + "BITFIELD invalid overflow type": { + input: []string{"bits", "SET", "u8", "0", "255", "INCRBY", "u8", "0", "100", "OVERFLOW", "wraap"}, + output: []byte("-ERR Invalid OVERFLOW type specified\r\n"), + }, + "BITFIELD missing arguments in SET": { + input: []string{"bits", "SET", "u8", "0", "INCRBY", "u8", "0", "100", "GET", "u8", "288"}, + output: []byte("-ERR value is not an integer or out of range\r\n"), + }, + } + runEvalTests(t, testCases, evalBITFIELD, store) +} func testEvalHINCRBYFLOAT(t *testing.T, store *dstore.Store) { tests := map[string]evalTestCase{ "HINCRBYFLOAT on a non-existing key and field": {