Skip to content

Commit 5069fd6

Browse files
authored
feat(cmd): Add CAS/CAD commands (#3583)
* add cas/cad commands * feat(command): Add SetIFDEQ, SetIFDNE and *Get cmds Decided to move the *Get argument as a separate methods, since the response will be always the previous value, but in the case where the previous value is `OK` there result may be ambiguous. * fix tests * matchValue to be interface{} * Only Args approach for DelEx * use uint64 for digest, add example * test only for 8.4
1 parent c176672 commit 5069fd6

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
@@ -698,6 +698,68 @@ func (cmd *IntCmd) readReply(rd *proto.Reader) (err error) {
698698

699699
//------------------------------------------------------------------------------
700700

701+
// DigestCmd is a command that returns a uint64 xxh3 hash digest.
702+
//
703+
// This command is specifically designed for the Redis DIGEST command,
704+
// which returns the xxh3 hash of a key's value as a hex string.
705+
// The hex string is automatically parsed to a uint64 value.
706+
//
707+
// The digest can be used for optimistic locking with SetIFDEQ, SetIFDNE,
708+
// and DelExArgs commands.
709+
//
710+
// For examples of client-side digest generation and usage patterns, see:
711+
// example/digest-optimistic-locking/
712+
//
713+
// Redis 8.4+. See https://redis.io/commands/digest/
714+
type DigestCmd struct {
715+
baseCmd
716+
717+
val uint64
718+
}
719+
720+
var _ Cmder = (*DigestCmd)(nil)
721+
722+
func NewDigestCmd(ctx context.Context, args ...interface{}) *DigestCmd {
723+
return &DigestCmd{
724+
baseCmd: baseCmd{
725+
ctx: ctx,
726+
args: args,
727+
},
728+
}
729+
}
730+
731+
func (cmd *DigestCmd) SetVal(val uint64) {
732+
cmd.val = val
733+
}
734+
735+
func (cmd *DigestCmd) Val() uint64 {
736+
return cmd.val
737+
}
738+
739+
func (cmd *DigestCmd) Result() (uint64, error) {
740+
return cmd.val, cmd.err
741+
}
742+
743+
func (cmd *DigestCmd) String() string {
744+
return cmdString(cmd, cmd.val)
745+
}
746+
747+
func (cmd *DigestCmd) readReply(rd *proto.Reader) (err error) {
748+
// Redis DIGEST command returns a hex string (e.g., "a1b2c3d4e5f67890")
749+
// We parse it as a uint64 xxh3 hash value
750+
var hexStr string
751+
hexStr, err = rd.ReadString()
752+
if err != nil {
753+
return err
754+
}
755+
756+
// Parse hex string to uint64
757+
cmd.val, err = strconv.ParseUint(hexStr, 16, 64)
758+
return err
759+
}
760+
761+
//------------------------------------------------------------------------------
762+
701763
type IntSliceCmd struct {
702764
baseCmd
703765

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)