mirror of
https://github.com/redis/go-redis.git
synced 2025-11-14 10:22:26 +03:00
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
This commit is contained in:
@@ -2,6 +2,7 @@ package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -9,6 +10,8 @@ type StringCmdable interface {
|
||||
Append(ctx context.Context, key, value string) *IntCmd
|
||||
Decr(ctx context.Context, key string) *IntCmd
|
||||
DecrBy(ctx context.Context, key string, decrement int64) *IntCmd
|
||||
DelExArgs(ctx context.Context, key string, a DelExArgs) *IntCmd
|
||||
Digest(ctx context.Context, key string) *DigestCmd
|
||||
Get(ctx context.Context, key string) *StringCmd
|
||||
GetRange(ctx context.Context, key string, start, end int64) *StringCmd
|
||||
GetSet(ctx context.Context, key string, value interface{}) *StringCmd
|
||||
@@ -25,6 +28,14 @@ type StringCmdable interface {
|
||||
Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *StatusCmd
|
||||
SetArgs(ctx context.Context, key string, value interface{}, a SetArgs) *StatusCmd
|
||||
SetEx(ctx context.Context, key string, value interface{}, expiration time.Duration) *StatusCmd
|
||||
SetIFEQ(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StatusCmd
|
||||
SetIFEQGet(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StringCmd
|
||||
SetIFNE(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StatusCmd
|
||||
SetIFNEGet(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StringCmd
|
||||
SetIFDEQ(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StatusCmd
|
||||
SetIFDEQGet(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StringCmd
|
||||
SetIFDNE(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StatusCmd
|
||||
SetIFDNEGet(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StringCmd
|
||||
SetNX(ctx context.Context, key string, value interface{}, expiration time.Duration) *BoolCmd
|
||||
SetXX(ctx context.Context, key string, value interface{}, expiration time.Duration) *BoolCmd
|
||||
SetRange(ctx context.Context, key string, offset int64, value string) *IntCmd
|
||||
@@ -49,6 +60,70 @@ func (c cmdable) DecrBy(ctx context.Context, key string, decrement int64) *IntCm
|
||||
return cmd
|
||||
}
|
||||
|
||||
// DelExArgs provides arguments for the DelExArgs function.
|
||||
type DelExArgs struct {
|
||||
// Mode can be `IFEQ`, `IFNE`, `IFDEQ`, or `IFDNE`.
|
||||
Mode string
|
||||
|
||||
// MatchValue is used with IFEQ/IFNE modes for compare-and-delete operations.
|
||||
// - IFEQ: only delete if current value equals MatchValue
|
||||
// - IFNE: only delete if current value does not equal MatchValue
|
||||
MatchValue interface{}
|
||||
|
||||
// MatchDigest is used with IFDEQ/IFDNE modes for digest-based compare-and-delete.
|
||||
// - IFDEQ: only delete if current value's digest equals MatchDigest
|
||||
// - IFDNE: only delete if current value's digest does not equal MatchDigest
|
||||
//
|
||||
// The digest is a uint64 xxh3 hash value.
|
||||
//
|
||||
// For examples of client-side digest generation, see:
|
||||
// example/digest-optimistic-locking/
|
||||
MatchDigest uint64
|
||||
}
|
||||
|
||||
// DelExArgs Redis `DELEX key [IFEQ|IFNE|IFDEQ|IFDNE] match-value` command.
|
||||
// Compare-and-delete with flexible conditions.
|
||||
//
|
||||
// Returns the number of keys that were removed (0 or 1).
|
||||
func (c cmdable) DelExArgs(ctx context.Context, key string, a DelExArgs) *IntCmd {
|
||||
args := []interface{}{"delex", key}
|
||||
|
||||
if a.Mode != "" {
|
||||
args = append(args, a.Mode)
|
||||
|
||||
// Add match value/digest based on mode
|
||||
switch a.Mode {
|
||||
case "ifeq", "IFEQ", "ifne", "IFNE":
|
||||
if a.MatchValue != nil {
|
||||
args = append(args, a.MatchValue)
|
||||
}
|
||||
case "ifdeq", "IFDEQ", "ifdne", "IFDNE":
|
||||
if a.MatchDigest != 0 {
|
||||
args = append(args, fmt.Sprintf("%016x", a.MatchDigest))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Digest returns the xxh3 hash (uint64) of the specified key's value.
|
||||
//
|
||||
// The digest is a 64-bit xxh3 hash that 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/
|
||||
func (c cmdable) Digest(ctx context.Context, key string) *DigestCmd {
|
||||
cmd := NewDigestCmd(ctx, "digest", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Get Redis `GET key` command. It returns redis.Nil error when key does not exist.
|
||||
func (c cmdable) Get(ctx context.Context, key string) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "get", key)
|
||||
@@ -258,9 +333,24 @@ func (c cmdable) Set(ctx context.Context, key string, value interface{}, expirat
|
||||
|
||||
// SetArgs provides arguments for the SetArgs function.
|
||||
type SetArgs struct {
|
||||
// Mode can be `NX` or `XX` or empty.
|
||||
// Mode can be `NX`, `XX`, `IFEQ`, `IFNE`, `IFDEQ`, `IFDNE` or empty.
|
||||
Mode string
|
||||
|
||||
// MatchValue is used with IFEQ/IFNE modes for compare-and-set operations.
|
||||
// - IFEQ: only set if current value equals MatchValue
|
||||
// - IFNE: only set if current value does not equal MatchValue
|
||||
MatchValue interface{}
|
||||
|
||||
// MatchDigest is used with IFDEQ/IFDNE modes for digest-based compare-and-set.
|
||||
// - IFDEQ: only set if current value's digest equals MatchDigest
|
||||
// - IFDNE: only set if current value's digest does not equal MatchDigest
|
||||
//
|
||||
// The digest is a uint64 xxh3 hash value.
|
||||
//
|
||||
// For examples of client-side digest generation, see:
|
||||
// example/digest-optimistic-locking/
|
||||
MatchDigest uint64
|
||||
|
||||
// Zero `TTL` or `Expiration` means that the key has no expiration time.
|
||||
TTL time.Duration
|
||||
ExpireAt time.Time
|
||||
@@ -296,6 +386,18 @@ func (c cmdable) SetArgs(ctx context.Context, key string, value interface{}, a S
|
||||
|
||||
if a.Mode != "" {
|
||||
args = append(args, a.Mode)
|
||||
|
||||
// Add match value/digest for CAS modes
|
||||
switch a.Mode {
|
||||
case "ifeq", "IFEQ", "ifne", "IFNE":
|
||||
if a.MatchValue != nil {
|
||||
args = append(args, a.MatchValue)
|
||||
}
|
||||
case "ifdeq", "IFDEQ", "ifdne", "IFDNE":
|
||||
if a.MatchDigest != 0 {
|
||||
args = append(args, fmt.Sprintf("%016x", a.MatchDigest))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if a.Get {
|
||||
@@ -363,6 +465,246 @@ func (c cmdable) SetXX(ctx context.Context, key string, value interface{}, expir
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SetIFEQ Redis `SET key value [expiration] IFEQ match-value` command.
|
||||
// Compare-and-set: only sets the value if the current value equals matchValue.
|
||||
//
|
||||
// Returns "OK" on success.
|
||||
// Returns nil if the operation was aborted due to condition not matching.
|
||||
// Zero expiration means the key has no expiration time.
|
||||
func (c cmdable) SetIFEQ(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StatusCmd {
|
||||
args := []interface{}{"set", key, value}
|
||||
|
||||
if expiration > 0 {
|
||||
if usePrecise(expiration) {
|
||||
args = append(args, "px", formatMs(ctx, expiration))
|
||||
} else {
|
||||
args = append(args, "ex", formatSec(ctx, expiration))
|
||||
}
|
||||
} else if expiration == KeepTTL {
|
||||
args = append(args, "keepttl")
|
||||
}
|
||||
|
||||
args = append(args, "ifeq", matchValue)
|
||||
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SetIFEQGet Redis `SET key value [expiration] IFEQ match-value GET` command.
|
||||
// Compare-and-set with GET: only sets the value if the current value equals matchValue,
|
||||
// and returns the previous value.
|
||||
//
|
||||
// Returns the previous value on success.
|
||||
// Returns nil if the operation was aborted due to condition not matching.
|
||||
// Zero expiration means the key has no expiration time.
|
||||
func (c cmdable) SetIFEQGet(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StringCmd {
|
||||
args := []interface{}{"set", key, value}
|
||||
|
||||
if expiration > 0 {
|
||||
if usePrecise(expiration) {
|
||||
args = append(args, "px", formatMs(ctx, expiration))
|
||||
} else {
|
||||
args = append(args, "ex", formatSec(ctx, expiration))
|
||||
}
|
||||
} else if expiration == KeepTTL {
|
||||
args = append(args, "keepttl")
|
||||
}
|
||||
|
||||
args = append(args, "ifeq", matchValue, "get")
|
||||
|
||||
cmd := NewStringCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SetIFNE Redis `SET key value [expiration] IFNE match-value` command.
|
||||
// Compare-and-set: only sets the value if the current value does not equal matchValue.
|
||||
//
|
||||
// Returns "OK" on success.
|
||||
// Returns nil if the operation was aborted due to condition not matching.
|
||||
// Zero expiration means the key has no expiration time.
|
||||
func (c cmdable) SetIFNE(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StatusCmd {
|
||||
args := []interface{}{"set", key, value}
|
||||
|
||||
if expiration > 0 {
|
||||
if usePrecise(expiration) {
|
||||
args = append(args, "px", formatMs(ctx, expiration))
|
||||
} else {
|
||||
args = append(args, "ex", formatSec(ctx, expiration))
|
||||
}
|
||||
} else if expiration == KeepTTL {
|
||||
args = append(args, "keepttl")
|
||||
}
|
||||
|
||||
args = append(args, "ifne", matchValue)
|
||||
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SetIFNEGet Redis `SET key value [expiration] IFNE match-value GET` command.
|
||||
// Compare-and-set with GET: only sets the value if the current value does not equal matchValue,
|
||||
// and returns the previous value.
|
||||
//
|
||||
// Returns the previous value on success.
|
||||
// Returns nil if the operation was aborted due to condition not matching.
|
||||
// Zero expiration means the key has no expiration time.
|
||||
func (c cmdable) SetIFNEGet(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StringCmd {
|
||||
args := []interface{}{"set", key, value}
|
||||
|
||||
if expiration > 0 {
|
||||
if usePrecise(expiration) {
|
||||
args = append(args, "px", formatMs(ctx, expiration))
|
||||
} else {
|
||||
args = append(args, "ex", formatSec(ctx, expiration))
|
||||
}
|
||||
} else if expiration == KeepTTL {
|
||||
args = append(args, "keepttl")
|
||||
}
|
||||
|
||||
args = append(args, "ifne", matchValue, "get")
|
||||
|
||||
cmd := NewStringCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SetIFDEQ sets the value only if the current value's digest equals matchDigest.
|
||||
//
|
||||
// This is a compare-and-set operation using xxh3 digest for optimistic locking.
|
||||
// The matchDigest parameter is a uint64 xxh3 hash value.
|
||||
//
|
||||
// Returns "OK" on success.
|
||||
// Returns redis.Nil if the digest doesn't match (value was modified).
|
||||
// Zero expiration means the key has no expiration time.
|
||||
//
|
||||
// For examples of client-side digest generation and usage patterns, see:
|
||||
// example/digest-optimistic-locking/
|
||||
//
|
||||
// Redis 8.4+. See https://redis.io/commands/set/
|
||||
func (c cmdable) SetIFDEQ(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StatusCmd {
|
||||
args := []interface{}{"set", key, value}
|
||||
|
||||
if expiration > 0 {
|
||||
if usePrecise(expiration) {
|
||||
args = append(args, "px", formatMs(ctx, expiration))
|
||||
} else {
|
||||
args = append(args, "ex", formatSec(ctx, expiration))
|
||||
}
|
||||
} else if expiration == KeepTTL {
|
||||
args = append(args, "keepttl")
|
||||
}
|
||||
|
||||
args = append(args, "ifdeq", fmt.Sprintf("%016x", matchDigest))
|
||||
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SetIFDEQGet sets the value only if the current value's digest equals matchDigest,
|
||||
// and returns the previous value.
|
||||
//
|
||||
// This is a compare-and-set operation using xxh3 digest for optimistic locking.
|
||||
// The matchDigest parameter is a uint64 xxh3 hash value.
|
||||
//
|
||||
// Returns the previous value on success.
|
||||
// Returns redis.Nil if the digest doesn't match (value was modified).
|
||||
// Zero expiration means the key has no expiration time.
|
||||
//
|
||||
// For examples of client-side digest generation and usage patterns, see:
|
||||
// example/digest-optimistic-locking/
|
||||
//
|
||||
// Redis 8.4+. See https://redis.io/commands/set/
|
||||
func (c cmdable) SetIFDEQGet(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StringCmd {
|
||||
args := []interface{}{"set", key, value}
|
||||
|
||||
if expiration > 0 {
|
||||
if usePrecise(expiration) {
|
||||
args = append(args, "px", formatMs(ctx, expiration))
|
||||
} else {
|
||||
args = append(args, "ex", formatSec(ctx, expiration))
|
||||
}
|
||||
} else if expiration == KeepTTL {
|
||||
args = append(args, "keepttl")
|
||||
}
|
||||
|
||||
args = append(args, "ifdeq", fmt.Sprintf("%016x", matchDigest), "get")
|
||||
|
||||
cmd := NewStringCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SetIFDNE sets the value only if the current value's digest does NOT equal matchDigest.
|
||||
//
|
||||
// This is a compare-and-set operation using xxh3 digest for optimistic locking.
|
||||
// The matchDigest parameter is a uint64 xxh3 hash value.
|
||||
//
|
||||
// Returns "OK" on success (digest didn't match, value was set).
|
||||
// Returns redis.Nil if the digest matches (value was not modified).
|
||||
// Zero expiration means the key has no expiration time.
|
||||
//
|
||||
// For examples of client-side digest generation and usage patterns, see:
|
||||
// example/digest-optimistic-locking/
|
||||
//
|
||||
// Redis 8.4+. See https://redis.io/commands/set/
|
||||
func (c cmdable) SetIFDNE(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StatusCmd {
|
||||
args := []interface{}{"set", key, value}
|
||||
|
||||
if expiration > 0 {
|
||||
if usePrecise(expiration) {
|
||||
args = append(args, "px", formatMs(ctx, expiration))
|
||||
} else {
|
||||
args = append(args, "ex", formatSec(ctx, expiration))
|
||||
}
|
||||
} else if expiration == KeepTTL {
|
||||
args = append(args, "keepttl")
|
||||
}
|
||||
|
||||
args = append(args, "ifdne", fmt.Sprintf("%016x", matchDigest))
|
||||
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SetIFDNEGet sets the value only if the current value's digest does NOT equal matchDigest,
|
||||
// and returns the previous value.
|
||||
//
|
||||
// This is a compare-and-set operation using xxh3 digest for optimistic locking.
|
||||
// The matchDigest parameter is a uint64 xxh3 hash value.
|
||||
//
|
||||
// Returns the previous value on success (digest didn't match, value was set).
|
||||
// Returns redis.Nil if the digest matches (value was not modified).
|
||||
// Zero expiration means the key has no expiration time.
|
||||
//
|
||||
// For examples of client-side digest generation and usage patterns, see:
|
||||
// example/digest-optimistic-locking/
|
||||
//
|
||||
// Redis 8.4+. See https://redis.io/commands/set/
|
||||
func (c cmdable) SetIFDNEGet(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StringCmd {
|
||||
args := []interface{}{"set", key, value}
|
||||
|
||||
if expiration > 0 {
|
||||
if usePrecise(expiration) {
|
||||
args = append(args, "px", formatMs(ctx, expiration))
|
||||
} else {
|
||||
args = append(args, "ex", formatSec(ctx, expiration))
|
||||
}
|
||||
} else if expiration == KeepTTL {
|
||||
args = append(args, "keepttl")
|
||||
}
|
||||
|
||||
args = append(args, "ifdne", fmt.Sprintf("%016x", matchDigest), "get")
|
||||
|
||||
cmd := NewStringCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) SetRange(ctx context.Context, key string, offset int64, value string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "setrange", key, offset, value)
|
||||
_ = c(ctx, cmd)
|
||||
|
||||
Reference in New Issue
Block a user