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 8d4d8ea6f feat(commands): expose hash, list and json metadata in 
kmetadata (#3454)
8d4d8ea6f is described below

commit 8d4d8ea6f40fb57fc1f0148e4d1582934592397f
Author: Twice <[email protected]>
AuthorDate: Thu Apr 16 11:18:42 2026 +0800

    feat(commands): expose hash, list and json metadata in kmetadata (#3454)
    
    This extends KMETADATA to expose hash, list, and JSON-specific metadata
    fields. It makes debugging and tests easier by improving observability
    around internal metadata.
    
    Assisted-by: Codex
---
 src/commands/cmd_key.cc                       | 78 ++++++++++++++++++++++----
 tests/gocase/unit/kmetadata/kmetadata_test.go | 81 ++++++++++++++++++++++++---
 2 files changed, 141 insertions(+), 18 deletions(-)

diff --git a/src/commands/cmd_key.cc b/src/commands/cmd_key.cc
index 476dc15a0..2aa647220 100644
--- a/src/commands/cmd_key.cc
+++ b/src/commands/cmd_key.cc
@@ -30,6 +30,30 @@
 
 namespace redis {
 
+namespace {
+
+std::string HashSubkeyEncodingModeName(HashSubkeyEncodingMode mode) {
+  switch (mode) {
+    case HashSubkeyEncodingMode::kLegacy:
+      return "legacy";
+    case HashSubkeyEncodingMode::kFieldExpiration:
+      return "field-expiration";
+  }
+  return "unknown";
+}
+
+std::string JsonStorageFormatName(JsonStorageFormat format) {
+  switch (format) {
+    case JsonStorageFormat::JSON:
+      return "json";
+    case JsonStorageFormat::CBOR:
+      return "cbor";
+  }
+  return "unknown";
+}
+
+}  // namespace
+
 class CommandType : public Commander {
  public:
   Status Execute(engine::Context &ctx, Server *srv, Connection *conn, 
std::string *output) override {
@@ -560,21 +584,53 @@ class CommandKMetadata : public Commander {
     std::string &key = args_[1];
     std::string nskey = redis.AppendNamespacePrefix(key);
 
+    std::string raw_metadata;
+    Slice rest;
     Metadata metadata(kRedisNone, false);
-    auto s = redis.GetMetadata(ctx, RedisTypes::All(), nskey, &metadata);
+    auto s = redis.GetMetadata(ctx, RedisTypes::All(), nskey, &raw_metadata, 
&metadata, &rest);
     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)}});
+    std::map<std::string, std::string> response = {
+        {redis::BulkString("type"), redis::BulkString(metadata.TypeName())},
+        {redis::BulkString("expire"), redis::Integer(metadata.expire)},
+        {redis::BulkString("flags"), redis::Integer(metadata.flags)},
+    };
+
+    if (!metadata.IsSingleKVType()) {
+      response.insert({redis::BulkString("size"), 
redis::Integer(metadata.size)});
+      response.insert({redis::BulkString("version"), 
redis::Integer(metadata.version)});
     }
+
+    if (metadata.Type() == kRedisHash) {
+      HashMetadata hash_metadata(false);
+      Slice metadata_bytes(raw_metadata);
+      s = Database::ParseMetadata({kRedisHash}, &metadata_bytes, 
&hash_metadata);
+      if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
+
+      response.insert({redis::BulkString("mode"), 
redis::BulkString(HashSubkeyEncodingModeName(hash_metadata.mode))});
+      if (hash_metadata.IsFieldExpirationEncoding()) {
+        response.insert({redis::BulkString("expsz"), 
redis::Integer(hash_metadata.expsz)});
+        response.insert({redis::BulkString("lower"), 
redis::Integer(hash_metadata.lower)});
+        response.insert({redis::BulkString("upper"), 
redis::Integer(hash_metadata.upper)});
+      }
+    } else if (metadata.Type() == kRedisList) {
+      ListMetadata list_metadata(false);
+      Slice metadata_bytes(raw_metadata);
+      s = Database::ParseMetadata({kRedisList}, &metadata_bytes, 
&list_metadata);
+      if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
+
+      response.insert({redis::BulkString("head"), 
redis::Integer(list_metadata.head)});
+      response.insert({redis::BulkString("tail"), 
redis::Integer(list_metadata.tail)});
+    } else if (metadata.Type() == kRedisJson) {
+      JsonMetadata json_metadata(false);
+      Slice metadata_bytes(raw_metadata);
+      s = Database::ParseMetadata({kRedisJson}, &metadata_bytes, 
&json_metadata);
+      if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
+
+      response.insert({redis::BulkString("format"), 
redis::BulkString(JsonStorageFormatName(json_metadata.format))});
+    }
+
+    *output = conn->Map(response);
     return Status::OK();
   }
 };
diff --git a/tests/gocase/unit/kmetadata/kmetadata_test.go 
b/tests/gocase/unit/kmetadata/kmetadata_test.go
index 7960c4b9b..96fdbed12 100644
--- a/tests/gocase/unit/kmetadata/kmetadata_test.go
+++ b/tests/gocase/unit/kmetadata/kmetadata_test.go
@@ -36,6 +36,13 @@ type kMetadataResponse struct {
        ktype   string `redis:"type"`
        flags   int64  `redis:"flags"`
        version int64  `redis:"version"`
+       mode    string `redis:"mode"`
+       format  string `redis:"format"`
+       expsz   int64  `redis:"expsz"`
+       lower   int64  `redis:"lower"`
+       upper   int64  `redis:"upper"`
+       head    int64  `redis:"head"`
+       tail    int64  `redis:"tail"`
 }
 
 func toInt64(val interface{}) (int64, error) {
@@ -65,6 +72,11 @@ func ExtractKMetadataResponse(result interface{}) 
(*kMetadataResponse, error) {
                "size":    &response.size,
                "flags":   &response.flags,
                "version": &response.version,
+               "expsz":   &response.expsz,
+               "lower":   &response.lower,
+               "upper":   &response.upper,
+               "head":    &response.head,
+               "tail":    &response.tail,
        } {
                if val, ok := resultMap[field]; ok {
                        converted, err := toInt64(val)
@@ -75,24 +87,45 @@ func ExtractKMetadataResponse(result interface{}) 
(*kMetadataResponse, error) {
                }
        }
 
-       // 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)
+       for field, target := range map[string]*string{
+               "type":   &response.ktype,
+               "mode":   &response.mode,
+               "format": &response.format,
+       } {
+               if val, ok := resultMap[field]; ok {
+                       if strVal, ok := val.(string); ok {
+                               *target = strVal
+                       } else {
+                               return nil, fmt.Errorf("%s is not a string, got 
%T", field, val)
+                       }
                }
        }
 
        return response, nil
 }
 
+func MustKMetadataMap(t *testing.T, result interface{}) 
map[interface{}]interface{} {
+       t.Helper()
+
+       resultMap, ok := result.(map[interface{}]interface{})
+       require.Truef(t, ok, "expected map[interface{}]interface{}, got %T", 
result)
+       return resultMap
+}
+
 func TestKMetadata(t *testing.T) {
        configOptions := []util.ConfigOptions{
                {
                        Name:    "resp3-enabled",
                        Options: []string{"yes"},
                },
+               {
+                       Name:    "hash-encoding-mode",
+                       Options: []string{"legacy", "field-expiration"},
+               },
+               {
+                       Name:    "json-storage-format",
+                       Options: []string{"json", "cbor"},
+               },
        }
        configsMatrix, err := util.GenerateConfigsMatrix(configOptions)
        require.NoError(t, err)
@@ -131,11 +164,25 @@ var testKMetadata = func(t *testing.T, configs 
util.KvrocksServerConfigs) {
                result, err := r.Result()
                require.NoError(t, err)
 
+               resultMap := MustKMetadataMap(t, result)
                metaResponse, err := ExtractKMetadataResponse(result)
                require.NoError(t, err)
                require.Equal(t, "hash", metaResponse.ktype)
                require.NotEqual(t, int64(0), metaResponse.version)
                require.Equal(t, int64(2), metaResponse.size)
+               require.Equal(t, configs["hash-encoding-mode"], 
metaResponse.mode)
+               if configs["hash-encoding-mode"] == "field-expiration" {
+                       require.Equal(t, int64(0), metaResponse.expsz)
+                       require.Equal(t, int64(0), metaResponse.lower)
+                       require.Equal(t, int64(0), metaResponse.upper)
+                       require.Contains(t, resultMap, "expsz")
+                       require.Contains(t, resultMap, "lower")
+                       require.Contains(t, resultMap, "upper")
+               } else {
+                       require.NotContains(t, resultMap, "expsz")
+                       require.NotContains(t, resultMap, "lower")
+                       require.NotContains(t, resultMap, "upper")
+               }
        })
 
        t.Run("Test KMetadata for set type", func(t *testing.T) {
@@ -187,7 +234,9 @@ var testKMetadata = func(t *testing.T, configs 
util.KvrocksServerConfigs) {
 
        t.Run("Test KMetadata for List type", func(t *testing.T) {
                listKey := "list_" + util.RandString(1, 10, util.Alpha)
-               require.NoError(t, rdb.RPush(ctx, listKey, "a", "b").Err())
+               const listInitialIndex = int64(^uint64(0) >> 1)
+
+               require.NoError(t, rdb.LPush(ctx, listKey, "a", "b").Err())
                r := rdb.Do(ctx, "kmetadata", listKey)
                result, err := r.Result()
                require.NoError(t, err)
@@ -197,6 +246,24 @@ var testKMetadata = func(t *testing.T, configs 
util.KvrocksServerConfigs) {
                require.Equal(t, "list", metaResponse.ktype)
                require.NotEqual(t, int64(0), metaResponse.version)
                require.Equal(t, int64(2), metaResponse.size)
+               require.Equal(t, listInitialIndex-2, metaResponse.head)
+               require.Equal(t, listInitialIndex, metaResponse.tail)
+       })
+
+       t.Run("Test KMetadata for JSON type", func(t *testing.T) {
+               jsonKey := "json_" + util.RandString(1, 10, util.Alpha)
+               require.NoError(t, rdb.Do(ctx, "JSON.SET", jsonKey, "$", 
`{"x":1,"y":2}`).Err())
+
+               r := rdb.Do(ctx, "kmetadata", jsonKey)
+               result, err := r.Result()
+               require.NoError(t, err)
+
+               metaResponse, err := ExtractKMetadataResponse(result)
+               require.NoError(t, err)
+               require.Equal(t, "ReJSON-RL", metaResponse.ktype)
+               require.Equal(t, configs["json-storage-format"], 
metaResponse.format)
+               require.Equal(t, int64(0), metaResponse.version)
+               require.Equal(t, int64(0), metaResponse.size)
        })
 
        t.Run("Test Key not present", func(t *testing.T) {

Reply via email to