mirror of
https://github.com/hanzokv/go.git
synced 2026-09-24 00:31:51 +00:00
feat(command): add support for INCREX (#3816)
* feat(command): add increx * add one additional test * update increx * use custom 8.8 image with new increx * address pr comments * always use by in float
This commit is contained in:
@@ -47,7 +47,7 @@ runs:
|
||||
elif [[ -n "$REDIS_VERSION" ]]; then
|
||||
# Mapping of redis version to redis testing containers
|
||||
declare -A redis_version_mapping=(
|
||||
["8.8.x"]="8.8-rc1"
|
||||
["8.8.x"]="custom-26172898734-debian"
|
||||
["8.6.x"]="8.6.1"
|
||||
["8.4.x"]="8.4.0"
|
||||
["8.2.x"]="8.2.1-pre"
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
|
||||
# Mapping of redis version to redis testing containers
|
||||
declare -A redis_version_mapping=(
|
||||
["8.8.x"]="8.8-rc1"
|
||||
["8.8.x"]="custom-26172898734-debian"
|
||||
["8.6.x"]="8.6.1"
|
||||
["8.4.x"]="8.4.0"
|
||||
["8.2.x"]="8.2.1-pre"
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
services:
|
||||
redis-stack:
|
||||
image: redislabs/client-libs-test:8.8-rc1
|
||||
image: redislabs/client-libs-test:custom-26172898734-debian
|
||||
env:
|
||||
TLS_ENABLED: no
|
||||
REDIS_CLUSTER: no
|
||||
|
||||
@@ -2,7 +2,7 @@ GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort)
|
||||
REDIS_VERSION ?= 8.8
|
||||
RE_CLUSTER ?= false
|
||||
RCE_DOCKER ?= true
|
||||
CLIENT_LIBS_TEST_IMAGE ?= redislabs/client-libs-test:8.8-rc1
|
||||
CLIENT_LIBS_TEST_IMAGE ?= redislabs/client-libs-test:custom-26172898734-debian
|
||||
|
||||
docker.start:
|
||||
export RE_CLUSTER=$(RE_CLUSTER) && \
|
||||
|
||||
+133
-2
@@ -156,6 +156,8 @@ const (
|
||||
CmdTypeTSTimestampValue
|
||||
CmdTypeTSTimestampValueSlice
|
||||
CmdTypeHotKeys
|
||||
CmdTypeIncrEXInt
|
||||
CmdTypeIncrEXFloat
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -4937,8 +4939,10 @@ func (c *cmdsInfoCache) Refresh() {
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
const requestPolicy = "request_policy"
|
||||
const responsePolicy = "response_policy"
|
||||
const (
|
||||
requestPolicy = "request_policy"
|
||||
responsePolicy = "response_policy"
|
||||
)
|
||||
|
||||
func parseCommandPolicies(commandInfoTips map[string]string, firstKeyPos int8) *routing.CommandPolicy {
|
||||
req := routing.ReqDefault
|
||||
@@ -8259,6 +8263,20 @@ func ExtractCommandValue(cmd interface{}) (interface{}, error) {
|
||||
}); ok {
|
||||
return hotKeysCmd.Val(), hotKeysCmd.Err()
|
||||
}
|
||||
case CmdTypeIncrEXInt:
|
||||
if incrEXCmd, ok := cmd.(interface {
|
||||
Val() IncrEXIntResult
|
||||
Err() error
|
||||
}); ok {
|
||||
return incrEXCmd.Val(), incrEXCmd.Err()
|
||||
}
|
||||
case CmdTypeIncrEXFloat:
|
||||
if incrEXCmd, ok := cmd.(interface {
|
||||
Val() IncrEXFloatResult
|
||||
Err() error
|
||||
}); ok {
|
||||
return incrEXCmd.Val(), incrEXCmd.Err()
|
||||
}
|
||||
case CmdTypeKeyValues:
|
||||
if keyValuesCmd, ok := cmd.(interface {
|
||||
Val() (string, []string)
|
||||
@@ -8557,3 +8575,116 @@ func ExtractCommandValue(cmd interface{}) (interface{}, error) {
|
||||
// If we can't get the command type, return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// IncrEXIntResult is the reply of an INCREX command issued via IncrEXInt.
|
||||
// Value is the new value of the key; AppliedIncrement is the increment that
|
||||
// the server actually applied (0 when an out-of-bounds operation was
|
||||
// rejected, clamped when SATURATE was set).
|
||||
type IncrEXIntResult struct {
|
||||
Value int64
|
||||
AppliedIncrement int64
|
||||
}
|
||||
|
||||
type IncrEXIntCmd struct {
|
||||
baseCmd
|
||||
|
||||
val IncrEXIntResult
|
||||
}
|
||||
|
||||
var _ Cmder = (*IncrEXIntCmd)(nil)
|
||||
|
||||
func NewIncrEXIntCmd(ctx context.Context, args ...interface{}) *IncrEXIntCmd {
|
||||
return &IncrEXIntCmd{
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
cmdType: CmdTypeIncrEXInt,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (cmd *IncrEXIntCmd) SetVal(val IncrEXIntResult) { cmd.val = val }
|
||||
func (cmd *IncrEXIntCmd) Val() IncrEXIntResult { return cmd.val }
|
||||
func (cmd *IncrEXIntCmd) Result() (IncrEXIntResult, error) {
|
||||
return cmd.val, cmd.err
|
||||
}
|
||||
func (cmd *IncrEXIntCmd) String() string { return cmdString(cmd, cmd.val) }
|
||||
|
||||
func (cmd *IncrEXIntCmd) readReply(rd *proto.Reader) error {
|
||||
if err := rd.ReadFixedArrayLen(2); err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := rd.ReadInt()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
applied, err := rd.ReadInt()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.val = IncrEXIntResult{Value: value, AppliedIncrement: applied}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cmd *IncrEXIntCmd) Clone() Cmder {
|
||||
return &IncrEXIntCmd{
|
||||
baseCmd: cmd.cloneBaseCmd(),
|
||||
val: cmd.val,
|
||||
}
|
||||
}
|
||||
|
||||
// IncrEXFloatResult is the reply of an INCREX command issued via IncrEXFloat.
|
||||
type IncrEXFloatResult struct {
|
||||
Value float64
|
||||
AppliedIncrement float64
|
||||
}
|
||||
|
||||
type IncrEXFloatCmd struct {
|
||||
baseCmd
|
||||
|
||||
val IncrEXFloatResult
|
||||
}
|
||||
|
||||
var _ Cmder = (*IncrEXFloatCmd)(nil)
|
||||
|
||||
func NewIncrEXFloatCmd(ctx context.Context, args ...interface{}) *IncrEXFloatCmd {
|
||||
return &IncrEXFloatCmd{
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
cmdType: CmdTypeIncrEXFloat,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (cmd *IncrEXFloatCmd) SetVal(val IncrEXFloatResult) { cmd.val = val }
|
||||
func (cmd *IncrEXFloatCmd) Val() IncrEXFloatResult { return cmd.val }
|
||||
func (cmd *IncrEXFloatCmd) Result() (IncrEXFloatResult, error) {
|
||||
return cmd.val, cmd.err
|
||||
}
|
||||
func (cmd *IncrEXFloatCmd) String() string { return cmdString(cmd, cmd.val) }
|
||||
|
||||
func (cmd *IncrEXFloatCmd) readReply(rd *proto.Reader) error {
|
||||
if err := rd.ReadFixedArrayLen(2); err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := rd.ReadFloat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
applied, err := rd.ReadFloat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.val = IncrEXFloatResult{Value: value, AppliedIncrement: applied}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cmd *IncrEXFloatCmd) Clone() Cmder {
|
||||
return &IncrEXFloatCmd{
|
||||
baseCmd: cmd.cloneBaseCmd(),
|
||||
val: cmd.val,
|
||||
}
|
||||
}
|
||||
|
||||
+224
-12
@@ -6,6 +6,7 @@ import (
|
||||
"crypto/sha1"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -368,7 +369,6 @@ var _ = Describe("Commands", func() {
|
||||
defer client2.Close()
|
||||
clientInfo = client2.ClientInfo(ctx).Val()
|
||||
Expect(clientInfo.LibName).To(ContainSubstring("go-redis(suffix,"))
|
||||
|
||||
})
|
||||
|
||||
It("should ConfigGet", func() {
|
||||
@@ -2067,6 +2067,209 @@ var _ = Describe("Commands", func() {
|
||||
Expect(incrByFloat.Val()).To(Equal(float64(996945661)))
|
||||
})
|
||||
|
||||
It("should IncrEXInt default", func() {
|
||||
SkipBeforeRedisVersion(8.8, "IncrEX is available since Redis 8.8")
|
||||
|
||||
Expect(client.Set(ctx, "key", "10", 0).Err()).NotTo(HaveOccurred())
|
||||
|
||||
res, err := client.IncrEXInt(ctx, "key", redis.IncrEXIntArgs{}).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res.Value).To(Equal(int64(11)))
|
||||
Expect(res.AppliedIncrement).To(Equal(int64(1)))
|
||||
|
||||
Expect(client.Get(ctx, "key").Val()).To(Equal("11"))
|
||||
})
|
||||
|
||||
It("should IncrEXInt BYINT", func() {
|
||||
SkipBeforeRedisVersion(8.8, "IncrEX is available since Redis 8.8")
|
||||
|
||||
Expect(client.Set(ctx, "key", "20", 0).Err()).NotTo(HaveOccurred())
|
||||
|
||||
res, err := client.IncrEXInt(ctx, "key", redis.IncrEXIntArgs{By: 4, HasBy: true}).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res.Value).To(Equal(int64(24)))
|
||||
Expect(res.AppliedIncrement).To(Equal(int64(4)))
|
||||
})
|
||||
|
||||
It("should IncrEXFloat with bounds", func() {
|
||||
SkipBeforeRedisVersion(8.8, "IncrEX is available since Redis 8.8")
|
||||
|
||||
Expect(client.Set(ctx, "key", "1.5", 0).Err()).NotTo(HaveOccurred())
|
||||
|
||||
res, err := client.IncrEXFloat(ctx, "key", redis.IncrEXFloatArgs{
|
||||
By: 0.25,
|
||||
LBound: 0,
|
||||
HasLBound: true,
|
||||
UBound: 2,
|
||||
HasUBound: true,
|
||||
}).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res.Value).To(BeNumerically("~", 1.75, 1e-9))
|
||||
Expect(res.AppliedIncrement).To(BeNumerically("~", 0.25, 1e-9))
|
||||
})
|
||||
|
||||
It("should IncrEXFloat saturating overflow", func() {
|
||||
SkipBeforeRedisVersion(8.8, "IncrEX is available since Redis 8.8")
|
||||
|
||||
Expect(client.Set(ctx, "key", "1.8", 0).Err()).NotTo(HaveOccurred())
|
||||
|
||||
res, err := client.IncrEXFloat(ctx, "key", redis.IncrEXFloatArgs{
|
||||
By: 0.7,
|
||||
UBound: 2,
|
||||
HasUBound: true,
|
||||
Saturate: true,
|
||||
}).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res.Value).To(BeNumerically("~", 2.0, 1e-9))
|
||||
Expect(res.AppliedIncrement).To(BeNumerically("~", 0.2, 1e-9))
|
||||
})
|
||||
|
||||
It("should IncrEXInt out-of-bounds default rejects", func() {
|
||||
SkipBeforeRedisVersion(8.8, "IncrEX is available since Redis 8.8")
|
||||
|
||||
Expect(client.Set(ctx, "key", "10", 0).Err()).NotTo(HaveOccurred())
|
||||
|
||||
res, err := client.IncrEXInt(ctx, "key", redis.IncrEXIntArgs{
|
||||
By: 5, HasBy: true,
|
||||
UBound: 12, HasUBound: true,
|
||||
}).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res.Value).To(Equal(int64(10)))
|
||||
Expect(res.AppliedIncrement).To(Equal(int64(0)))
|
||||
|
||||
Expect(client.Get(ctx, "key").Val()).To(Equal("10"))
|
||||
})
|
||||
|
||||
It("should IncrEXInt out-of-bounds with LBOUND default rejects", func() {
|
||||
SkipBeforeRedisVersion(8.8, "IncrEX is available since Redis 8.8")
|
||||
|
||||
Expect(client.Set(ctx, "key", "5", 0).Err()).NotTo(HaveOccurred())
|
||||
|
||||
res, err := client.IncrEXInt(ctx, "key", redis.IncrEXIntArgs{
|
||||
By: 1, HasBy: true,
|
||||
LBound: 10, HasLBound: true,
|
||||
}).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res.Value).To(Equal(int64(5)))
|
||||
Expect(res.AppliedIncrement).To(Equal(int64(0)))
|
||||
|
||||
Expect(client.Get(ctx, "key").Val()).To(Equal("5"))
|
||||
})
|
||||
|
||||
It("should IncrEXInt with EX expiration and ENX", func() {
|
||||
SkipBeforeRedisVersion(8.8, "IncrEX is available since Redis 8.8")
|
||||
|
||||
Expect(client.Set(ctx, "key", "40", 0).Err()).NotTo(HaveOccurred())
|
||||
Expect(client.TTL(ctx, "key").Val()).To(Equal(time.Duration(-1)))
|
||||
|
||||
res, err := client.IncrEXInt(ctx, "key", redis.IncrEXIntArgs{
|
||||
By: 2, HasBy: true,
|
||||
Expiration: &redis.ExpirationOption{Mode: redis.EX, Value: 60},
|
||||
ENX: true,
|
||||
}).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res.Value).To(Equal(int64(42)))
|
||||
Expect(res.AppliedIncrement).To(Equal(int64(2)))
|
||||
|
||||
ttl1 := client.TTL(ctx, "key").Val()
|
||||
Expect(ttl1).To(BeNumerically(">", 0))
|
||||
Expect(ttl1).To(BeNumerically("<=", 60*time.Second))
|
||||
|
||||
// ENX should NOT replace an existing TTL.
|
||||
res, err = client.IncrEXInt(ctx, "key", redis.IncrEXIntArgs{
|
||||
By: 3, HasBy: true,
|
||||
Expiration: &redis.ExpirationOption{Mode: redis.EX, Value: 600},
|
||||
ENX: true,
|
||||
}).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res.Value).To(Equal(int64(45)))
|
||||
|
||||
ttl2 := client.TTL(ctx, "key").Val()
|
||||
Expect(ttl2).To(BeNumerically(">", 0))
|
||||
Expect(ttl2).To(BeNumerically("<=", ttl1))
|
||||
})
|
||||
|
||||
It("should IncrEXInt SATURATE clamps to LBOUND with negative increment", func() {
|
||||
SkipBeforeRedisVersion(8.8, "IncrEX is available since Redis 8.8")
|
||||
|
||||
Expect(client.Set(ctx, "credits", "50", 0).Err()).NotTo(HaveOccurred())
|
||||
|
||||
res, err := client.IncrEXInt(ctx, "credits", redis.IncrEXIntArgs{
|
||||
By: -1, HasBy: true,
|
||||
LBound: 0, HasLBound: true,
|
||||
Saturate: true,
|
||||
}).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res.Value).To(Equal(int64(49)))
|
||||
Expect(res.AppliedIncrement).To(Equal(int64(-1)))
|
||||
})
|
||||
|
||||
It("should IncrEXInt PERSIST removes TTL", func() {
|
||||
SkipBeforeRedisVersion(8.8, "IncrEX is available since Redis 8.8")
|
||||
|
||||
Expect(client.Set(ctx, "key", "1", 60*time.Second).Err()).NotTo(HaveOccurred())
|
||||
Expect(client.TTL(ctx, "key").Val()).To(BeNumerically(">", 0))
|
||||
|
||||
_, err := client.IncrEXInt(ctx, "key", redis.IncrEXIntArgs{
|
||||
By: 1, HasBy: true,
|
||||
Expiration: &redis.ExpirationOption{Mode: redis.PERSIST},
|
||||
}).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TTL(ctx, "key").Val()).To(Equal(time.Duration(-1)))
|
||||
})
|
||||
|
||||
It("should IncrEXInt SATURATE without bounds clamps to LLONG_MAX", func() {
|
||||
SkipBeforeRedisVersion(8.8, "IncrEX is available since Redis 8.8")
|
||||
|
||||
// Start near LLONG_MAX; with no UBOUND the server should clamp to LLONG_MAX.
|
||||
Expect(client.Set(ctx, "key", "9223372036854775800", 0).Err()).NotTo(HaveOccurred())
|
||||
|
||||
res, err := client.IncrEXInt(ctx, "key", redis.IncrEXIntArgs{
|
||||
By: 1000, HasBy: true,
|
||||
Saturate: true,
|
||||
}).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res.Value).To(Equal(int64(math.MaxInt64)))
|
||||
Expect(res.AppliedIncrement).To(Equal(int64(math.MaxInt64 - 9223372036854775800)))
|
||||
})
|
||||
|
||||
It("should IncrEXInt default reject leaves TTL untouched", func() {
|
||||
SkipBeforeRedisVersion(8.8, "IncrEX is available since Redis 8.8")
|
||||
|
||||
Expect(client.Set(ctx, "key", "10", 60*time.Second).Err()).NotTo(HaveOccurred())
|
||||
ttlBefore := client.TTL(ctx, "key").Val()
|
||||
Expect(ttlBefore).To(BeNumerically(">", 0))
|
||||
|
||||
// Out-of-bounds increment with an EX clause — server should reject
|
||||
// and NOT apply the new TTL.
|
||||
res, err := client.IncrEXInt(ctx, "key", redis.IncrEXIntArgs{
|
||||
By: 5, HasBy: true,
|
||||
UBound: 12, HasUBound: true,
|
||||
Expiration: &redis.ExpirationOption{Mode: redis.EX, Value: 6000},
|
||||
}).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res.Value).To(Equal(int64(10)))
|
||||
Expect(res.AppliedIncrement).To(Equal(int64(0)))
|
||||
|
||||
ttlAfter := client.TTL(ctx, "key").Val()
|
||||
// Original TTL was ~60s; rejected EX 6000s must not have been applied.
|
||||
Expect(ttlAfter).To(BeNumerically("<=", ttlBefore))
|
||||
Expect(ttlAfter).To(BeNumerically(">", 0))
|
||||
Expect(ttlAfter).To(BeNumerically("<=", 60*time.Second))
|
||||
})
|
||||
|
||||
It("should IncrEXInt on non-numeric key returns error", func() {
|
||||
SkipBeforeRedisVersion(8.8, "IncrEX is available since Redis 8.8")
|
||||
|
||||
Expect(client.Set(ctx, "key", "not-a-number", 0).Err()).NotTo(HaveOccurred())
|
||||
|
||||
_, err := client.IncrEXInt(ctx, "key", redis.IncrEXIntArgs{
|
||||
By: 1, HasBy: true,
|
||||
}).Result()
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(client.Get(ctx, "key").Val()).To(Equal("not-a-number"))
|
||||
})
|
||||
|
||||
It("should MSetMGet", func() {
|
||||
mSet := client.MSet(ctx, "key1", "hello1", "key2", "hello2")
|
||||
Expect(mSet.Err()).NotTo(HaveOccurred())
|
||||
@@ -6541,17 +6744,20 @@ var _ = Describe("Commands", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
vals, err := client.ZRevRangeByScore(
|
||||
ctx, "zset", &redis.ZRangeBy{Max: "+inf", Min: "-inf"}).Result()
|
||||
ctx, "zset", &redis.ZRangeBy{Max: "+inf", Min: "-inf"},
|
||||
).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(vals).To(Equal([]string{"three", "two", "one"}))
|
||||
|
||||
vals, err = client.ZRevRangeByScore(
|
||||
ctx, "zset", &redis.ZRangeBy{Max: "2", Min: "(1"}).Result()
|
||||
ctx, "zset", &redis.ZRangeBy{Max: "2", Min: "(1"},
|
||||
).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(vals).To(Equal([]string{"two"}))
|
||||
|
||||
vals, err = client.ZRevRangeByScore(
|
||||
ctx, "zset", &redis.ZRangeBy{Max: "(2", Min: "(1"}).Result()
|
||||
ctx, "zset", &redis.ZRangeBy{Max: "(2", Min: "(1"},
|
||||
).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(vals).To(Equal([]string{}))
|
||||
})
|
||||
@@ -6565,17 +6771,20 @@ var _ = Describe("Commands", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
vals, err := client.ZRevRangeByLex(
|
||||
ctx, "zset", &redis.ZRangeBy{Max: "+", Min: "-"}).Result()
|
||||
ctx, "zset", &redis.ZRangeBy{Max: "+", Min: "-"},
|
||||
).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(vals).To(Equal([]string{"c", "b", "a"}))
|
||||
|
||||
vals, err = client.ZRevRangeByLex(
|
||||
ctx, "zset", &redis.ZRangeBy{Max: "[b", Min: "(a"}).Result()
|
||||
ctx, "zset", &redis.ZRangeBy{Max: "[b", Min: "(a"},
|
||||
).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(vals).To(Equal([]string{"b"}))
|
||||
|
||||
vals, err = client.ZRevRangeByLex(
|
||||
ctx, "zset", &redis.ZRangeBy{Max: "(b", Min: "(a"}).Result()
|
||||
ctx, "zset", &redis.ZRangeBy{Max: "(b", Min: "(a"},
|
||||
).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(vals).To(Equal([]string{}))
|
||||
})
|
||||
@@ -6589,7 +6798,8 @@ var _ = Describe("Commands", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
vals, err := client.ZRevRangeByScoreWithScores(
|
||||
ctx, "zset", &redis.ZRangeBy{Max: "+inf", Min: "-inf"}).Result()
|
||||
ctx, "zset", &redis.ZRangeBy{Max: "+inf", Min: "-inf"},
|
||||
).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(vals).To(Equal([]redis.Z{{
|
||||
Score: 3,
|
||||
@@ -6612,7 +6822,8 @@ var _ = Describe("Commands", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
vals, err := client.ZRevRangeByScoreWithScores(
|
||||
ctx, "zset", &redis.ZRangeBy{Max: "+inf", Min: "-inf"}).Result()
|
||||
ctx, "zset", &redis.ZRangeBy{Max: "+inf", Min: "-inf"},
|
||||
).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(vals).To(Equal([]redis.Z{{
|
||||
Score: 3,
|
||||
@@ -6626,12 +6837,14 @@ var _ = Describe("Commands", func() {
|
||||
}}))
|
||||
|
||||
vals, err = client.ZRevRangeByScoreWithScores(
|
||||
ctx, "zset", &redis.ZRangeBy{Max: "2", Min: "(1"}).Result()
|
||||
ctx, "zset", &redis.ZRangeBy{Max: "2", Min: "(1"},
|
||||
).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(vals).To(Equal([]redis.Z{{Score: 2, Member: "two"}}))
|
||||
|
||||
vals, err = client.ZRevRangeByScoreWithScores(
|
||||
ctx, "zset", &redis.ZRangeBy{Max: "(2", Min: "(1"}).Result()
|
||||
ctx, "zset", &redis.ZRangeBy{Max: "(2", Min: "(1"},
|
||||
).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(vals).To(Equal([]redis.Z{}))
|
||||
})
|
||||
@@ -7723,7 +7936,6 @@ var _ = Describe("Commands", func() {
|
||||
Values: map[string]interface{}{"tres": "troix"},
|
||||
}}))
|
||||
Expect(ids).To(Equal([]string{"2-0"}))
|
||||
|
||||
})
|
||||
|
||||
It("should XClaim", func() {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
|
||||
x-default-image: &default-image ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.8-rc1}
|
||||
x-default-image: &default-image ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:custom-26172898734-debian}
|
||||
|
||||
services:
|
||||
redis:
|
||||
|
||||
@@ -3,6 +3,7 @@ package redis
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -20,6 +21,8 @@ type StringCmdable interface {
|
||||
Incr(ctx context.Context, key string) *IntCmd
|
||||
IncrBy(ctx context.Context, key string, value int64) *IntCmd
|
||||
IncrByFloat(ctx context.Context, key string, value float64) *FloatCmd
|
||||
IncrEXInt(ctx context.Context, key string, args IncrEXIntArgs) *IncrEXIntCmd
|
||||
IncrEXFloat(ctx context.Context, key string, args IncrEXFloatArgs) *IncrEXFloatCmd
|
||||
LCS(ctx context.Context, q *LCSQuery) *LCSCmd
|
||||
MGet(ctx context.Context, keys ...string) *SliceCmd
|
||||
MSet(ctx context.Context, values ...interface{}) *StatusCmd
|
||||
@@ -197,6 +200,129 @@ func (c cmdable) IncrByFloat(ctx context.Context, key string, value float64) *Fl
|
||||
return cmd
|
||||
}
|
||||
|
||||
// IncrEXIntArgs are the arguments to IncrEXInt (the BYINT variant of INCREX).
|
||||
//
|
||||
// If By is zero and HasBy is false, the server increments by 1.
|
||||
// HasLBound/HasUBound gate the optional LBOUND/UBOUND clauses so that 0 is a
|
||||
// valid bound. Expiration is shared with the SET command via ExpirationOption.
|
||||
type IncrEXIntArgs struct {
|
||||
By int64
|
||||
HasBy bool
|
||||
|
||||
LBound, UBound int64
|
||||
HasLBound, HasUBound bool
|
||||
|
||||
// Saturate clamps the result to LBOUND/UBOUND (or LLONG_MAX/MIN when no
|
||||
// explicit bound is given) when the increment would exceed it. Without
|
||||
// this flag, out-of-bounds operations are rejected: the key and TTL are
|
||||
// left unchanged and the reply is [current_value, 0].
|
||||
Saturate bool
|
||||
|
||||
// Expiration sets the TTL semantics: EX, PX, EXAT, PXAT, or PERSIST.
|
||||
Expiration *ExpirationOption
|
||||
|
||||
// ENX applies the expiration only when the key does not already have an
|
||||
// expiration. Requires Expiration to set one of EX/PX/EXAT/PXAT.
|
||||
ENX bool
|
||||
}
|
||||
|
||||
// IncrEXFloatArgs are the arguments to IncrEXFloat (the BYFLOAT variant of
|
||||
// INCREX). BYFLOAT is always sent — even when By is zero — to keep the
|
||||
// operation in float mode on the server side; omitting BYFLOAT would cause
|
||||
// the server to treat the call as an integer increment by 1.
|
||||
// HasLBound/HasUBound gate the optional LBOUND/UBOUND clauses so that 0 is
|
||||
// a valid bound.
|
||||
type IncrEXFloatArgs struct {
|
||||
By float64
|
||||
|
||||
LBound, UBound float64
|
||||
HasLBound, HasUBound bool
|
||||
|
||||
// Saturate clamps the result to LBOUND/UBOUND (or ±LDBL_MAX when no
|
||||
// explicit bound is given) when the increment would exceed it. Without
|
||||
// this flag, out-of-bounds operations are rejected: the key and TTL are
|
||||
// left unchanged and the reply is [current_value, 0].
|
||||
Saturate bool
|
||||
|
||||
Expiration *ExpirationOption
|
||||
|
||||
ENX bool
|
||||
}
|
||||
|
||||
// IncrEXInt Redis `INCREX key [BYINT amount] [LBOUND value] [UBOUND value]
|
||||
// [SATURATE] [EX seconds | PX ms | EXAT ts | PXAT ts | PERSIST] [ENX]`
|
||||
// command.
|
||||
//
|
||||
// Atomically increments the integer value stored at key, optionally
|
||||
// constraining the result to a range and applying expiration semantics.
|
||||
// Returns the new value and the increment that was actually applied. When
|
||||
// the increment would exceed LBOUND/UBOUND and SATURATE is not set, the key
|
||||
// and TTL are left unchanged and the reply is [current_value, 0].
|
||||
//
|
||||
// Available since Redis 8.8.
|
||||
// For more information, see https://redis.io/commands/increx
|
||||
func (c cmdable) IncrEXInt(ctx context.Context, key string, a IncrEXIntArgs) *IncrEXIntCmd {
|
||||
args := make([]interface{}, 0, 14)
|
||||
args = append(args, "increx", key)
|
||||
if a.HasBy {
|
||||
args = append(args, "byint", a.By)
|
||||
}
|
||||
if a.HasLBound {
|
||||
args = append(args, "lbound", a.LBound)
|
||||
}
|
||||
if a.HasUBound {
|
||||
args = append(args, "ubound", a.UBound)
|
||||
}
|
||||
if a.Saturate {
|
||||
args = append(args, "saturate")
|
||||
}
|
||||
args = appendIncrEXTail(args, a.Expiration, a.ENX)
|
||||
|
||||
cmd := NewIncrEXIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// IncrEXFloat Redis `INCREX key [BYFLOAT amount] [LBOUND value] [UBOUND value]
|
||||
// [SATURATE] [EX seconds | PX ms | EXAT ts | PXAT ts | PERSIST] [ENX]`
|
||||
// command.
|
||||
//
|
||||
// Available since Redis 8.8.
|
||||
// For more information, see https://redis.io/commands/increx
|
||||
func (c cmdable) IncrEXFloat(ctx context.Context, key string, a IncrEXFloatArgs) *IncrEXFloatCmd {
|
||||
args := make([]interface{}, 0, 14)
|
||||
args = append(args, "increx", key, "byfloat", a.By)
|
||||
if a.HasLBound {
|
||||
args = append(args, "lbound", a.LBound)
|
||||
}
|
||||
if a.HasUBound {
|
||||
args = append(args, "ubound", a.UBound)
|
||||
}
|
||||
if a.Saturate {
|
||||
args = append(args, "saturate")
|
||||
}
|
||||
args = appendIncrEXTail(args, a.Expiration, a.ENX)
|
||||
|
||||
cmd := NewIncrEXFloatCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func appendIncrEXTail(args []interface{}, exp *ExpirationOption, enx bool) []interface{} {
|
||||
if exp != nil {
|
||||
switch exp.Mode {
|
||||
case EX, PX, EXAT, PXAT:
|
||||
args = append(args, strings.ToLower(string(exp.Mode)), exp.Value)
|
||||
case PERSIST:
|
||||
args = append(args, "persist")
|
||||
}
|
||||
}
|
||||
if enx {
|
||||
args = append(args, "enx")
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
type SetCondition string
|
||||
|
||||
const (
|
||||
@@ -219,6 +345,8 @@ const (
|
||||
PXAT ExpirationMode = "PXAT"
|
||||
// KEEPTTL keeps the existing TTL
|
||||
KEEPTTL ExpirationMode = "KEEPTTL"
|
||||
// PERSIST removes the existing TTL. Used by INCREX.
|
||||
PERSIST ExpirationMode = "PERSIST"
|
||||
)
|
||||
|
||||
type ExpirationOption struct {
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// captureCmdable returns a cmdable that records the Cmder passed to it without
|
||||
// dispatching the command. Used to assert that command builders produce the
|
||||
// expected argument list.
|
||||
func captureCmdable(out *Cmder) cmdable {
|
||||
return func(_ context.Context, cmd Cmder) error {
|
||||
*out = cmd
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncrEXInt_Args(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args IncrEXIntArgs
|
||||
want []interface{}
|
||||
}{
|
||||
{
|
||||
name: "default",
|
||||
args: IncrEXIntArgs{},
|
||||
want: []interface{}{"increx", "key"},
|
||||
},
|
||||
{
|
||||
name: "byint",
|
||||
args: IncrEXIntArgs{By: 4, HasBy: true},
|
||||
want: []interface{}{"increx", "key", "byint", int64(4)},
|
||||
},
|
||||
{
|
||||
name: "byint_zero_explicit",
|
||||
args: IncrEXIntArgs{By: 0, HasBy: true},
|
||||
want: []interface{}{"increx", "key", "byint", int64(0)},
|
||||
},
|
||||
{
|
||||
name: "lbound_ubound",
|
||||
args: IncrEXIntArgs{
|
||||
By: 2, HasBy: true,
|
||||
LBound: 0, HasLBound: true,
|
||||
UBound: 20, HasUBound: true,
|
||||
},
|
||||
want: []interface{}{"increx", "key", "byint", int64(2), "lbound", int64(0), "ubound", int64(20)},
|
||||
},
|
||||
{
|
||||
name: "lbound_zero_is_sent",
|
||||
args: IncrEXIntArgs{
|
||||
LBound: 0, HasLBound: true,
|
||||
},
|
||||
want: []interface{}{"increx", "key", "lbound", int64(0)},
|
||||
},
|
||||
{
|
||||
name: "ex_and_enx",
|
||||
args: IncrEXIntArgs{
|
||||
By: 2, HasBy: true,
|
||||
Expiration: &ExpirationOption{Mode: EX, Value: 60},
|
||||
ENX: true,
|
||||
},
|
||||
want: []interface{}{"increx", "key", "byint", int64(2), "ex", int64(60), "enx"},
|
||||
},
|
||||
{
|
||||
name: "px",
|
||||
args: IncrEXIntArgs{Expiration: &ExpirationOption{Mode: PX, Value: 1500}},
|
||||
want: []interface{}{"increx", "key", "px", int64(1500)},
|
||||
},
|
||||
{
|
||||
name: "exat",
|
||||
args: IncrEXIntArgs{Expiration: &ExpirationOption{Mode: EXAT, Value: 1753265054}},
|
||||
want: []interface{}{"increx", "key", "exat", int64(1753265054)},
|
||||
},
|
||||
{
|
||||
name: "pxat",
|
||||
args: IncrEXIntArgs{Expiration: &ExpirationOption{Mode: PXAT, Value: 1753265054014}},
|
||||
want: []interface{}{"increx", "key", "pxat", int64(1753265054014)},
|
||||
},
|
||||
{
|
||||
name: "persist",
|
||||
args: IncrEXIntArgs{Expiration: &ExpirationOption{Mode: PERSIST}},
|
||||
want: []interface{}{"increx", "key", "persist"},
|
||||
},
|
||||
{
|
||||
name: "saturate_with_lbound",
|
||||
args: IncrEXIntArgs{
|
||||
By: 1, HasBy: true,
|
||||
LBound: 10, HasLBound: true,
|
||||
Saturate: true,
|
||||
},
|
||||
want: []interface{}{"increx", "key", "byint", int64(1), "lbound", int64(10), "saturate"},
|
||||
},
|
||||
{
|
||||
name: "saturate_with_ubound",
|
||||
args: IncrEXIntArgs{
|
||||
By: 5, HasBy: true,
|
||||
UBound: 12, HasUBound: true,
|
||||
Saturate: true,
|
||||
},
|
||||
want: []interface{}{"increx", "key", "byint", int64(5), "ubound", int64(12), "saturate"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var captured Cmder
|
||||
c := captureCmdable(&captured)
|
||||
cmd := c.IncrEXInt(context.Background(), "key", tt.args)
|
||||
if cmd == nil {
|
||||
t.Fatalf("IncrEXInt returned nil")
|
||||
}
|
||||
if !reflect.DeepEqual(cmd.Args(), tt.want) {
|
||||
t.Errorf("args mismatch\n got: %#v\nwant: %#v", cmd.Args(), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncrEXFloat_Args(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args IncrEXFloatArgs
|
||||
want []interface{}
|
||||
}{
|
||||
{
|
||||
name: "default_zero_by",
|
||||
args: IncrEXFloatArgs{},
|
||||
want: []interface{}{"increx", "key", "byfloat", float64(0)},
|
||||
},
|
||||
{
|
||||
name: "byfloat",
|
||||
args: IncrEXFloatArgs{By: 0.25},
|
||||
want: []interface{}{"increx", "key", "byfloat", 0.25},
|
||||
},
|
||||
{
|
||||
name: "bounds_and_saturate",
|
||||
args: IncrEXFloatArgs{
|
||||
By: 0.7,
|
||||
UBound: 2, HasUBound: true,
|
||||
Saturate: true,
|
||||
},
|
||||
want: []interface{}{"increx", "key", "byfloat", 0.7, "ubound", float64(2), "saturate"},
|
||||
},
|
||||
{
|
||||
name: "lbound_ubound",
|
||||
args: IncrEXFloatArgs{
|
||||
By: 0.5,
|
||||
LBound: -1.5, HasLBound: true,
|
||||
UBound: 1.5, HasUBound: true,
|
||||
},
|
||||
want: []interface{}{"increx", "key", "byfloat", 0.5, "lbound", -1.5, "ubound", 1.5},
|
||||
},
|
||||
{
|
||||
name: "ex_and_enx",
|
||||
args: IncrEXFloatArgs{
|
||||
By: 1.5,
|
||||
Expiration: &ExpirationOption{Mode: EX, Value: 30},
|
||||
ENX: true,
|
||||
},
|
||||
want: []interface{}{"increx", "key", "byfloat", 1.5, "ex", int64(30), "enx"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var captured Cmder
|
||||
c := captureCmdable(&captured)
|
||||
cmd := c.IncrEXFloat(context.Background(), "key", tt.args)
|
||||
if cmd == nil {
|
||||
t.Fatalf("IncrEXFloat returned nil")
|
||||
}
|
||||
if !reflect.DeepEqual(cmd.Args(), tt.want) {
|
||||
t.Errorf("args mismatch\n got: %#v\nwant: %#v", cmd.Args(), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user