diff --git a/commands_test.go b/commands_test.go index f528b5cee2..c72bd73275 100644 --- a/commands_test.go +++ b/commands_test.go @@ -1935,6 +1935,137 @@ var _ = Describe("Commands", func() { Expect(mSetNX.Val()).To(Equal(true)) }) + It("should MSetEX", func() { + SkipBeforeRedisVersion(8.3, "MSetEX is available since redis 8.4") + args := redis.MSetEXArgs{ + Expiration: &redis.ExpirationOption{ + Mode: redis.EX, + Value: 1, + }, + } + mSetEX := client.MSetEX(ctx, args, "key1", "hello1", "key2", "hello2") + Expect(mSetEX.Err()).NotTo(HaveOccurred()) + Expect(mSetEX.Val()).To(Equal(int64(1))) + + // Verify keys were set + val1 := client.Get(ctx, "key1") + Expect(val1.Err()).NotTo(HaveOccurred()) + Expect(val1.Val()).To(Equal("hello1")) + + val2 := client.Get(ctx, "key2") + Expect(val2.Err()).NotTo(HaveOccurred()) + Expect(val2.Val()).To(Equal("hello2")) + + // Verify TTL was set + ttl1 := client.TTL(ctx, "key1") + Expect(ttl1.Err()).NotTo(HaveOccurred()) + Expect(ttl1.Val()).To(BeNumerically(">", 0)) + Expect(ttl1.Val()).To(BeNumerically("<=", 1*time.Second)) + + ttl2 := client.TTL(ctx, "key2") + Expect(ttl2.Err()).NotTo(HaveOccurred()) + Expect(ttl2.Val()).To(BeNumerically(">", 0)) + Expect(ttl2.Val()).To(BeNumerically("<=", 1*time.Second)) + }) + + It("should MSetEX with NX mode", func() { + SkipBeforeRedisVersion(8.3, "MSetEX is available since redis 8.4") + + client.Set(ctx, "key1", "existing", 0) + + // Try to set with NX mode - should fail because key1 exists + args := redis.MSetEXArgs{ + Condition: redis.NX, + Expiration: &redis.ExpirationOption{ + Mode: redis.EX, + Value: 1, + }, + } + mSetEX := client.MSetEX(ctx, args, "key1", "new1", "key2", "new2") + Expect(mSetEX.Err()).NotTo(HaveOccurred()) + Expect(mSetEX.Val()).To(Equal(int64(0))) + + val1 := client.Get(ctx, "key1") + Expect(val1.Err()).NotTo(HaveOccurred()) + Expect(val1.Val()).To(Equal("existing")) + + val2 := client.Get(ctx, "key2") + Expect(val2.Err()).To(Equal(redis.Nil)) + + client.Del(ctx, "key1") + + // Now try with NX mode when keys don't exist - should succeed + mSetEX = client.MSetEX(ctx, args, "key1", "new1", "key2", "new2") + Expect(mSetEX.Err()).NotTo(HaveOccurred()) + Expect(mSetEX.Val()).To(Equal(int64(1))) + + val1 = client.Get(ctx, "key1") + Expect(val1.Err()).NotTo(HaveOccurred()) + Expect(val1.Val()).To(Equal("new1")) + + val2 = client.Get(ctx, "key2") + Expect(val2.Err()).NotTo(HaveOccurred()) + Expect(val2.Val()).To(Equal("new2")) + }) + + It("should MSetEX with XX mode", func() { + SkipBeforeRedisVersion(8.3, "MSetEX is available since redis 8.4") + + args := redis.MSetEXArgs{ + Condition: redis.XX, + Expiration: &redis.ExpirationOption{ + Mode: redis.EX, + Value: 1, + }, + } + mSetEX := client.MSetEX(ctx, args, "key1", "new1", "key2", "new2") + Expect(mSetEX.Err()).NotTo(HaveOccurred()) + Expect(mSetEX.Val()).To(Equal(int64(0))) + + client.Set(ctx, "key1", "existing1", 0) + client.Set(ctx, "key2", "existing2", 0) + + mSetEX = client.MSetEX(ctx, args, "key1", "new1", "key2", "new2") + Expect(mSetEX.Err()).NotTo(HaveOccurred()) + Expect(mSetEX.Val()).To(Equal(int64(1))) + + val1 := client.Get(ctx, "key1") + Expect(val1.Err()).NotTo(HaveOccurred()) + Expect(val1.Val()).To(Equal("new1")) + + val2 := client.Get(ctx, "key2") + Expect(val2.Err()).NotTo(HaveOccurred()) + Expect(val2.Val()).To(Equal("new2")) + + ttl1 := client.TTL(ctx, "key1") + Expect(ttl1.Err()).NotTo(HaveOccurred()) + Expect(ttl1.Val()).To(BeNumerically(">", 0)) + }) + + It("should MSetEX with map", func() { + SkipBeforeRedisVersion(8.3, "MSetEX is available since redis 8.4") + args := redis.MSetEXArgs{ + Expiration: &redis.ExpirationOption{ + Mode: redis.EX, + Value: 1, + }, + } + mSetEX := client.MSetEX(ctx, args, map[string]interface{}{ + "key1": "value1", + "key2": "value2", + }) + Expect(mSetEX.Err()).NotTo(HaveOccurred()) + Expect(mSetEX.Val()).To(Equal(int64(1))) + + val1 := client.Get(ctx, "key1") + Expect(val1.Err()).NotTo(HaveOccurred()) + Expect(val1.Val()).To(Equal("value1")) + + val2 := client.Get(ctx, "key2") + Expect(val2.Err()).NotTo(HaveOccurred()) + Expect(val2.Val()).To(Equal("value2")) + }) + It("should SetWithArgs with TTL", func() { args := redis.SetArgs{ TTL: 500 * time.Millisecond, diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index d4272c977b..21271aedf9 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -22,4 +22,3 @@ retract ( v9.7.2 // This version was accidentally released. Please use version 9.7.3 instead. v9.5.3 // This version was accidentally released. Please use version 9.6.0 instead. ) - diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index d8c03b6af4..56d6408016 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -19,4 +19,3 @@ retract ( v9.7.2 // This version was accidentally released. Please use version 9.7.3 instead. v9.5.3 // This version was accidentally released. Please use version 9.6.0 instead. ) - diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index 23cec11a2d..ade870c568 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -27,4 +27,3 @@ retract ( v9.7.2 // This version was accidentally released. Please use version 9.7.3 instead. v9.5.3 // This version was accidentally released. Please use version 9.6.0 instead. ) - diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index fd4e2d93ee..d704f9a7c4 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -26,4 +26,3 @@ retract ( v9.7.2 // This version was accidentally released. Please use version 9.7.3 instead. v9.5.3 // This version was accidentally released. Please use version 9.6.0 instead. ) - diff --git a/string_commands.go b/string_commands.go index eff5880dcd..cc49800d55 100644 --- a/string_commands.go +++ b/string_commands.go @@ -21,6 +21,7 @@ type StringCmdable interface { MGet(ctx context.Context, keys ...string) *SliceCmd MSet(ctx context.Context, values ...interface{}) *StatusCmd MSetNX(ctx context.Context, values ...interface{}) *BoolCmd + MSetEX(ctx context.Context, args MSetEXArgs, values ...interface{}) *IntCmd 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 @@ -112,6 +113,35 @@ func (c cmdable) IncrByFloat(ctx context.Context, key string, value float64) *Fl return cmd } +type SetCondition string + +const ( + // NX only set the keys and their expiration if none exist + NX SetCondition = "NX" + // XX only set the keys and their expiration if all already exist + XX SetCondition = "XX" +) + +type ExpirationMode string + +const ( + // EX sets expiration in seconds + EX ExpirationMode = "EX" + // PX sets expiration in milliseconds + PX ExpirationMode = "PX" + // EXAT sets expiration as Unix timestamp in seconds + EXAT ExpirationMode = "EXAT" + // PXAT sets expiration as Unix timestamp in milliseconds + PXAT ExpirationMode = "PXAT" + // KEEPTTL keeps the existing TTL + KEEPTTL ExpirationMode = "KEEPTTL" +) + +type ExpirationOption struct { + Mode ExpirationMode + Value int64 +} + func (c cmdable) LCS(ctx context.Context, q *LCSQuery) *LCSCmd { cmd := NewLCSCmd(ctx, q) _ = c(ctx, cmd) @@ -157,6 +187,49 @@ func (c cmdable) MSetNX(ctx context.Context, values ...interface{}) *BoolCmd { return cmd } +type MSetEXArgs struct { + Condition SetCondition + Expiration *ExpirationOption +} + +// MSetEX sets the given keys to their respective values. +// This command is an extension of the MSETNX that adds expiration and XX options. +// Available since Redis 8.4 +// Important: When this method is used with Cluster clients, all keys +// must be in the same hash slot, otherwise CROSSSLOT error will be returned. +// For more information, see https://redis.io/commands/msetex +func (c cmdable) MSetEX(ctx context.Context, args MSetEXArgs, values ...interface{}) *IntCmd { + expandedArgs := appendArgs([]interface{}{}, values) + numkeys := len(expandedArgs) / 2 + + cmdArgs := make([]interface{}, 0, 2+len(expandedArgs)+3) + cmdArgs = append(cmdArgs, "msetex", numkeys) + cmdArgs = append(cmdArgs, expandedArgs...) + + if args.Condition != "" { + cmdArgs = append(cmdArgs, string(args.Condition)) + } + + if args.Expiration != nil { + switch args.Expiration.Mode { + case EX: + cmdArgs = append(cmdArgs, "ex", args.Expiration.Value) + case PX: + cmdArgs = append(cmdArgs, "px", args.Expiration.Value) + case EXAT: + cmdArgs = append(cmdArgs, "exat", args.Expiration.Value) + case PXAT: + cmdArgs = append(cmdArgs, "pxat", args.Expiration.Value) + case KEEPTTL: + cmdArgs = append(cmdArgs, "keepttl") + } + } + + cmd := NewIntCmd(ctx, cmdArgs...) + _ = c(ctx, cmd) + return cmd +} + // Set Redis `SET key value [expiration]` command. // Use expiration for `SETEx`-like behavior. //