Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fdad4ab
feat: implement DIGEST command
chakkk309 Dec 26, 2025
e03b477
Update src/commands/cmd_string.cc
chakkk309 Dec 27, 2025
624ce78
Update tests/gocase/unit/type/string/digest_test.go
chakkk309 Dec 27, 2025
db10ddd
style: use String() method instead of typecast in digest tests
chakkk309 Dec 27, 2025
ec7052e
Merge remote-tracking branch 'fork/feat-implement-DIGEST-command' int…
chakkk309 Dec 27, 2025
b9e603c
style: add license header in digest test
chakkk309 Dec 27, 2025
302b8d0
style: fix clang-format issues
chakkk309 Dec 27, 2025
02670d7
Update tests/gocase/unit/type/string/digest_test.go
chakkk309 Dec 27, 2025
928dcf6
Merge remote-tracking branch 'fork/feat-implement-DIGEST-command' int…
chakkk309 Dec 27, 2025
20974ca
style: remove meaningless comments from digest test
chakkk309 Dec 27, 2025
694b53a
refactor: implement DIGEST command
chakkk309 Dec 28, 2025
d1120f8
fix: revert DIGEST tests to use Val() method instead of String()
chakkk309 Dec 28, 2025
2226341
fix: clang format error
chakkk309 Dec 28, 2025
8623728
refactor: move ComputeXXH3Hash from redis::String to util
chakkk309 Dec 28, 2025
c9e1744
Update src/common/string_util.cc
chakkk309 Dec 29, 2025
0c93d42
Update src/common/string_util.cc
chakkk309 Dec 29, 2025
0c3f318
fix: remove bitmap support from DIGEST command
chakkk309 Dec 29, 2025
d5da219
fix: digest test expected hash values
chakkk309 Dec 29, 2025
d2618f3
Update src/types/redis_string.cc
PragmaTwice Dec 29, 2025
c7a99dd
style: fix clang format
chakkk309 Dec 29, 2025
ca1dbdb
fix: remove useless import in digest test
chakkk309 Dec 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/commands/cmd_string.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
#include <cstdint>
#include <optional>
#include <string>
#include <fmt/format.h>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this pls.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


#include "xxhash.h"
#include "commander.h"
#include "commands/command_parser.h"
#include "error_constants.h"
Expand Down Expand Up @@ -721,10 +723,47 @@ class CommandLCS : public Commander {
int64_t min_match_len_ = 0;
};

class CommandDigest : public Commander {
public:
Status Parse(const std::vector<std::string> &args) override {
if (args.size() != 2) {
return {Status::RedisParseErr, errWrongNumOfArguments};
}
return Commander::Parse(args);
}

Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
std::string value;
redis::String string_db(srv->storage, conn->GetNamespace());

auto s = string_db.Get(ctx, args_[1], &value);

if (s.IsInvalidArgument()) {
Config *config = srv->GetConfig();
uint32_t max_btos_size = static_cast<uint32_t>(config->max_bitmap_to_string_mb) * MiB;
redis::Bitmap bitmap_db(srv->storage, conn->GetNamespace());
s = bitmap_db.GetString(ctx, args_[1], max_btos_size, &value);
}
if (!s.ok() && !s.IsNotFound()) {
return {Status::RedisExecErr, s.ToString()};
}

if (s.IsNotFound()) {
*output = conn->NilString();
return Status::OK();
}

uint64_t hash = XXH3_64bits(value.data(), value.size());
*output = redis::BulkString(fmt::format("{:016x}", hash));
return Status::OK();
}
};

REDIS_REGISTER_COMMANDS(
String, MakeCmdAttr<CommandGet>("get", 2, "read-only", 1, 1, 1),
MakeCmdAttr<CommandGetEx>("getex", -2, "write", 1, 1, 1),
MakeCmdAttr<CommandStrlen>("strlen", 2, "read-only", 1, 1, 1),
MakeCmdAttr<CommandDigest>("digest", 2, "read-only", 1, 1, 1),
MakeCmdAttr<CommandGetSet>("getset", 3, "write", 1, 1, 1),
MakeCmdAttr<CommandGetRange>("getrange", 4, "read-only", 1, 1, 1),
MakeCmdAttr<CommandSubStr>("substr", 4, "read-only", 1, 1, 1),
Expand Down
158 changes: 158 additions & 0 deletions tests/gocase/unit/type/string/digest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package string

import (
"context"
"testing"

"github.com/stretchr/testify/require"

"github.com/apache/kvrocks/tests/gocase/util"
)

func TestDigest(t *testing.T) {
srv := util.StartServer(t, map[string]string{})
defer srv.Close()
ctx := context.Background()
rdb := srv.NewClient()
defer func() { require.NoError(t, rdb.Close()) }()

t.Run("DIGEST with existing string key", func(t *testing.T) {
require.NoError(t, rdb.Set(ctx, "key1", "Hello world", 0).Err())

// DIGEST should return hex hash
digest := rdb.Do(ctx, "DIGEST", "key1").Val()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can use the String to avoid the typecast. The same as other places.

Suggested change
digest := rdb.Do(ctx, "DIGEST", "key1").Val()
digest := rdb.Do(ctx, "DIGEST", "key1").String()

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can use the String to avoid the typecast. The same as other places.

Thanks for the suggestion, I will update other places.

require.NotNil(t, digest)

// Result should be a string with 16 hex characters
digestStr, ok := digest.(string)
require.True(t, ok)
require.Len(t, digestStr, 16)

// Verify it contains only valid hex characters
for _, c := range digestStr {
require.True(t, (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))
}
})

t.Run("DIGEST with non-existent key", func(t *testing.T) {
// DIGEST should return nil for non-existent key
digest := rdb.Do(ctx, "DIGEST", "nonexistent").Val()
require.Nil(t, digest)
})

t.Run("DIGEST with different string values produces different hashes", func(t *testing.T) {
require.NoError(t, rdb.Set(ctx, "key1", "Hello", 0).Err())
require.NoError(t, rdb.Set(ctx, "key2", "World", 0).Err())

digest1 := rdb.Do(ctx, "DIGEST", "key1").Val().(string)
digest2 := rdb.Do(ctx, "DIGEST", "key2").Val().(string)

require.NotEqual(t, digest1, digest2)
})

t.Run("DIGEST with same string value produces same hash", func(t *testing.T) {
require.NoError(t, rdb.Set(ctx, "key1", "consistent", 0).Err())
require.NoError(t, rdb.Set(ctx, "key2", "consistent", 0).Err())

digest1 := rdb.Do(ctx, "DIGEST", "key1").Val().(string)
digest2 := rdb.Do(ctx, "DIGEST", "key2").Val().(string)

require.Equal(t, digest1, digest2)
})

t.Run("DIGEST with empty string", func(t *testing.T) {
require.NoError(t, rdb.Set(ctx, "empty", "", 0).Err())

digest := rdb.Do(ctx, "DIGEST", "empty").Val()
require.NotNil(t, digest)

// Should still return a valid hex string
digestStr := digest.(string)
require.Len(t, digestStr, 16)
})

t.Run("DIGEST with binary data", func(t *testing.T) {
binaryData := "\x00\x01\x02\xff\xfe\xfd"
require.NoError(t, rdb.Set(ctx, "binary", binaryData, 0).Err())

digest := rdb.Do(ctx, "DIGEST", "binary").Val()
require.NotNil(t, digest)

digestStr := digest.(string)
require.Len(t, digestStr, 16)
})

t.Run("DIGEST with large string", func(t *testing.T) {
// Create a large string
largeString := make([]byte, 10240)
for i := range largeString {
largeString[i] = byte(i % 256)
}

require.NoError(t, rdb.Set(ctx, "large", string(largeString), 0).Err())

digest := rdb.Do(ctx, "DIGEST", "large").Val()
require.NotNil(t, digest)

digestStr := digest.(string)
require.Len(t, digestStr, 16)
})

t.Run("DIGEST wrong number of arguments", func(t *testing.T) {
// Too few arguments
err := rdb.Do(ctx, "DIGEST").Err()
require.Error(t, err)
require.Contains(t, err.Error(), "wrong number of arguments")

// Too many arguments
err = rdb.Do(ctx, "DIGEST", "key1", "extra").Err()
require.Error(t, err)
require.Contains(t, err.Error(), "wrong number of arguments")
})

t.Run("DIGEST with wrong key type should fail", func(t *testing.T) {
// Set up a non-string key
require.NoError(t, rdb.LPush(ctx, "list_key", "value").Err())

// DIGEST should fail on non-string keys
err := rdb.Do(ctx, "DIGEST", "list_key").Err()
require.Error(t, err)
require.Contains(t, err.Error(), "WRONGTYPE")
})
}

func TestDigestCompatibility(t *testing.T) {
srv := util.StartServer(t, map[string]string{})
defer srv.Close()
ctx := context.Background()
rdb := srv.NewClient()
defer func() { require.NoError(t, rdb.Close()) }()

// Test compatibility with Redis command syntax
testCases := []struct {
name string
value string
expected string
}{
{"simple string", "hello", ""},
{"number as string", "123", ""},
{"special chars", "!@#$%^&*()", ""},
{"unicode", "こんにちは", ""},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please fill these expcted value..

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the reminder, i 've already added.

}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
require.NoError(t, rdb.Set(ctx, "test_key", tc.value, 0).Err())

digest := rdb.Do(ctx, "DIGEST", "test_key").Val()
require.NotNil(t, digest)

digestStr := digest.(string)
require.Len(t, digestStr, 16)

if tc.expected != "" {
require.Equal(t, tc.expected, digestStr)
}
})
}
}
Loading