Skip to content

Commit

Permalink
DiceDB#654: feat: Implement HSETNX command (DiceDB#715)
Browse files Browse the repository at this point in the history
  • Loading branch information
shreyas23sk authored Sep 25, 2024
1 parent 4b30833 commit 1398403
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 1 deletion.
38 changes: 38 additions & 0 deletions integration_tests/commands/async/hsetnx_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package async

import (
"testing"

"gotest.tools/v3/assert"
)

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

testCases := []TestCase{
{
commands: []string{"HSETNX key_nx_t1 field value", "HSET key_nx_t1 field value_new"},
expected: []interface{}{ONE, ZERO},
},
{
commands: []string{"HSETNX key_nx_t2 field1 value1"},
expected: []interface{}{ONE},
},
{
commands: []string{"HSETNX key_nx_t3 field value", "HSETNX key_nx_t3 field new_value", "HSETNX key_nx_t3"},
expected: []interface{}{ONE, ZERO, "ERR wrong number of arguments for 'hsetnx' command"},
},
{
commands: []string{"SET key_nx_t4 v", "HSETNX key_nx_t4 f v"},
expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
},
}

for _, tc := range testCases {
for i, cmd := range tc.commands {
result := FireCommand(conn, cmd)
assert.DeepEqual(t, tc.expected[i], result)
}
}
}
10 changes: 10 additions & 0 deletions internal/eval/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,15 @@ var (
Arity: -4,
KeySpecs: KeySpecs{BeginIndex: 1},
}
hsetnxCmdMeta = DiceCmdMeta{
Name: "HSETNX",
Info: `Sets field in the hash stored at key to value, only if field does not yet exist.
If key does not exist, a new key holding a hash is created. If field already exists,
this operation has no effect.`,
Eval: evalHSETNX,
Arity: 4,
KeySpecs: KeySpecs{BeginIndex: 1},
}
hgetCmdMeta = DiceCmdMeta{
Name: "HGET",
Info: `Returns the value associated with field in the hash stored at key.`,
Expand Down Expand Up @@ -958,6 +967,7 @@ func init() {
DiceCmds["GETEX"] = getexCmdMeta
DiceCmds["PTTL"] = pttlCmdMeta
DiceCmds["HSET"] = hsetCmdMeta
DiceCmds["HSETNX"] = hsetnxCmdMeta
DiceCmds["OBJECT"] = objectCmdMeta
DiceCmds["TOUCH"] = touchCmdMeta
DiceCmds["LPUSH"] = lpushCmdMeta
Expand Down
20 changes: 20 additions & 0 deletions internal/eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -2903,6 +2903,26 @@ func evalHSET(args []string, store *dstore.Store) []byte {
return clientio.Encode(numKeys, false)
}

func evalHSETNX(args []string, store *dstore.Store) []byte {
if len(args) != 3 {
return diceerrors.NewErrArity("HSETNX")
}

key := args[0]
hmKey := args[1]

val, errWithMessage := getValueFromHashMap(key, hmKey, store)
if errWithMessage != nil {
return errWithMessage
}
if !bytes.Equal(val, clientio.RespNIL) { // hmKey is already present in hash map
return clientio.RespZero
}

evalHSET(args, store)
return clientio.RespOne
}

func evalHGETALL(args []string, store *dstore.Store) []byte {
if len(args) != 1 {
return diceerrors.NewErrArity("HGETALL")
Expand Down
65 changes: 64 additions & 1 deletion internal/eval/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"strings"
"testing"
"time"

"github.com/bytedance/sonic"
"github.com/dicedb/dice/internal/server/utils"
"github.com/ohler55/ojg/jp"
Expand Down Expand Up @@ -87,6 +87,7 @@ func TestEval(t *testing.T) {
testEvalCOMMAND(t, store)
testEvalJSONOBJKEYS(t, store)
testEvalGETRANGE(t, store)
testEvalHSETNX(t, store)
testEvalPING(t, store)
testEvalSETEX(t, store)
testEvalFLUSHDB(t, store)
Expand Down Expand Up @@ -3486,6 +3487,68 @@ func BenchmarkEvalGETRANGE(b *testing.B) {
}
}

func BenchmarkEvalHSETNX(b *testing.B) {
store := dstore.NewStore(nil)
for i := 0; i < b.N; i++ {
evalHSETNX([]string{"KEY", fmt.Sprintf("FIELD_%d", i/2), fmt.Sprintf("VALUE_%d", i)}, store)
}
}

func testEvalHSETNX(t *testing.T, store *dstore.Store) {
tests := map[string]evalTestCase{
"no args passed": {
setup: func() {},
input: nil,
output: []byte("-ERR wrong number of arguments for 'hsetnx' command\r\n"),
},
"only key passed": {
setup: func() {},
input: []string{"key"},
output: []byte("-ERR wrong number of arguments for 'hsetnx' command\r\n"),
},
"only key and field_name passed": {
setup: func() {},
input: []string{"KEY", "field_name"},
output: []byte("-ERR wrong number of arguments for 'hsetnx' command\r\n"),
},
"more than one field and value passed": {
setup: func() {},
input: []string{"KEY", "field1", "value1", "field2", "value2"},
output: []byte("-ERR wrong number of arguments for 'hsetnx' command\r\n"),
},
"key, field and value passed": {
setup: func() {},
input: []string{"KEY1", "field_name", "value"},
output: clientio.Encode(int64(1), false),
},
"new set of key, field and value added": {
setup: func() {},
input: []string{"KEY2", "field_name_new", "value_new_new"},
output: clientio.Encode(int64(1), false),
},
"apply with duplicate key, field and value names": {
setup: func() {
key := "KEY_MOCK"
field := "mock_field_name"
newMap := make(HashMap)
newMap[field] = "mock_field_value"

obj := &object.Obj{
TypeEncoding: object.ObjTypeHashMap | object.ObjEncodingHashMap,
Value: newMap,
LastAccessedAt: uint32(time.Now().Unix()),
}

store.Put(key, obj)
},
input: []string{"KEY_MOCK", "mock_field_name", "mock_field_value_2"},
output: clientio.Encode(int64(0), false),
},
}

runEvalTests(t, tests, evalHSETNX, store)
}

func TestMSETConsistency(t *testing.T) {
store := dstore.NewStore(nil)
evalMSET([]string{"KEY", "VAL", "KEY2", "VAL2"}, store)
Expand Down

0 comments on commit 1398403

Please sign in to comment.