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) {