This is an automated email from the ASF dual-hosted git repository.
hulk pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks.git
The following commit(s) were added to refs/heads/unstable by this push:
new 0a2dcee8 Add the support of the HRANDFIELD command (#1565)
0a2dcee8 is described below
commit 0a2dcee8bedfa6df23e5d78e002065a717bc1617
Author: Zevin <[email protected]>
AuthorDate: Sun Jul 16 21:37:52 2023 +0800
Add the support of the HRANDFIELD command (#1565)
---
src/commands/cmd_hash.cc | 53 ++++++++++++++++++++++++++++++-
src/types/redis_hash.cc | 54 ++++++++++++++++++++++++++++++++
src/types/redis_hash.h | 2 ++
tests/cppunit/types/hash_test.cc | 31 ++++++++++++++++++
tests/gocase/unit/type/hash/hash_test.go | 49 +++++++++++++++++++++++++++++
5 files changed, 188 insertions(+), 1 deletion(-)
diff --git a/src/commands/cmd_hash.cc b/src/commands/cmd_hash.cc
index c6e2ddf6..1e0947e7 100644
--- a/src/commands/cmd_hash.cc
+++ b/src/commands/cmd_hash.cc
@@ -377,6 +377,56 @@ class CommandHScan : public CommandSubkeyScanBase {
}
};
+class CommandHRandField : public Commander {
+ public:
+ Status Parse(const std::vector<std::string> &args) override {
+ if (args.size() >= 3) {
+ no_parameters_ = false;
+ auto parse_result = ParseInt<int64_t>(args[2], 10);
+ if (!parse_result) {
+ return {Status::RedisParseErr, errValueNotInteger};
+ }
+ command_count_ = *parse_result;
+
+ if (args.size() > 4 || (args.size() == 4 && !util::EqualICase(args[3],
"withvalues"))) {
+ return {Status::RedisParseErr, errInvalidSyntax};
+ } else if (args.size() == 4) {
+ withvalues_ = true;
+ }
+ }
+ return Commander::Parse(args);
+ }
+
+ Status Execute(Server *svr, Connection *conn, std::string *output) override {
+ redis::Hash hash_db(svr->storage, conn->GetNamespace());
+ std::vector<FieldValue> field_values;
+
+ auto s = hash_db.RandField(args_[1], command_count_, &field_values,
+ withvalues_ ? HashFetchType::kAll :
HashFetchType::kOnlyKey);
+ if (!s.ok() && !s.IsNotFound()) {
+ return {Status::RedisExecErr, s.ToString()};
+ }
+
+ std::vector<std::string> result_entries;
+ result_entries.reserve(field_values.size());
+ for (const auto &p : field_values) {
+ result_entries.emplace_back(p.field);
+ if (withvalues_) result_entries.emplace_back(p.value);
+ }
+
+ if (no_parameters_)
+ *output = s.IsNotFound() ? redis::NilString() :
redis::BulkString(result_entries[0]);
+ else
+ *output = redis::MultiBulkString(result_entries, false);
+ return Status::OK();
+ }
+
+ private:
+ bool withvalues_ = false;
+ int64_t command_count_ = 1;
+ bool no_parameters_ = true;
+};
+
REDIS_REGISTER_COMMANDS(MakeCmdAttr<CommandHGet>("hget", 3, "read-only", 1, 1,
1),
MakeCmdAttr<CommandHIncrBy>("hincrby", 4, "write", 1,
1, 1),
MakeCmdAttr<CommandHIncrByFloat>("hincrbyfloat", 4,
"write", 1, 1, 1),
@@ -392,6 +442,7 @@ REDIS_REGISTER_COMMANDS(MakeCmdAttr<CommandHGet>("hget", 3,
"read-only", 1, 1, 1
MakeCmdAttr<CommandHVals>("hvals", 2, "read-only", 1,
1, 1),
MakeCmdAttr<CommandHGetAll>("hgetall", 2, "read-only",
1, 1, 1),
MakeCmdAttr<CommandHScan>("hscan", -3, "read-only", 1,
1, 1),
- MakeCmdAttr<CommandHRangeByLex>("hrangebylex", -4,
"read-only", 1, 1, 1), )
+ MakeCmdAttr<CommandHRangeByLex>("hrangebylex", -4,
"read-only", 1, 1, 1),
+ MakeCmdAttr<CommandHRandField>("hrandfield", -2,
"read-only", 1, 1, 1), )
} // namespace redis
diff --git a/src/types/redis_hash.cc b/src/types/redis_hash.cc
index 2946f47e..b61f8219 100644
--- a/src/types/redis_hash.cc
+++ b/src/types/redis_hash.cc
@@ -25,6 +25,7 @@
#include <algorithm>
#include <cctype>
#include <cmath>
+#include <random>
#include <utility>
#include "db_util.h"
@@ -384,4 +385,57 @@ rocksdb::Status Hash::Scan(const Slice &user_key, const
std::string &cursor, uin
return SubKeyScanner::Scan(kRedisHash, user_key, cursor, limit,
field_prefix, fields, values);
}
+rocksdb::Status Hash::RandField(const Slice &user_key, int64_t command_count,
std::vector<FieldValue> *field_values,
+ HashFetchType type) {
+ uint64_t count = (command_count >= 0) ? static_cast<uint64_t>(command_count)
: static_cast<uint64_t>(-command_count);
+ bool unique = (command_count >= 0);
+
+ std::string ns_key;
+ AppendNamespacePrefix(user_key, &ns_key);
+ HashMetadata metadata(/*generate_version=*/false);
+ rocksdb::Status s = GetMetadata(ns_key, &metadata);
+ if (!s.ok()) return s;
+
+ uint64_t size = metadata.size;
+ std::vector<FieldValue> samples;
+ // TODO: Getting all values in Hash might be heavy, consider lazy-loading
these values later
+ if (count == 0) return rocksdb::Status::OK();
+ s = GetAll(user_key, &samples, type);
+ if (!s.ok()) return s;
+ auto append_field_with_index = [field_values, &samples, type](uint64_t
index) {
+ if (type == HashFetchType::kAll) {
+ field_values->emplace_back(samples[index].field, samples[index].value);
+ } else {
+ field_values->emplace_back(samples[index].field, "");
+ }
+ };
+ field_values->reserve(std::min(size, count));
+ if (!unique || count == 1) {
+ // Case 1: Negative count, randomly select elements or without parameter
+ std::random_device rd;
+ std::mt19937 gen(rd());
+ std::uniform_int_distribution<uint64_t> dis(0, size - 1);
+ for (uint64_t i = 0; i < count; i++) {
+ uint64_t index = dis(gen);
+ append_field_with_index(index);
+ }
+ } else if (size <= count) {
+ // Case 2: Requested count is greater than or equal to the number of
elements inside the hash
+ for (uint64_t i = 0; i < size; i++) {
+ append_field_with_index(i);
+ }
+ } else {
+ // Case 3: Requested count is less than the number of elements inside the
hash
+ std::vector<uint64_t> indices(size);
+ std::iota(indices.begin(), indices.end(), 0);
+ std::shuffle(indices.begin(), indices.end(),
+ std::random_device{}); // use Fisher-Yates shuffle algorithm
to randomize the order
+ for (uint64_t i = 0; i < count; i++) {
+ uint64_t index = indices[i];
+ append_field_with_index(index);
+ }
+ }
+ return rocksdb::Status::OK();
+}
+
} // namespace redis
diff --git a/src/types/redis_hash.h b/src/types/redis_hash.h
index ee175834..fc004ed8 100644
--- a/src/types/redis_hash.h
+++ b/src/types/redis_hash.h
@@ -61,6 +61,8 @@ class Hash : public SubKeyScanner {
rocksdb::Status Scan(const Slice &user_key, const std::string &cursor,
uint64_t limit,
const std::string &field_prefix,
std::vector<std::string> *fields,
std::vector<std::string> *values = nullptr);
+ rocksdb::Status RandField(const Slice &user_key, int64_t command_count,
std::vector<FieldValue> *field_values,
+ HashFetchType type = HashFetchType::kOnlyKey);
private:
rocksdb::Status GetMetadata(const Slice &ns_key, HashMetadata *metadata);
diff --git a/tests/cppunit/types/hash_test.cc b/tests/cppunit/types/hash_test.cc
index 61745029..216acc72 100644
--- a/tests/cppunit/types/hash_test.cc
+++ b/tests/cppunit/types/hash_test.cc
@@ -331,3 +331,34 @@ TEST_F(RedisHashTest, HRangeByLexNonExistingKey) {
EXPECT_TRUE(s.ok());
EXPECT_EQ(result.size(), 0);
}
+
+TEST_F(RedisHashTest, HRandField) {
+ uint64_t ret = 0;
+ for (size_t i = 0; i < fields_.size(); i++) {
+ auto s = hash_->Set(key_, fields_[i], values_[i], &ret);
+ EXPECT_TRUE(s.ok() && ret == 1);
+ }
+ auto size = static_cast<int64_t>(fields_.size());
+ std::vector<FieldValue> fvs;
+ // Case 1: Negative count, randomly select elements
+ fvs.clear();
+ auto s = hash_->RandField(key_, -(size + 10), &fvs);
+ EXPECT_TRUE(s.ok() && fvs.size() == (fields_.size() + 10));
+
+ // Case 2: Requested count is greater than or equal to the number of
elements inside the hash
+ fvs.clear();
+ s = hash_->RandField(key_, size + 1, &fvs);
+ EXPECT_TRUE(s.ok() && fvs.size() == fields_.size());
+
+ // Case 3: Requested count is less than the number of elements inside the
hash
+ fvs.clear();
+ s = hash_->RandField(key_, size - 1, &fvs);
+ EXPECT_TRUE(s.ok() && fvs.size() == fields_.size() - 1);
+
+ // hrandfield key 0
+ fvs.clear();
+ s = hash_->RandField(key_, 0, &fvs);
+ EXPECT_TRUE(s.ok() && fvs.size() == 0);
+
+ hash_->Del(key_);
+}
diff --git a/tests/gocase/unit/type/hash/hash_test.go
b/tests/gocase/unit/type/hash/hash_test.go
index 84895273..bf93d268 100644
--- a/tests/gocase/unit/type/hash/hash_test.go
+++ b/tests/gocase/unit/type/hash/hash_test.go
@@ -783,6 +783,55 @@ func TestHash(t *testing.T) {
require.Equal(t, []interface{}{"field1", "some-value",
"field2", ""}, rdb.Do(ctx, "HrangeByLex", testKey, "[a", "[z").Val())
})
+
+ t.Run("HRandField count is positive", func(t *testing.T) {
+ testKey := "test-hash-1"
+ require.NoError(t, rdb.Del(ctx, testKey).Err())
+ require.NoError(t, rdb.HSet(ctx, testKey, "key1",
"value1", "key2", "value2", "key3", "value3").Err())
+ result, err := rdb.HRandField(ctx, testKey, 5).Result()
+ require.NoError(t, err)
+ require.Len(t, result, 3)
+ require.Equal(t, []string{"key1", "key2", "key3"},
result)
+ result, err = rdb.HRandField(ctx, testKey, 2).Result()
+ require.NoError(t, err)
+ require.Len(t, result, 2)
+ require.Contains(t, []string{"key1", "key2", "key3"},
result[0])
+ require.Contains(t, []string{"key1", "key2", "key3"},
result[1])
+ result, err = rdb.HRandField(ctx, testKey, 0).Result()
+ require.NoError(t, err)
+ require.Len(t, result, 0)
+ result, err = rdb.HRandField(ctx, "nonexistent-key",
1).Result()
+ require.NoError(t, err)
+ require.Len(t, result, 0)
+ var rv [][]interface{}
+ resultWithValues, err := rdb.HRandFieldWithValues(ctx,
testKey, 5).Result()
+ require.NoError(t, err)
+ require.Len(t, resultWithValues, 3)
+ for _, kv := range resultWithValues {
+ keys := []interface{}{kv.Key, kv.Value}
+ rv = append(rv, keys)
+ }
+ require.Equal(t, [][]interface{}{
+ {"key1", "value1"},
+ {"key2", "value2"},
+ {"key3", "value3"},
+ }, rv)
+ // TODO: Add test to verify randomness of the selected
random fields
+ })
+
+ t.Run("HRandField count is negative", func(t *testing.T) {
+ testKey := "test-hash-1"
+ require.NoError(t, rdb.Del(ctx, testKey).Err())
+ require.NoError(t, rdb.HSet(ctx, testKey, "key1",
"value1", "key2", "value2", "key3", "value3").Err())
+ result, err := rdb.HRandField(ctx, testKey, -4).Result()
+ require.NoError(t, err)
+ require.Len(t, result, 4)
+ resultWithValues, err := rdb.HRandFieldWithValues(ctx,
testKey, -12).Result()
+ require.NoError(t, err)
+ require.Len(t, resultWithValues, 12)
+ // TODO: Add test to verify randomness of the selected
random fields
+ })
+
}
}