Repository: DiceDB/dice (Go)

Issue:
Add support for command `HMGET`
Add support for the `HMGET` command in DiceDB similar to the [`HMGET` command in Redis](https://redis.io/docs/latest/commands/command-list/). Please refer to the following commit in Redis to understand the implementation specifics - [source](https://github.com/redis/redis/tree/f60370ce28b946c1146dcea77c9c399d39601aaa).

Write unit and integration tests for the command referring to the tests written in the [Redis codebase 7.2.5](https://github.com/redis/redis/tree/f60370ce28b946c1146dcea77c9c399d39601aaa). For integration tests, you can refer to the `tests` folder. Note: they have used `TCL` for the test suite, and we need to port that to our way of writing integration tests using the relevant helper methods. Please refer to our [tests directory](https://github.com/DiceDB/dice/tree/master/tests).

For the command, benchmark the code and measure the time taken and memory allocs using [`benchmem`](https://blog.logrocket.com/benchmarking-golang-improve-function-performance/) and try to keep them to the bare minimum.



Discussion:
Hi @arpitbbhayani , I'd like to pick this up. 
@vishnuchandrashekar assigned, thanks for contributing.
Hello @vishnuchandrashekar,

There has been no activity on this issue for the past 5 days.
It would be awesome if you keep posting updates to this issue so that we know you are actively working on it.

We are really eager to close this issue at the earliest, hence if we continue to see the inactivity, we will have to reassign the issue to someone else. We are doing this to ensure that the project maintains its momentum and others are not blocked on this work.

Just drop a comment with the current status of the work or share any issues you are facing. We can always chip in to help you out.

Thanks again.




Patch:
diff --git a/internal/eval/commands.go b/internal/eval/commands.go
index 5b3e37e8c..e09e1a1ad 100644
--- a/internal/eval/commands.go
+++ b/internal/eval/commands.go
@@ -618,6 +618,13 @@ var (
 		Arity:    -3,
 		KeySpecs: KeySpecs{BeginIndex: 1},
 	}
+	hmgetCmdMeta = DiceCmdMeta{
+		Name:     "HMGET",
+		Info:     `Returns the values associated with the specified fields in the hash stored at key.`,
+		Eval:     evalHMGET,
+		Arity:    -2,
+		KeySpecs: KeySpecs{BeginIndex: 1},
+	}
 	hgetAllCmdMeta = DiceCmdMeta{
 		Name: "HGETALL",
 		Info: `Returns all fields and values of the hash stored at key. In the returned value,
@@ -1015,6 +1022,7 @@ func init() {
 	DiceCmds["PFADD"] = pfAddCmdMeta
 	DiceCmds["PFCOUNT"] = pfCountCmdMeta
 	DiceCmds["HGET"] = hgetCmdMeta
+	DiceCmds["HMGET"] = hmgetCmdMeta
 	DiceCmds["HSTRLEN"] = hstrLenCmdMeta
 	DiceCmds["PFMERGE"] = pfMergeCmdMeta
 	DiceCmds["JSON.STRLEN"] = jsonStrlenCmdMeta
diff --git a/internal/eval/eval.go b/internal/eval/eval.go
index a6db0cefb..1b005451b 100644
--- a/internal/eval/eval.go
+++ b/internal/eval/eval.go
@@ -3035,6 +3035,40 @@ func evalHGET(args []string, store *dstore.Store) []byte {
 	return val
 }
 
+// evalHMGET returns an array of values associated with the given fields,
+// in the same order as they are requested.
+// If a field does not exist, returns a corresponding nil value in the array.
+// If the key does not exist, returns an array of nil values. 
+func evalHMGET(args []string, store *dstore.Store) []byte {
+	if len(args) < 2 {
+		return diceerrors.NewErrArity("HMGET")
+	}
+	key := args[0]
+
+	obj := store.Get(key)
+
+	results := make([]interface{}, len(args[1:]))
+	if obj == nil {
+		return clientio.Encode(results, false)
+	}
+	if err := object.AssertTypeAndEncoding(obj.TypeEncoding, object.ObjTypeHashMap, object.ObjEncodingHashMap); err != nil {
+		return diceerrors.NewErrWithMessage(diceerrors.WrongTypeErr)
+	}
+
+	hashMap := obj.Value.(HashMap)
+
+	for i, hmKey := range args[1:] {
+		hmValue, ok := hashMap.Get(hmKey)
+		if ok {
+			results[i] = *hmValue
+		} else {
+			results[i] = clientio.RespNIL
+		}
+	}
+
+	return clientio.Encode(results, false)
+}
+
 func evalHDEL(args []string, store *dstore.Store) []byte {
 	if len(args) < 2 {
 		return diceerrors.NewErrArity("HDEL")


Test patch:
diff --git a/integration_tests/commands/async/hmget_test.go b/integration_tests/commands/async/hmget_test.go
new file mode 100644
index 000000000..72cb9804d
--- /dev/null
+++ b/integration_tests/commands/async/hmget_test.go
@@ -0,0 +1,57 @@
+package async
+
+import (
+	"testing"
+
+	"gotest.tools/v3/assert"
+)
+
+func TestHMGET(t *testing.T) {
+	conn := getLocalConnection()
+	defer conn.Close()
+	defer FireCommand(conn, "DEL key_hmGet key_hmGet1")
+
+	testCases := []TestCase{
+		{
+			name:     "hmget existing keys and fields",
+			commands: []string{"HSET key_hmGet field value", "HSET key_hmGet field2 value_new", "HMGET key_hmGet field field2"},
+			expected: []interface{}{ONE, ONE, []interface{}{"value", "value_new"}},
+		},
+		{
+			name:     "hmget key does not exist",
+			commands: []string{"HMGET doesntexist field"},
+			expected: []interface{}{[]interface{}{"(nil)"}},
+		},
+		{
+			name:     "hmget field does not exist",
+			commands: []string{"HMGET key_hmGet field3"},
+			expected: []interface{}{[]interface{}{"(nil)"}},
+		},
+		{
+			name:     "hmget some fields do not exist",
+			commands: []string{"HMGET key_hmGet field field2 field3 field3"},
+			expected: []interface{}{[]interface{}{"value", "value_new", "(nil)", "(nil)"}},
+		},
+		{
+			name:     "hmget with wrongtype",
+			commands: []string{"SET key_hmGet1 field", "HMGET key_hmGet1 field"},
+			expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
+		},
+		{
+			name:     "wrong number of arguments",
+			commands: []string{"HMGET key_hmGet", "HMGET"},
+			expected: []interface{}{"ERR wrong number of arguments for 'hmget' command",
+				"ERR wrong number of arguments for 'hmget' command"},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			for i, cmd := range tc.commands {
+				// Fire the command and get the result
+				result := FireCommand(conn, cmd)
+				assert.DeepEqual(t, result, tc.expected[i])
+			}
+		})
+	}
+}
diff --git a/internal/eval/eval_test.go b/internal/eval/eval_test.go
index 01c9e12d3..caa6985b5 100644
--- a/internal/eval/eval_test.go
+++ b/internal/eval/eval_test.go
@@ -73,6 +73,7 @@ func TestEval(t *testing.T) {
 	testEvalPFADD(t, store)
 	testEvalPFCOUNT(t, store)
 	testEvalHGET(t, store)
+	testEvalHMGET(t, store)
 	testEvalHSTRLEN(t, store)
 	testEvalHDEL(t, store)
 	testEvalPFMERGE(t, store)
@@ -2005,6 +2006,82 @@ func testEvalHGET(t *testing.T, store *dstore.Store) {
 	runEvalTests(t, tests, evalHGET, store)
 }
 
+func testEvalHMGET(t *testing.T, store *dstore.Store) {
+	tests := map[string]evalTestCase{
+		"wrong number of args passed": {
+			setup:  func() {},
+			input:  nil,
+			output: []byte("-ERR wrong number of arguments for 'hmget' command\r\n"),
+		},
+		"only key passed": {
+			setup:  func() {},
+			input:  []string{"KEY"},
+			output: []byte("-ERR wrong number of arguments for 'hmget' command\r\n"),
+		},
+		"key doesn't exists": {
+			setup:  func() {},
+			input:  []string{"KEY", "field_name"},
+			output: clientio.Encode([]interface{}{nil}, false),
+		},
+		"key exists but field_name doesn't exists": {
+			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", "non_existent_key"},
+			output: clientio.Encode([]interface{}{nil}, false),
+		},
+		"both key and field_name exists": {
+			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"},
+			output: clientio.Encode([]interface{}{"mock_field_value"}, false),
+		},
+		"some fields exist some do not": {
+			setup: func() {
+				key := "KEY_MOCK"
+				newMap := HashMap{
+					"field1": "value1",
+					"field2": "value2",
+				}
+				obj := &object.Obj{
+					TypeEncoding:   object.ObjTypeHashMap | object.ObjEncodingHashMap,
+					Value:          newMap,
+					LastAccessedAt: uint32(time.Now().Unix()),
+				}
+
+				store.Put(key, obj)
+			},
+			input:  []string{"KEY_MOCK", "field1", "field2", "field3", "field4"},
+			output: clientio.Encode([]interface{}{"value1", "value2", nil, nil}, false),
+		},
+	}
+
+	runEvalTests(t, tests, evalHMGET, store)
+}
+
 func testEvalHVALS(t *testing.T, store *dstore.Store) {
 	tests := map[string]evalTestCase{
 		"wrong number of args passed": {