Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
20 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
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
34 changes: 32 additions & 2 deletions src/commands/cmd_string.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@
*
*/

#include "commander.h"
#include <cstdint>
#include <optional>
#include <string>

#include "commander.h"
#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 "commands/command_parser.h"
#include "common/string_util.h"
Copy link
Member

Choose a reason for hiding this comment

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

and this.

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 "error_constants.h"
#include "server/redis_reply.h"
#include "server/redis_request.h"
Expand Down Expand Up @@ -721,10 +722,39 @@ class CommandLCS : public Commander {
int64_t min_match_len_ = 0;
};

class CommandDigest : public Commander {
public:
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
redis::String string_db(srv->storage, conn->GetNamespace());
std::string digest;
auto s = string_db.Digest(ctx, args_[1], &digest);
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());
std::string value;
s = bitmap_db.GetString(ctx, args_[1], max_btos_size, &value);
if (s.ok()) {
digest = util::ComputeXXH3Hash(value);
}
}
Copy link
Member

Choose a reason for hiding this comment

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

ditto.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks, i will remove bitmap related code.

if (!s.ok() && !s.IsNotFound()) {
return {Status::RedisExecErr, s.ToString()};
}
if (s.IsNotFound()) {
*output = conn->NilString();
return Status::OK();
}
*output = redis::BulkString(digest);
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
6 changes: 6 additions & 0 deletions src/common/string_util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include <string>

#include "parse_util.h"
#include "xxh3.h"

namespace util {

Expand Down Expand Up @@ -556,4 +557,9 @@ std::string StringNext(std::string s) {
return s;
}

std::string ComputeXXH3Hash(const std::string &data) {
uint64_t hash = XXH3_64bits(data.data(), data.size());
return fmt::format("{:016x}", hash);
}

} // namespace util
1 change: 1 addition & 0 deletions src/common/string_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ std::string StringToHex(std::string_view input);
std::vector<std::string> TokenizeRedisProtocol(const std::string &value);
std::string EscapeString(std::string_view s);
std::string StringNext(std::string s);
std::string ComputeXXH3Hash(const std::string &data);

template <typename T, typename F, std::enable_if_t<std::is_invocable_v<F, typename T::value_type>, int> = 0>
std::string StringJoin(const T &con, F &&f, std::string_view sep = ", ") {
Expand Down
14 changes: 14 additions & 0 deletions src/types/redis_string.cc
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
#include <cstdint>
#include <optional>
#include <string>
#include <fmt/format.h>

#include "common/string_util.h"
#include "parse_util.h"
#include "storage/redis_metadata.h"
#include "time_util.h"
Expand Down Expand Up @@ -657,4 +659,16 @@ rocksdb::Status String::LCS(engine::Context &ctx, const std::string &user_key1,
return rocksdb::Status::OK();
}


rocksdb::Status String::Digest(engine::Context &ctx, const std::string &user_key, std::string *digest) {
std::string value;
auto s = Get(ctx, user_key, &value);
if (!s.ok()) {
return s;
}

*digest = util::ComputeXXH3Hash(value);
return rocksdb::Status::OK();
}

} // namespace redis
1 change: 1 addition & 0 deletions src/types/redis_string.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ class String : public Database {
rocksdb::Status CAD(engine::Context &ctx, const std::string &user_key, const std::string &value, int *flag);
rocksdb::Status LCS(engine::Context &ctx, const std::string &user_key1, const std::string &user_key2,
StringLCSArgs args, StringLCSResult *rst);
rocksdb::Status Digest(engine::Context &ctx, const std::string &user_key, std::string *digest);

private:
rocksdb::Status getValue(engine::Context &ctx, const std::string &ns_key, std::string *value);
Expand Down
151 changes: 151 additions & 0 deletions tests/gocase/unit/type/string/digest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* 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 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 := rdb.Do(ctx, "DIGEST", "key1").Val().(string)
require.Equal(t, "b6acb9d84a38ff74", digest)
})

t.Run("DIGEST with non-existent key", func(t *testing.T) {
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().(string)
require.NotEmpty(t, digest)
require.Len(t, digest, 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().(string)
require.NotEmpty(t, digest)
require.Len(t, digest, 16)
})

t.Run("DIGEST with large string", func(t *testing.T) {
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().(string)
require.NotEmpty(t, digest)
require.Len(t, digest, 16)
})

t.Run("DIGEST wrong number of arguments", func(t *testing.T) {
err := rdb.Do(ctx, "DIGEST").Err()
require.Error(t, err)
require.Contains(t, err.Error(), "wrong number of 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) {
require.NoError(t, rdb.LPush(ctx, "list_key", "value").Err())

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

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().(string)
require.NotEmpty(t, digest)
require.Len(t, digest, 16)

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

Loading