feat(hash): support NOVALUES in HSCAN (#3109)

diff --git a/src/commands/cmd_hash.cc b/src/commands/cmd_hash.cc
index 13d13de..35e7c98 100644
--- a/src/commands/cmd_hash.cc
+++ b/src/commands/cmd_hash.cc
@@ -408,17 +408,23 @@
     std::vector<std::string> values;
     auto key_name = srv->GetKeyNameFromCursor(cursor_, CursorType::kTypeHash);
 
-    auto s = hash_db.Scan(ctx, key_, key_name, limit_, prefix_, &fields, &values);
+    auto s = hash_db.Scan(ctx, key_, key_name, limit_, prefix_, &fields, no_values_ ? nullptr : &values);
     if (!s.ok() && !s.IsNotFound()) {
       return {Status::RedisExecErr, s.ToString()};
     }
 
     auto cursor = GetNextCursor(srv, fields, CursorType::kTypeHash);
     std::vector<std::string> entries;
-    entries.reserve(2 * fields.size());
+    if (no_values_) {
+      entries.reserve(fields.size());
+    } else {
+      entries.reserve(2 * fields.size());
+    }
     for (size_t i = 0; i < fields.size(); i++) {
       entries.emplace_back(redis::BulkString(fields[i]));
-      entries.emplace_back(redis::BulkString(values[i]));
+      if (!no_values_) {
+        entries.emplace_back(redis::BulkString(values[i]));
+      }
     }
     *output = redis::Array({redis::BulkString(cursor), redis::Array(entries)});
     return Status::OK();
diff --git a/src/commands/scan_base.h b/src/commands/scan_base.h
index 5a0d4ca..d80c1e4 100644
--- a/src/commands/scan_base.h
+++ b/src/commands/scan_base.h
@@ -64,6 +64,8 @@
         } else {
           return {Status::RedisExecErr, "Invalid type"};
         }
+      } else if (parser.EatEqICase("novalues")) {
+        no_values_ = true;
       } else {
         return parser.InvalidSyntax();
       }
@@ -102,6 +104,7 @@
   std::string suffix_glob_ = "*";
   int limit_ = 20;
   RedisType type_ = kRedisNone;
+  bool no_values_ = false;
 };
 
 class CommandSubkeyScanBase : public CommandScanBase {
diff --git a/tests/gocase/unit/type/hash/hash_test.go b/tests/gocase/unit/type/hash/hash_test.go
index 8d15367..eb568b2 100644
--- a/tests/gocase/unit/type/hash/hash_test.go
+++ b/tests/gocase/unit/type/hash/hash_test.go
@@ -775,6 +775,23 @@
 		require.Equal(t, "b", rdb.HGet(ctx, "hash", strings.Repeat("k", 336)).Val())
 	})
 
+	t.Run("HSCAN without NOVALUES", func(t *testing.T) {
+		rdb.Del(ctx, "langhash")
+		rdb.HMSet(ctx, "langhash", []string{"lang1", "C++", "lang2", "JavaScript", "lang3", "Python", "lang4", "GoLanguage"})
+		res, _, _ := rdb.HScan(ctx, "langhash", 0, "lang1", 0).Result()
+		require.Equal(t, 2, len(res))
+		require.Equal(t, "lang1", res[0])
+		require.Equal(t, "C++", res[1])
+	})
+
+	t.Run("HSCAN with NOVALUES", func(t *testing.T) {
+		rdb.Del(ctx, "langhash")
+		rdb.HMSet(ctx, "langhash", []string{"lang1", "C++", "lang2", "JavaScript", "lang3", "Python", "lang4", "GoLanguage"})
+		res, _, _ := rdb.HScanNoValues(ctx, "langhash", 0, "lang1", 0).Result()
+		require.Equal(t, 1, len(res))
+		require.Equal(t, "lang1", res[0])
+	})
+
 	for _, size := range []int64{10, 512} {
 		t.Run(fmt.Sprintf("Hash fuzzing #1 - %d fields", size), func(t *testing.T) {
 			for times := 0; times < 10; times++ {