Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/doctests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:

services:
redis-stack:
image: redislabs/client-libs-test:8.0.2
image: redislabs/client-libs-test:8.4-RC1-pre.2
env:
TLS_ENABLED: no
REDIS_CLUSTER: no
Expand Down
62 changes: 62 additions & 0 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,68 @@ func (cmd *IntCmd) readReply(rd *proto.Reader) (err error) {

//------------------------------------------------------------------------------

// DigestCmd is a command that returns a uint64 xxh3 hash digest.
//
// This command is specifically designed for the Redis DIGEST command,
// which returns the xxh3 hash of a key's value as a hex string.
// The hex string is automatically parsed to a uint64 value.
//
// The digest can be used for optimistic locking with SetIFDEQ, SetIFDNE,
// and DelExArgs commands.
//
// For examples of client-side digest generation and usage patterns, see:
// example/digest-optimistic-locking/
//
// Redis 8.4+. See https://redis.io/commands/digest/
type DigestCmd struct {
baseCmd

val uint64
}

var _ Cmder = (*DigestCmd)(nil)

func NewDigestCmd(ctx context.Context, args ...interface{}) *DigestCmd {
return &DigestCmd{
baseCmd: baseCmd{
ctx: ctx,
args: args,
},
}
}

func (cmd *DigestCmd) SetVal(val uint64) {
cmd.val = val
}

func (cmd *DigestCmd) Val() uint64 {
return cmd.val
}

func (cmd *DigestCmd) Result() (uint64, error) {
return cmd.val, cmd.err
}

func (cmd *DigestCmd) String() string {
return cmdString(cmd, cmd.val)
}

func (cmd *DigestCmd) readReply(rd *proto.Reader) (err error) {
// Redis DIGEST command returns a hex string (e.g., "a1b2c3d4e5f67890")
// We parse it as a uint64 xxh3 hash value
var hexStr string
hexStr, err = rd.ReadString()
if err != nil {
return err
}

// Parse hex string to uint64
cmd.val, err = strconv.ParseUint(hexStr, 16, 64)
return err
}

//------------------------------------------------------------------------------

type IntSliceCmd struct {
baseCmd

Expand Down
118 changes: 118 additions & 0 deletions command_digest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package redis

import (
"context"
"fmt"
"testing"

"github.com/redis/go-redis/v9/internal/proto"
)

func TestDigestCmd(t *testing.T) {
tests := []struct {
name string
hexStr string
expected uint64
wantErr bool
}{
{
name: "zero value",
hexStr: "0",
expected: 0,
wantErr: false,
},
{
name: "small value",
hexStr: "ff",
expected: 255,
wantErr: false,
},
{
name: "medium value",
hexStr: "1234abcd",
expected: 0x1234abcd,
wantErr: false,
},
{
name: "large value",
hexStr: "ffffffffffffffff",
expected: 0xffffffffffffffff,
wantErr: false,
},
{
name: "uppercase hex",
hexStr: "DEADBEEF",
expected: 0xdeadbeef,
wantErr: false,
},
{
name: "mixed case hex",
hexStr: "DeAdBeEf",
expected: 0xdeadbeef,
wantErr: false,
},
{
name: "typical xxh3 hash",
hexStr: "a1b2c3d4e5f67890",
expected: 0xa1b2c3d4e5f67890,
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a mock reader that returns the hex string in RESP format
// Format: $<length>\r\n<data>\r\n
respData := []byte(fmt.Sprintf("$%d\r\n%s\r\n", len(tt.hexStr), tt.hexStr))

rd := proto.NewReader(newMockConn(respData))

cmd := NewDigestCmd(context.Background(), "digest", "key")
err := cmd.readReply(rd)

if (err != nil) != tt.wantErr {
t.Errorf("DigestCmd.readReply() error = %v, wantErr %v", err, tt.wantErr)
return
}

if !tt.wantErr && cmd.Val() != tt.expected {
t.Errorf("DigestCmd.Val() = %d (0x%x), want %d (0x%x)", cmd.Val(), cmd.Val(), tt.expected, tt.expected)
}
})
}
}

func TestDigestCmdResult(t *testing.T) {
cmd := NewDigestCmd(context.Background(), "digest", "key")
expected := uint64(0xdeadbeefcafebabe)
cmd.SetVal(expected)

val, err := cmd.Result()
if err != nil {
t.Errorf("DigestCmd.Result() error = %v", err)
}

if val != expected {
t.Errorf("DigestCmd.Result() = %d (0x%x), want %d (0x%x)", val, val, expected, expected)
}
}

// mockConn is a simple mock connection for testing
type mockConn struct {
data []byte
pos int
}

func newMockConn(data []byte) *mockConn {
return &mockConn{data: data}
}

func (c *mockConn) Read(p []byte) (n int, err error) {
if c.pos >= len(c.data) {
return 0, nil
}
n = copy(p, c.data[c.pos:])
c.pos += n
return n, nil
}

Loading
Loading