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

#648: feat: adding HINCRBYFLOAT command implementation #853

Merged
merged 1 commit into from
Sep 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions internal/eval/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,18 @@ var (
Arity: -4,
KeySpecs: KeySpecs{BeginIndex: 1},
}
hincrbyFloatCmdMeta = DiceCmdMeta{
Name: "HINCRBYFLOAT",
Info: `HINCRBYFLOAT increments the specified field of a hash stored at the key,
and representing a floating point number, by the specified increment.
If the field does not exist, it is set to 0 before performing the operation.
If the field contains a value of wrong type or specified increment
is not parsable as floating point number, then an error occurs.
`,
Eval: evalHINCRBYFLOAT,
Arity: -4,
KeySpecs: KeySpecs{BeginIndex: 1},
}
)

func init() {
Expand Down Expand Up @@ -1054,6 +1066,7 @@ func init() {
DiceCmds["HVALS"] = hValsCmdMeta
DiceCmds["ZADD"] = zaddCmdMeta
DiceCmds["ZRANGE"] = zrangeCmdMeta
DiceCmds["HINCRBYFLOAT"] = hincrbyFloatCmdMeta
}

// Function to convert DiceCmdMeta to []interface{}
Expand Down
37 changes: 37 additions & 0 deletions internal/eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -4400,3 +4400,40 @@ func evalZRANGE(args []string, store *dstore.Store) []byte {

return clientio.Encode(result, false)
}

func evalHINCRBYFLOAT(args []string, store *dstore.Store) []byte {
if len(args) < 3 {
return diceerrors.NewErrArity("HINCRBYFLOAT")
}
incr, err := strconv.ParseFloat(strings.TrimSpace(args[2]), 64)

if err != nil {
return diceerrors.NewErrWithMessage(diceerrors.IntOrFloatErr)
}

key := args[0]
obj := store.Get(key)
var hashmap HashMap

if obj != nil {
if err := object.AssertTypeAndEncoding(obj.TypeEncoding, object.ObjTypeHashMap, object.ObjEncodingHashMap); err != nil {
return diceerrors.NewErrWithMessage(diceerrors.WrongTypeErr)
}
hashmap = obj.Value.(HashMap)
}

if hashmap == nil {
hashmap = make(HashMap)
}

field := args[1]
numkey, err := hashmap.incrementFloatValue(field, incr)
if err != nil {
return diceerrors.NewErrWithMessage(err.Error())
}

obj = store.NewObj(hashmap, -1, object.ObjTypeHashMap, object.ObjEncodingHashMap)
store.Put(key, obj)

return clientio.Encode(numkey, false)
}
172 changes: 172 additions & 0 deletions internal/eval/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ func TestEval(t *testing.T) {
testEvalZADD(t, store)
testEvalZRANGE(t, store)
testEvalHVALS(t, store)
testEvalHINCRBYFLOAT(t, store)
}

func testEvalPING(t *testing.T, store *dstore.Store) {
Expand Down Expand Up @@ -4426,3 +4427,174 @@ func testEvalZRANGE(t *testing.T, store *dstore.Store) {

runEvalTests(t, tests, evalZRANGE, store)
}

func testEvalHINCRBYFLOAT(t *testing.T, store *dstore.Store) {
tests := map[string]evalTestCase{
"HINCRBYFLOAT on a non-existing key and field": {
setup: func() {},
input: []string{"key", "field", "0.1"},
output: clientio.Encode("0.1", false),
},
"HINCRBYFLOAT on an existing key and non-existing field": {
setup: func() {
key := "key"
h := make(HashMap)
obj := &object.Obj{
TypeEncoding: object.ObjTypeHashMap | object.ObjEncodingHashMap,
Value: h,
LastAccessedAt: uint32(time.Now().Unix()),
}
store.Put(key, obj)
},
input: []string{"key", "field", "0.1"},
output: clientio.Encode("0.1", false),
},
"HINCRBYFLOAT on an existing key and field with a float value": {
setup: func() {
key := "key"
field := "field"
h := make(HashMap)
h[field] = "2.1"
obj := &object.Obj{
TypeEncoding: object.ObjTypeHashMap | object.ObjEncodingHashMap,
Value: h,
LastAccessedAt: uint32(time.Now().Unix()),
}
store.Put(key, obj)
},
input: []string{"key", "field", "0.1"},
output: clientio.Encode("2.2", false),
},
"HINCRBYFLOAT on an existing key and field with an integer value": {
setup: func() {
key := "key"
field := "field"
h := make(HashMap)
h[field] = "2"
obj := &object.Obj{
TypeEncoding: object.ObjTypeHashMap | object.ObjEncodingHashMap,
Value: h,
LastAccessedAt: uint32(time.Now().Unix()),
}
store.Put(key, obj)
},
input: []string{"key", "field", "0.1"},
output: clientio.Encode("2.1", false),
},
"HINCRBYFLOAT with a negative increment": {
setup: func() {
key := "key"
field := "field"
h := make(HashMap)
h[field] = "2.0"
obj := &object.Obj{
TypeEncoding: object.ObjTypeHashMap | object.ObjEncodingHashMap,
Value: h,
LastAccessedAt: uint32(time.Now().Unix()),
}
store.Put(key, obj)
},
input: []string{"key", "field", "-0.1"},
output: clientio.Encode("1.9", false),
},
// this is failing
"HINCRBYFLOAT by a non-numeric increment": {
setup: func() {
key := "key"
field := "field"
h := make(HashMap)
h[field] = "2.0"
obj := &object.Obj{
TypeEncoding: object.ObjTypeHashMap | object.ObjEncodingHashMap,
Value: h,
LastAccessedAt: uint32(time.Now().Unix()),
}
store.Put(key, obj)
},
input: []string{"key", "field", "a"},
output: []byte("-ERR value is not an integer or a float\r\n"),
},
// this is failing
"HINCRBYFLOAT on a field with non-numeric value": {
setup: func() {
key := "key"
field := "field"
h := make(HashMap)
h[field] = "non_numeric"
obj := &object.Obj{
TypeEncoding: object.ObjTypeHashMap | object.ObjEncodingHashMap,
Value: h,
LastAccessedAt: uint32(time.Now().Unix()),
}
store.Put(key, obj)
},
input: []string{"key", "field", "0.1"},
output: []byte("-ERR value is not an integer or a float\r\n"),
},
// this failing
"HINCRBYFLOAT by a value that would turn float64 to Inf": {
setup: func() {
key := "key"
field := "field"
h := make(HashMap)
h[field] = "1e308"
obj := &object.Obj{
TypeEncoding: object.ObjTypeHashMap | object.ObjEncodingHashMap,
Value: h,
LastAccessedAt: uint32(time.Now().Unix()),
}
store.Put(key, obj)
},
input: []string{"key", "field", "1e308"},
output: []byte("-ERR increment or decrement would overflow\r\n"),
},
"HINCRBYFLOAT with scientific notation": {
setup: func() {
key := "key"
field := "field"
h := make(HashMap)
h[field] = "1e2"
obj := &object.Obj{
TypeEncoding: object.ObjTypeHashMap | object.ObjEncodingHashMap,
Value: h,
LastAccessedAt: uint32(time.Now().Unix()),
}
store.Put(key, obj)
},
input: []string{"key", "field", "1e-1"},
output: clientio.Encode("100.1", false),
},
}

runEvalTests(t, tests, evalHINCRBYFLOAT, store)
}

func BenchmarkEvalHINCRBYFLOAT(b *testing.B) {
store := dstore.NewStore(nil)

// Setting initial fields with some values
store.Put("key1", store.NewObj(HashMap{"field1": "1.0", "field2": "1.2"}, maxExDuration, object.ObjTypeHashMap, object.ObjEncodingHashMap))
store.Put("key2", store.NewObj(HashMap{"field1": "0.1"}, maxExDuration, object.ObjTypeHashMap, object.ObjEncodingHashMap))

inputs := []struct {
key string
field string
incr string
}{
{"key1", "field1", "0.1"},
{"key1", "field1", "-0.1"},
{"key1", "field2", "1000000.1"},
{"key1", "field2", "-1000000.1"},
{"key2", "field1", "-10.1234"},
{"key3", "field1", "1.5"}, // testing with non-existing key
{"key2", "field2", "2.75"}, // testing with non-existing field in existing key
}

for _, input := range inputs {
b.Run(fmt.Sprintf("HINCRBYFLOAT %s %s %s", input.key, input.field, input.incr), func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = evalHINCRBYFLOAT([]string{"HINCRBYFLOAT", input.key, input.field, input.incr}, store)
}
})
}
}
24 changes: 24 additions & 0 deletions internal/eval/hmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,27 @@ func (h HashMap) incrementValue(field string, increment int64) (int64, error) {

return total, nil
}

func (h HashMap) incrementFloatValue(field string, incr float64) (string, error) {
val, ok := h[field]
if !ok {
h[field] = fmt.Sprintf("%v", incr)
strValue := formatFloat(incr, false)
return strValue, nil
}

i, err := strconv.ParseFloat(val, 64)
if err != nil {
return "-1", diceerrors.NewErr(diceerrors.IntOrFloatErr)
}

if (i > 0 && incr > 0 && i > math.MaxFloat64-incr) || (i < 0 && incr < 0 && i < -math.MaxFloat64-incr) {
return "-1", diceerrors.NewErr(diceerrors.IncrDecrOverflowErr)
}

total := i + incr
strValue := formatFloat(total, false)
h[field] = fmt.Sprintf("%v", total)

return strValue, nil
}