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())
+       })
+
+}

Reply via email to