diff --git a/commands_test.go b/commands_test.go index 681fe470..a9e90fc9 100644 --- a/commands_test.go +++ b/commands_test.go @@ -2659,7 +2659,6 @@ var _ = Describe("Commands", func() { Expect(res).To(Equal([]int64{1, 1, -2})) }) - It("should HPExpire", Label("hash-expiration", "NonRedisEnterprise"), func() { SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") res, err := client.HPExpire(ctx, "no_such_key", 10*time.Second, "field1", "field2", "field3").Result() @@ -2812,6 +2811,148 @@ var _ = Describe("Commands", func() { Expect(err).NotTo(HaveOccurred()) Expect(res[0]).To(BeNumerically("~", 10*time.Second.Milliseconds(), 1)) }) + + It("should HGETDEL", Label("hash", "HGETDEL"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + + err := client.HSet(ctx, "myhash", "f1", "val1", "f2", "val2", "f3", "val3").Err() + Expect(err).NotTo(HaveOccurred()) + + // Execute HGETDEL on fields f1 and f2. + res, err := client.HGetDel(ctx, "myhash", "f1", "f2").Result() + Expect(err).NotTo(HaveOccurred()) + // Expect the returned values for f1 and f2. + Expect(res).To(Equal([]string{"val1", "val2"})) + + // Verify that f1 and f2 have been deleted, while f3 remains. + remaining, err := client.HMGet(ctx, "myhash", "f1", "f2", "f3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(remaining[0]).To(BeNil()) + Expect(remaining[1]).To(BeNil()) + Expect(remaining[2]).To(Equal("val3")) + }) + + It("should return nil responses for HGETDEL on non-existent key", Label("hash", "HGETDEL"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + // HGETDEL on a key that does not exist. + res, err := client.HGetDel(ctx, "nonexistent", "f1", "f2").Result() + Expect(err).To(BeNil()) + Expect(res).To(Equal([]string{"", ""})) + }) + + // ----------------------------- + // HGETEX with various TTL options + // ----------------------------- + It("should HGETEX with EX option", Label("hash", "HGETEX"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + + err := client.HSet(ctx, "myhash", "f1", "val1", "f2", "val2").Err() + Expect(err).NotTo(HaveOccurred()) + + // Call HGETEX with EX option and 60 seconds TTL. + opt := redis.HGetEXOptions{ + ExpirationType: redis.HGetEXExpirationEX, + ExpirationVal: 60, + } + res, err := client.HGetEXWithArgs(ctx, "myhash", &opt, "f1", "f2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]string{"val1", "val2"})) + }) + + It("should HGETEX with PERSIST option", Label("hash", "HGETEX"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + + err := client.HSet(ctx, "myhash", "f1", "val1", "f2", "val2").Err() + Expect(err).NotTo(HaveOccurred()) + + // Call HGETEX with PERSIST (no TTL value needed). + opt := redis.HGetEXOptions{ExpirationType: redis.HGetEXExpirationPERSIST} + res, err := client.HGetEXWithArgs(ctx, "myhash", &opt, "f1", "f2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]string{"val1", "val2"})) + }) + + It("should HGETEX with EXAT option", Label("hash", "HGETEX"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + + err := client.HSet(ctx, "myhash", "f1", "val1", "f2", "val2").Err() + Expect(err).NotTo(HaveOccurred()) + + // Set expiration at a specific Unix timestamp (60 seconds from now). + expireAt := time.Now().Add(60 * time.Second).Unix() + opt := redis.HGetEXOptions{ + ExpirationType: redis.HGetEXExpirationEXAT, + ExpirationVal: expireAt, + } + res, err := client.HGetEXWithArgs(ctx, "myhash", &opt, "f1", "f2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]string{"val1", "val2"})) + }) + + // ----------------------------- + // HSETEX with FNX/FXX options + // ----------------------------- + It("should HSETEX with FNX condition", Label("hash", "HSETEX"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + + opt := redis.HSetEXOptions{ + Condition: redis.HSetEXFNX, + ExpirationType: redis.HSetEXExpirationEX, + ExpirationVal: 60, + } + res, err := client.HSetEXWithArgs(ctx, "myhash", &opt, "f1", "val1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(int64(1))) + + opt = redis.HSetEXOptions{ + Condition: redis.HSetEXFNX, + ExpirationType: redis.HSetEXExpirationEX, + ExpirationVal: 60, + } + res, err = client.HSetEXWithArgs(ctx, "myhash", &opt, "f1", "val2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(int64(0))) + }) + + It("should HSETEX with FXX condition", Label("hash", "HSETEX"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + + err := client.HSet(ctx, "myhash", "f2", "val1").Err() + Expect(err).NotTo(HaveOccurred()) + + opt := redis.HSetEXOptions{ + Condition: redis.HSetEXFXX, + ExpirationType: redis.HSetEXExpirationEX, + ExpirationVal: 60, + } + res, err := client.HSetEXWithArgs(ctx, "myhash", &opt, "f2", "val2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(int64(1))) + opt = redis.HSetEXOptions{ + Condition: redis.HSetEXFXX, + ExpirationType: redis.HSetEXExpirationEX, + ExpirationVal: 60, + } + res, err = client.HSetEXWithArgs(ctx, "myhash", &opt, "f3", "val3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(int64(0))) + }) + + It("should HSETEX with multiple field operations", Label("hash", "HSETEX"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + + opt := redis.HSetEXOptions{ + ExpirationType: redis.HSetEXExpirationEX, + ExpirationVal: 60, + } + res, err := client.HSetEXWithArgs(ctx, "myhash", &opt, "f1", "val1", "f2", "val2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(int64(1))) + + values, err := client.HMGet(ctx, "myhash", "f1", "f2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(values).To(Equal([]interface{}{"val1", "val2"})) + }) }) Describe("hyperloglog", func() { diff --git a/example/hset-struct/go.sum b/example/hset-struct/go.sum index 1602e702..5496d29e 100644 --- a/example/hset-struct/go.sum +++ b/example/hset-struct/go.sum @@ -1,7 +1,5 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/hash_commands.go b/hash_commands.go index 039d8e07..1f53f344 100644 --- a/hash_commands.go +++ b/hash_commands.go @@ -10,13 +10,17 @@ type HashCmdable interface { HExists(ctx context.Context, key, field string) *BoolCmd HGet(ctx context.Context, key, field string) *StringCmd HGetAll(ctx context.Context, key string) *MapStringStringCmd - HIncrBy(ctx context.Context, key, field string, incr int64) *IntCmd + HGetDel(ctx context.Context, key string, fields ...string) *StringSliceCmd + HGetEX(ctx context.Context, key string, fields ...string) *StringSliceCmd + HGetEXWithArgs(ctx context.Context, key string, options *HGetEXOptions, fields ...string) *StringSliceCmd HIncrByFloat(ctx context.Context, key, field string, incr float64) *FloatCmd HKeys(ctx context.Context, key string) *StringSliceCmd HLen(ctx context.Context, key string) *IntCmd HMGet(ctx context.Context, key string, fields ...string) *SliceCmd HSet(ctx context.Context, key string, values ...interface{}) *IntCmd HMSet(ctx context.Context, key string, values ...interface{}) *BoolCmd + HSetEX(ctx context.Context, key string, fieldsAndValues ...string) *IntCmd + HSetEXWithArgs(ctx context.Context, key string, options *HSetEXOptions, fieldsAndValues ...string) *IntCmd HSetNX(ctx context.Context, key, field string, value interface{}) *BoolCmd HScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd HScanNoValues(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd @@ -454,3 +458,113 @@ func (c cmdable) HPTTL(ctx context.Context, key string, fields ...string) *IntSl _ = c(ctx, cmd) return cmd } + +func (c cmdable) HGetDel(ctx context.Context, key string, fields ...string) *StringSliceCmd { + args := []interface{}{"HGETDEL", key, "FIELDS", len(fields)} + for _, field := range fields { + args = append(args, field) + } + cmd := NewStringSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HGetEX(ctx context.Context, key string, fields ...string) *StringSliceCmd { + args := []interface{}{"HGETEX", key, "FIELDS", len(fields)} + for _, field := range fields { + args = append(args, field) + } + cmd := NewStringSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// ExpirationType represents an expiration option for the HGETEX command. +type HGetEXExpirationType string + +const ( + HGetEXExpirationEX HGetEXExpirationType = "EX" + HGetEXExpirationPX HGetEXExpirationType = "PX" + HGetEXExpirationEXAT HGetEXExpirationType = "EXAT" + HGetEXExpirationPXAT HGetEXExpirationType = "PXAT" + HGetEXExpirationPERSIST HGetEXExpirationType = "PERSIST" +) + +type HGetEXOptions struct { + ExpirationType HGetEXExpirationType + ExpirationVal int64 +} + +func (c cmdable) HGetEXWithArgs(ctx context.Context, key string, options *HGetEXOptions, fields ...string) *StringSliceCmd { + args := []interface{}{"HGETEX", key} + if options.ExpirationType != "" { + args = append(args, string(options.ExpirationType)) + if options.ExpirationType != HGetEXExpirationPERSIST { + args = append(args, options.ExpirationVal) + } + } + + args = append(args, "FIELDS", len(fields)) + for _, field := range fields { + args = append(args, field) + } + + cmd := NewStringSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +type HSetEXCondition string + +const ( + HSetEXFNX HSetEXCondition = "FNX" // Only set the fields if none of them already exist. + HSetEXFXX HSetEXCondition = "FXX" // Only set the fields if all already exist. +) + +type HSetEXExpirationType string + +const ( + HSetEXExpirationEX HSetEXExpirationType = "EX" + HSetEXExpirationPX HSetEXExpirationType = "PX" + HSetEXExpirationEXAT HSetEXExpirationType = "EXAT" + HSetEXExpirationPXAT HSetEXExpirationType = "PXAT" + HSetEXExpirationKEEPTTL HSetEXExpirationType = "KEEPTTL" +) + +type HSetEXOptions struct { + Condition HSetEXCondition + ExpirationType HSetEXExpirationType + ExpirationVal int64 +} + +func (c cmdable) HSetEX(ctx context.Context, key string, fieldsAndValues ...string) *IntCmd { + args := []interface{}{"HSETEX", key, "FIELDS", len(fieldsAndValues) / 2} + for _, field := range fieldsAndValues { + args = append(args, field) + } + + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HSetEXWithArgs(ctx context.Context, key string, options *HSetEXOptions, fieldsAndValues ...string) *IntCmd { + args := []interface{}{"HSETEX", key} + if options.Condition != "" { + args = append(args, string(options.Condition)) + } + if options.ExpirationType != "" { + args = append(args, string(options.ExpirationType)) + if options.ExpirationType != HSetEXExpirationKEEPTTL { + args = append(args, options.ExpirationVal) + } + } + args = append(args, "FIELDS", len(fieldsAndValues)/2) + for _, field := range fieldsAndValues { + args = append(args, field) + } + + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +}