Skip to content

Commit cc3beaf

Browse files
authored
Merge branch 'master' into load-balance-search-commands-to-shards
2 parents c235f6c + 5069fd6 commit cc3beaf

File tree

10 files changed

+1769
-2
lines changed

10 files changed

+1769
-2
lines changed

.github/workflows/doctests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616

1717
services:
1818
redis-stack:
19-
image: redislabs/client-libs-test:8.0.2
19+
image: redislabs/client-libs-test:8.4-RC1-pre.2
2020
env:
2121
TLS_ENABLED: no
2222
REDIS_CLUSTER: no

command.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,68 @@ func (cmd *IntCmd) Clone() Cmder {
880880

881881
//------------------------------------------------------------------------------
882882

883+
// DigestCmd is a command that returns a uint64 xxh3 hash digest.
884+
//
885+
// This command is specifically designed for the Redis DIGEST command,
886+
// which returns the xxh3 hash of a key's value as a hex string.
887+
// The hex string is automatically parsed to a uint64 value.
888+
//
889+
// The digest can be used for optimistic locking with SetIFDEQ, SetIFDNE,
890+
// and DelExArgs commands.
891+
//
892+
// For examples of client-side digest generation and usage patterns, see:
893+
// example/digest-optimistic-locking/
894+
//
895+
// Redis 8.4+. See https://redis.io/commands/digest/
896+
type DigestCmd struct {
897+
baseCmd
898+
899+
val uint64
900+
}
901+
902+
var _ Cmder = (*DigestCmd)(nil)
903+
904+
func NewDigestCmd(ctx context.Context, args ...interface{}) *DigestCmd {
905+
return &DigestCmd{
906+
baseCmd: baseCmd{
907+
ctx: ctx,
908+
args: args,
909+
},
910+
}
911+
}
912+
913+
func (cmd *DigestCmd) SetVal(val uint64) {
914+
cmd.val = val
915+
}
916+
917+
func (cmd *DigestCmd) Val() uint64 {
918+
return cmd.val
919+
}
920+
921+
func (cmd *DigestCmd) Result() (uint64, error) {
922+
return cmd.val, cmd.err
923+
}
924+
925+
func (cmd *DigestCmd) String() string {
926+
return cmdString(cmd, cmd.val)
927+
}
928+
929+
func (cmd *DigestCmd) readReply(rd *proto.Reader) (err error) {
930+
// Redis DIGEST command returns a hex string (e.g., "a1b2c3d4e5f67890")
931+
// We parse it as a uint64 xxh3 hash value
932+
var hexStr string
933+
hexStr, err = rd.ReadString()
934+
if err != nil {
935+
return err
936+
}
937+
938+
// Parse hex string to uint64
939+
cmd.val, err = strconv.ParseUint(hexStr, 16, 64)
940+
return err
941+
}
942+
943+
//------------------------------------------------------------------------------
944+
883945
type IntSliceCmd struct {
884946
baseCmd
885947

command_digest_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package redis
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/redis/go-redis/v9/internal/proto"
9+
)
10+
11+
func TestDigestCmd(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
hexStr string
15+
expected uint64
16+
wantErr bool
17+
}{
18+
{
19+
name: "zero value",
20+
hexStr: "0",
21+
expected: 0,
22+
wantErr: false,
23+
},
24+
{
25+
name: "small value",
26+
hexStr: "ff",
27+
expected: 255,
28+
wantErr: false,
29+
},
30+
{
31+
name: "medium value",
32+
hexStr: "1234abcd",
33+
expected: 0x1234abcd,
34+
wantErr: false,
35+
},
36+
{
37+
name: "large value",
38+
hexStr: "ffffffffffffffff",
39+
expected: 0xffffffffffffffff,
40+
wantErr: false,
41+
},
42+
{
43+
name: "uppercase hex",
44+
hexStr: "DEADBEEF",
45+
expected: 0xdeadbeef,
46+
wantErr: false,
47+
},
48+
{
49+
name: "mixed case hex",
50+
hexStr: "DeAdBeEf",
51+
expected: 0xdeadbeef,
52+
wantErr: false,
53+
},
54+
{
55+
name: "typical xxh3 hash",
56+
hexStr: "a1b2c3d4e5f67890",
57+
expected: 0xa1b2c3d4e5f67890,
58+
wantErr: false,
59+
},
60+
}
61+
62+
for _, tt := range tests {
63+
t.Run(tt.name, func(t *testing.T) {
64+
// Create a mock reader that returns the hex string in RESP format
65+
// Format: $<length>\r\n<data>\r\n
66+
respData := []byte(fmt.Sprintf("$%d\r\n%s\r\n", len(tt.hexStr), tt.hexStr))
67+
68+
rd := proto.NewReader(newMockConn(respData))
69+
70+
cmd := NewDigestCmd(context.Background(), "digest", "key")
71+
err := cmd.readReply(rd)
72+
73+
if (err != nil) != tt.wantErr {
74+
t.Errorf("DigestCmd.readReply() error = %v, wantErr %v", err, tt.wantErr)
75+
return
76+
}
77+
78+
if !tt.wantErr && cmd.Val() != tt.expected {
79+
t.Errorf("DigestCmd.Val() = %d (0x%x), want %d (0x%x)", cmd.Val(), cmd.Val(), tt.expected, tt.expected)
80+
}
81+
})
82+
}
83+
}
84+
85+
func TestDigestCmdResult(t *testing.T) {
86+
cmd := NewDigestCmd(context.Background(), "digest", "key")
87+
expected := uint64(0xdeadbeefcafebabe)
88+
cmd.SetVal(expected)
89+
90+
val, err := cmd.Result()
91+
if err != nil {
92+
t.Errorf("DigestCmd.Result() error = %v", err)
93+
}
94+
95+
if val != expected {
96+
t.Errorf("DigestCmd.Result() = %d (0x%x), want %d (0x%x)", val, val, expected, expected)
97+
}
98+
}
99+
100+
// mockConn is a simple mock connection for testing
101+
type mockConn struct {
102+
data []byte
103+
pos int
104+
}
105+
106+
func newMockConn(data []byte) *mockConn {
107+
return &mockConn{data: data}
108+
}
109+
110+
func (c *mockConn) Read(p []byte) (n int, err error) {
111+
if c.pos >= len(c.data) {
112+
return 0, nil
113+
}
114+
n = copy(p, c.data[c.pos:])
115+
c.pos += n
116+
return n, nil
117+
}
118+

0 commit comments

Comments
 (0)