diff --git a/integration_tests/commands/async/hsetnx_test.go b/integration_tests/commands/async/hsetnx_test.go new file mode 100644 index 000000000..9d77bebc4 --- /dev/null +++ b/integration_tests/commands/async/hsetnx_test.go @@ -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) + } + } +} diff --git a/internal/eval/commands.go b/internal/eval/commands.go index 085809efa..bced2b26d 100644 --- a/internal/eval/commands.go +++ b/internal/eval/commands.go @@ -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.`, @@ -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 diff --git a/internal/eval/eval.go b/internal/eval/eval.go index 7f5d1f331..dbaec79c6 100644 --- a/internal/eval/eval.go +++ b/internal/eval/eval.go @@ -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") diff --git a/internal/eval/eval_test.go b/internal/eval/eval_test.go index c66426afd..5b0f33363 100644 --- a/internal/eval/eval_test.go +++ b/internal/eval/eval_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" "time" - + "github.com/bytedance/sonic" "github.com/dicedb/dice/internal/server/utils" "github.com/ohler55/ojg/jp" @@ -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) @@ -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)