This is an automated email from the ASF dual-hosted git repository.
twice 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 74084cc0d feat(cmd): add new command KMETADATA (#2827)
74084cc0d is described below
commit 74084cc0dfb8abdc155183e9d60d6c3bf068d31b
Author: Luigi Tagliamonte <[email protected]>
AuthorDate: Fri Mar 21 23:47:42 2025 -0700
feat(cmd): add new command KMETADATA (#2827)
Co-authored-by: Aleks Lozovyuk <[email protected]>
Co-authored-by: Twice <[email protected]>
Co-authored-by: hulk <[email protected]>
---
src/commands/cmd_key.cc | 29 ++-
tests/gocase/unit/kmetadata/kmetadata_test.go | 248 ++++++++++++++++++++++++++
2 files changed, 276 insertions(+), 1 deletion(-)
diff --git a/src/commands/cmd_key.cc b/src/commands/cmd_key.cc
index 827f6f52e..093e0cc2b 100644
--- a/src/commands/cmd_key.cc
+++ b/src/commands/cmd_key.cc
@@ -553,6 +553,32 @@ class CommandSort : public Commander {
SortArgument sort_argument_;
};
+class CommandKMetadata : public Commander {
+ public:
+ Status Execute(engine::Context &ctx, Server *srv, Connection *conn,
std::string *output) override {
+ redis::Database redis(srv->storage, conn->GetNamespace());
+ std::string &key = args_[1];
+ std::string nskey = redis.AppendNamespacePrefix(key);
+
+ Metadata metadata(kRedisNone, false);
+ auto s = redis.GetMetadata(ctx, RedisTypes::All(), nskey, &metadata);
+ if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
+
+ if (metadata.IsSingleKVType()) {
+ *output = conn->Map({{redis::BulkString("type"),
redis::BulkString(metadata.TypeName())},
+ {redis::BulkString("expire"),
redis::Integer(metadata.expire)},
+ {redis::BulkString("flags"),
redis::Integer(metadata.flags)}});
+ } else {
+ *output = conn->Map({{redis::BulkString("type"),
redis::BulkString(metadata.TypeName())},
+ {redis::BulkString("size"),
redis::Integer(metadata.size)},
+ {redis::BulkString("expire"),
redis::Integer(metadata.expire)},
+ {redis::BulkString("flags"),
redis::Integer(metadata.flags)},
+ {redis::BulkString("version"),
redis::Integer(metadata.version)}});
+ }
+ return Status::OK();
+ }
+};
+
REDIS_REGISTER_COMMANDS(Key, MakeCmdAttr<CommandTTL>("ttl", 2, "read-only", 1,
1, 1),
MakeCmdAttr<CommandPTTL>("pttl", 2, "read-only", 1, 1,
1),
MakeCmdAttr<CommandType>("type", 2, "read-only", 1, 1,
1),
@@ -573,6 +599,7 @@ REDIS_REGISTER_COMMANDS(Key, MakeCmdAttr<CommandTTL>("ttl",
2, "read-only", 1, 1
MakeCmdAttr<CommandRenameNX>("renamenx", 3, "write",
1, 2, 1),
MakeCmdAttr<CommandCopy>("copy", -3, "write", 1, 2, 1),
MakeCmdAttr<CommandSort<false>>("sort", -2, "write
slow", 1, 1, 1),
- MakeCmdAttr<CommandSort<true>>("sort_ro", -2,
"read-only slow", 1, 1, 1))
+ MakeCmdAttr<CommandSort<true>>("sort_ro", -2,
"read-only slow", 1, 1, 1),
+ MakeCmdAttr<CommandKMetadata>("kmetadata", 2,
"read-only", 1, 1, 1))
} // namespace redis
diff --git a/tests/gocase/unit/kmetadata/kmetadata_test.go
b/tests/gocase/unit/kmetadata/kmetadata_test.go
new file mode 100644
index 000000000..adfabc132
--- /dev/null
+++ b/tests/gocase/unit/kmetadata/kmetadata_test.go
@@ -0,0 +1,248 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package kmetadata
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/redis/go-redis/v9"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/apache/kvrocks/tests/gocase/util"
+)
+
+type kMetadataResponse struct {
+ expire int64 `redis:"expire"`
+ size int64 `redis:"size"`
+ ktype string `redis:"type"`
+ flags int64 `redis:"flags"`
+ version int64 `redis:"version"`
+}
+
+func toInt64(val interface{}) (int64, error) {
+ switch v := val.(type) {
+ case int64:
+ return v, nil
+ case int:
+ return int64(v), nil
+ case float64:
+ return int64(v), nil
+ default:
+ return 0, fmt.Errorf("value is not a number, got %T", val)
+ }
+}
+
+func ExtractKMetadataResponse(result interface{}) (*kMetadataResponse, error) {
+ resultMap, ok := result.(map[interface{}]interface{})
+ if !ok {
+ return nil, fmt.Errorf("expected map[interface{}]interface{},
got %T", result)
+ }
+
+ response := &kMetadataResponse{}
+
+ // Convert numeric fields
+ for field, target := range map[string]*int64{
+ "expire": &response.expire,
+ "size": &response.size,
+ "flags": &response.flags,
+ "version": &response.version,
+ } {
+ if val, ok := resultMap[field]; ok {
+ converted, err := toInt64(val)
+ if err != nil {
+ return nil, fmt.Errorf("%s: %v", field, err)
+ }
+ *target = converted
+ }
+ }
+
+ // Extract Type field
+ if val, ok := resultMap["type"]; ok {
+ if strVal, ok := val.(string); ok {
+ response.ktype = strVal
+ } else {
+ return nil, fmt.Errorf("type is not a string, got %T",
val)
+ }
+ }
+
+ return response, nil
+}
+
+func TestKMetadata(t *testing.T) {
+ configOptions := []util.ConfigOptions{
+ {
+ Name: "resp3-enabled",
+ Options: []string{"yes"},
+ ConfigType: util.YesNo,
+ },
+ }
+ configsMatrix, err := util.GenerateConfigsMatrix(configOptions)
+ require.NoError(t, err)
+ for _, configs := range configsMatrix {
+ testKMetadata(t, configs)
+ }
+}
+
+var testKMetadata = func(t *testing.T, configs util.KvrocksServerConfigs) {
+ srv := util.StartServer(t, configs)
+ defer srv.Close()
+ ctx := context.Background()
+ rdb := srv.NewClient()
+ defer func() { require.NoError(t, rdb.Close()) }()
+
+ t.Run("Test KMetadata for String type", func(t *testing.T) {
+ key := "__avoid_collisions__" + "_KMetadataString_" +
util.RandString(1, 10, util.Alpha)
+ val := "__avoid_collisions__" + "_KMetadataString_" +
util.RandString(1, 10, util.Alpha)
+ rdb.Set(ctx, key, val, 0)
+ r := rdb.Do(ctx, "kmetadata", key)
+ result, err := r.Result()
+ if err != nil {
+ t.Fatalf("Command failed: %v", err)
+ }
+ metaResponse, err := ExtractKMetadataResponse(result)
+ if err != nil {
+ t.Fatalf("Failed to extract response: %v", err)
+ }
+ assert.Equal(t, "string", metaResponse.ktype)
+ assert.Equal(t, int64(0), metaResponse.version)
+ assert.Equal(t, int64(0), metaResponse.size)
+ })
+
+ t.Run("Test KMetadata for hash type", func(t *testing.T) {
+ key := "__avoid_collisions__" + "_kMetadataHash_" +
util.RandString(1, 10, util.Alpha)
+ f1 := "__avoid_collisions__" + "_kMetadataHash_" +
util.RandString(1, 10, util.Alpha)
+ v1 := "__avoid_collisions__" + "_kMetadataHash_" +
util.RandString(1, 10, util.Alpha)
+ f2 := "__avoid_collisions__" + "_kMetadataHash_" +
util.RandString(1, 10, util.Alpha)
+ v2 := "__avoid_collisions__" + "_kMetadataHash_" +
util.RandString(1, 10, util.Alpha)
+ rdb.HSet(ctx, key, f1, v1, f2, v2)
+ r := rdb.Do(ctx, "kmetadata", key)
+ result, err := r.Result()
+ if err != nil {
+ t.Fatalf("Command failed: %v", err)
+ }
+ metaResponse, err := ExtractKMetadataResponse(result)
+ if err != nil {
+ t.Fatalf("Failed to extract response: %v", err)
+ }
+ assert.Equal(t, "hash", metaResponse.ktype)
+ assert.NotEqual(t, int64(0), metaResponse.version)
+ assert.Equal(t, int64(2), metaResponse.size)
+ })
+
+ t.Run("Test KMetadata for set type", func(t *testing.T) {
+ setName := "__avoid_collisions__" + "_kMetadataSet_" +
util.RandString(1, 10, util.Alpha)
+ item1 := "__avoid_collisions__" + "_kMetadataSet_" +
util.RandString(1, 10, util.Alpha)
+ item2 := "__avoid_collisions__" + "_kMetadataSet_" +
util.RandString(1, 10, util.Alpha)
+ item3 := "__avoid_collisions__" + "_kMetadataSet_" +
util.RandString(1, 10, util.Alpha)
+ item4 := "__avoid_collisions__" + "_kMetadataSet_" +
util.RandString(1, 10, util.Alpha)
+ rdb.SAdd(ctx, setName, item1, item2, item3, item4)
+ r := rdb.Do(ctx, "kmetadata", setName)
+ result, err := r.Result()
+ if err != nil {
+ t.Fatalf("Command failed: %v", err)
+ }
+ metaResponse, err := ExtractKMetadataResponse(result)
+ if err != nil {
+ t.Fatalf("Failed to extract response: %v", err)
+ }
+ assert.Equal(t, "set", metaResponse.ktype)
+ assert.NotEqual(t, int64(0), metaResponse.version)
+ assert.Equal(t, int64(4), metaResponse.size)
+ })
+
+ t.Run("Test KMetadata for zset type", func(t *testing.T) {
+ zsetName := "__avoid_collisions__" + "_kMetadataZSet_" +
util.RandString(1, 10, util.Alpha)
+ members := []redis.Z{
+ {
+ Score: 1.0,
+ Member: "__avoid_collisions__" +
"_kMetadataZSet_" + util.RandString(1, 10, util.Alpha),
+ },
+ {
+ Score: 2.0,
+ Member: "__avoid_collisions__" +
"_kMetadataZSet_" + util.RandString(1, 10, util.Alpha),
+ },
+ {
+ Score: 3.0,
+ Member: "__avoid_collisions__" +
"_kMetadataZSet_" + util.RandString(1, 10, util.Alpha),
+ },
+ }
+ rdb.ZAdd(ctx, zsetName, members...)
+ r := rdb.Do(ctx, "kmetadata", zsetName)
+ result, err := r.Result()
+ if err != nil {
+ t.Fatalf("Command failed: %v", err)
+ }
+ metaResponse, err := ExtractKMetadataResponse(result)
+ if err != nil {
+ t.Fatalf("Failed to extract response: %v", err)
+ }
+ assert.Equal(t, "zset", metaResponse.ktype)
+ assert.NotEqual(t, int64(0), metaResponse.version)
+ assert.Equal(t, int64(3), metaResponse.size)
+ })
+
+ t.Run("Test KMetadata for Bitmap type", func(t *testing.T) {
+ bitMapKey := "__avoid_collisions__" + "_kMetadataBitMap_" +
util.RandString(1, 10, util.Alpha)
+ rdb.SetBit(ctx, bitMapKey, 0, 1)
+ r := rdb.Do(ctx, "kmetadata", bitMapKey)
+ result, err := r.Result()
+ if err != nil {
+ t.Fatalf("Command failed: %v", err)
+ }
+ metaResponse, err := ExtractKMetadataResponse(result)
+ if err != nil {
+ t.Fatalf("Failed to extract response: %v", err)
+ }
+ assert.Equal(t, "bitmap", metaResponse.ktype)
+ assert.NotEqual(t, int64(0), metaResponse.version)
+ assert.Equal(t, int64(1), metaResponse.size)
+ })
+
+ t.Run("Test KMetadata for List type", func(t *testing.T) {
+ listKey := "__avoid_collisions__" + "_kMetadataList_" +
util.RandString(1, 10, util.Alpha)
+ item1 := "__avoid_collisions__" + "_kMetadataList_" +
util.RandString(1, 10, util.Alpha)
+ item2 := "__avoid_collisions__" + "_kMetadataList_" +
util.RandString(1, 10, util.Alpha)
+ rdb.RPush(ctx, listKey, item1, item2)
+ r := rdb.Do(ctx, "kmetadata", listKey)
+ result, err := r.Result()
+ if err != nil {
+ t.Fatalf("Command failed: %v", err)
+ }
+ metaResponse, err := ExtractKMetadataResponse(result)
+ if err != nil {
+ t.Fatalf("Failed to extract response: %v", err)
+ }
+ assert.Equal(t, "list", metaResponse.ktype)
+ assert.NotEqual(t, int64(0), metaResponse.version)
+ assert.Equal(t, int64(2), metaResponse.size)
+ })
+
+ t.Run("Test Key not present", func(t *testing.T) {
+ notFoundKey := "__avoid_collisions__" + "_kMetadataNotFound_" +
util.RandString(1, 10, util.Alpha)
+ r := rdb.Do(ctx, "kmetadata", notFoundKey)
+ val := r.Val()
+ assert.Equal(t, nil, val)
+ assert.Error(t, r.Err())
+ })
+
+}