Skip to content

Commit 1505939

Browse files
authored
Support new hash commands: HGETDEL, HGETEX, HSETEX (#3305)
1 parent d29aace commit 1505939

File tree

3 files changed

+257
-4
lines changed

3 files changed

+257
-4
lines changed

commands_test.go

+142-1
Original file line numberDiff line numberDiff line change
@@ -2659,7 +2659,6 @@ var _ = Describe("Commands", func() {
26592659
Expect(res).To(Equal([]int64{1, 1, -2}))
26602660
})
26612661

2662-
26632662
It("should HPExpire", Label("hash-expiration", "NonRedisEnterprise"), func() {
26642663
SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images")
26652664
res, err := client.HPExpire(ctx, "no_such_key", 10*time.Second, "field1", "field2", "field3").Result()
@@ -2812,6 +2811,148 @@ var _ = Describe("Commands", func() {
28122811
Expect(err).NotTo(HaveOccurred())
28132812
Expect(res[0]).To(BeNumerically("~", 10*time.Second.Milliseconds(), 1))
28142813
})
2814+
2815+
It("should HGETDEL", Label("hash", "HGETDEL"), func() {
2816+
SkipBeforeRedisVersion(7.9, "requires Redis 8.x")
2817+
2818+
err := client.HSet(ctx, "myhash", "f1", "val1", "f2", "val2", "f3", "val3").Err()
2819+
Expect(err).NotTo(HaveOccurred())
2820+
2821+
// Execute HGETDEL on fields f1 and f2.
2822+
res, err := client.HGetDel(ctx, "myhash", "f1", "f2").Result()
2823+
Expect(err).NotTo(HaveOccurred())
2824+
// Expect the returned values for f1 and f2.
2825+
Expect(res).To(Equal([]string{"val1", "val2"}))
2826+
2827+
// Verify that f1 and f2 have been deleted, while f3 remains.
2828+
remaining, err := client.HMGet(ctx, "myhash", "f1", "f2", "f3").Result()
2829+
Expect(err).NotTo(HaveOccurred())
2830+
Expect(remaining[0]).To(BeNil())
2831+
Expect(remaining[1]).To(BeNil())
2832+
Expect(remaining[2]).To(Equal("val3"))
2833+
})
2834+
2835+
It("should return nil responses for HGETDEL on non-existent key", Label("hash", "HGETDEL"), func() {
2836+
SkipBeforeRedisVersion(7.9, "requires Redis 8.x")
2837+
// HGETDEL on a key that does not exist.
2838+
res, err := client.HGetDel(ctx, "nonexistent", "f1", "f2").Result()
2839+
Expect(err).To(BeNil())
2840+
Expect(res).To(Equal([]string{"", ""}))
2841+
})
2842+
2843+
// -----------------------------
2844+
// HGETEX with various TTL options
2845+
// -----------------------------
2846+
It("should HGETEX with EX option", Label("hash", "HGETEX"), func() {
2847+
SkipBeforeRedisVersion(7.9, "requires Redis 8.x")
2848+
2849+
err := client.HSet(ctx, "myhash", "f1", "val1", "f2", "val2").Err()
2850+
Expect(err).NotTo(HaveOccurred())
2851+
2852+
// Call HGETEX with EX option and 60 seconds TTL.
2853+
opt := redis.HGetEXOptions{
2854+
ExpirationType: redis.HGetEXExpirationEX,
2855+
ExpirationVal: 60,
2856+
}
2857+
res, err := client.HGetEXWithArgs(ctx, "myhash", &opt, "f1", "f2").Result()
2858+
Expect(err).NotTo(HaveOccurred())
2859+
Expect(res).To(Equal([]string{"val1", "val2"}))
2860+
})
2861+
2862+
It("should HGETEX with PERSIST option", Label("hash", "HGETEX"), func() {
2863+
SkipBeforeRedisVersion(7.9, "requires Redis 8.x")
2864+
2865+
err := client.HSet(ctx, "myhash", "f1", "val1", "f2", "val2").Err()
2866+
Expect(err).NotTo(HaveOccurred())
2867+
2868+
// Call HGETEX with PERSIST (no TTL value needed).
2869+
opt := redis.HGetEXOptions{ExpirationType: redis.HGetEXExpirationPERSIST}
2870+
res, err := client.HGetEXWithArgs(ctx, "myhash", &opt, "f1", "f2").Result()
2871+
Expect(err).NotTo(HaveOccurred())
2872+
Expect(res).To(Equal([]string{"val1", "val2"}))
2873+
})
2874+
2875+
It("should HGETEX with EXAT option", Label("hash", "HGETEX"), func() {
2876+
SkipBeforeRedisVersion(7.9, "requires Redis 8.x")
2877+
2878+
err := client.HSet(ctx, "myhash", "f1", "val1", "f2", "val2").Err()
2879+
Expect(err).NotTo(HaveOccurred())
2880+
2881+
// Set expiration at a specific Unix timestamp (60 seconds from now).
2882+
expireAt := time.Now().Add(60 * time.Second).Unix()
2883+
opt := redis.HGetEXOptions{
2884+
ExpirationType: redis.HGetEXExpirationEXAT,
2885+
ExpirationVal: expireAt,
2886+
}
2887+
res, err := client.HGetEXWithArgs(ctx, "myhash", &opt, "f1", "f2").Result()
2888+
Expect(err).NotTo(HaveOccurred())
2889+
Expect(res).To(Equal([]string{"val1", "val2"}))
2890+
})
2891+
2892+
// -----------------------------
2893+
// HSETEX with FNX/FXX options
2894+
// -----------------------------
2895+
It("should HSETEX with FNX condition", Label("hash", "HSETEX"), func() {
2896+
SkipBeforeRedisVersion(7.9, "requires Redis 8.x")
2897+
2898+
opt := redis.HSetEXOptions{
2899+
Condition: redis.HSetEXFNX,
2900+
ExpirationType: redis.HSetEXExpirationEX,
2901+
ExpirationVal: 60,
2902+
}
2903+
res, err := client.HSetEXWithArgs(ctx, "myhash", &opt, "f1", "val1").Result()
2904+
Expect(err).NotTo(HaveOccurred())
2905+
Expect(res).To(Equal(int64(1)))
2906+
2907+
opt = redis.HSetEXOptions{
2908+
Condition: redis.HSetEXFNX,
2909+
ExpirationType: redis.HSetEXExpirationEX,
2910+
ExpirationVal: 60,
2911+
}
2912+
res, err = client.HSetEXWithArgs(ctx, "myhash", &opt, "f1", "val2").Result()
2913+
Expect(err).NotTo(HaveOccurred())
2914+
Expect(res).To(Equal(int64(0)))
2915+
})
2916+
2917+
It("should HSETEX with FXX condition", Label("hash", "HSETEX"), func() {
2918+
SkipBeforeRedisVersion(7.9, "requires Redis 8.x")
2919+
2920+
err := client.HSet(ctx, "myhash", "f2", "val1").Err()
2921+
Expect(err).NotTo(HaveOccurred())
2922+
2923+
opt := redis.HSetEXOptions{
2924+
Condition: redis.HSetEXFXX,
2925+
ExpirationType: redis.HSetEXExpirationEX,
2926+
ExpirationVal: 60,
2927+
}
2928+
res, err := client.HSetEXWithArgs(ctx, "myhash", &opt, "f2", "val2").Result()
2929+
Expect(err).NotTo(HaveOccurred())
2930+
Expect(res).To(Equal(int64(1)))
2931+
opt = redis.HSetEXOptions{
2932+
Condition: redis.HSetEXFXX,
2933+
ExpirationType: redis.HSetEXExpirationEX,
2934+
ExpirationVal: 60,
2935+
}
2936+
res, err = client.HSetEXWithArgs(ctx, "myhash", &opt, "f3", "val3").Result()
2937+
Expect(err).NotTo(HaveOccurred())
2938+
Expect(res).To(Equal(int64(0)))
2939+
})
2940+
2941+
It("should HSETEX with multiple field operations", Label("hash", "HSETEX"), func() {
2942+
SkipBeforeRedisVersion(7.9, "requires Redis 8.x")
2943+
2944+
opt := redis.HSetEXOptions{
2945+
ExpirationType: redis.HSetEXExpirationEX,
2946+
ExpirationVal: 60,
2947+
}
2948+
res, err := client.HSetEXWithArgs(ctx, "myhash", &opt, "f1", "val1", "f2", "val2").Result()
2949+
Expect(err).NotTo(HaveOccurred())
2950+
Expect(res).To(Equal(int64(1)))
2951+
2952+
values, err := client.HMGet(ctx, "myhash", "f1", "f2").Result()
2953+
Expect(err).NotTo(HaveOccurred())
2954+
Expect(values).To(Equal([]interface{}{"val1", "val2"}))
2955+
})
28152956
})
28162957

28172958
Describe("hyperloglog", func() {

example/hset-struct/go.sum

-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
22
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
3-
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
4-
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
53
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
64
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
75
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=

hash_commands.go

+115-1
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,17 @@ type HashCmdable interface {
1010
HExists(ctx context.Context, key, field string) *BoolCmd
1111
HGet(ctx context.Context, key, field string) *StringCmd
1212
HGetAll(ctx context.Context, key string) *MapStringStringCmd
13-
HIncrBy(ctx context.Context, key, field string, incr int64) *IntCmd
13+
HGetDel(ctx context.Context, key string, fields ...string) *StringSliceCmd
14+
HGetEX(ctx context.Context, key string, fields ...string) *StringSliceCmd
15+
HGetEXWithArgs(ctx context.Context, key string, options *HGetEXOptions, fields ...string) *StringSliceCmd
1416
HIncrByFloat(ctx context.Context, key, field string, incr float64) *FloatCmd
1517
HKeys(ctx context.Context, key string) *StringSliceCmd
1618
HLen(ctx context.Context, key string) *IntCmd
1719
HMGet(ctx context.Context, key string, fields ...string) *SliceCmd
1820
HSet(ctx context.Context, key string, values ...interface{}) *IntCmd
1921
HMSet(ctx context.Context, key string, values ...interface{}) *BoolCmd
22+
HSetEX(ctx context.Context, key string, fieldsAndValues ...string) *IntCmd
23+
HSetEXWithArgs(ctx context.Context, key string, options *HSetEXOptions, fieldsAndValues ...string) *IntCmd
2024
HSetNX(ctx context.Context, key, field string, value interface{}) *BoolCmd
2125
HScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd
2226
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
454458
_ = c(ctx, cmd)
455459
return cmd
456460
}
461+
462+
func (c cmdable) HGetDel(ctx context.Context, key string, fields ...string) *StringSliceCmd {
463+
args := []interface{}{"HGETDEL", key, "FIELDS", len(fields)}
464+
for _, field := range fields {
465+
args = append(args, field)
466+
}
467+
cmd := NewStringSliceCmd(ctx, args...)
468+
_ = c(ctx, cmd)
469+
return cmd
470+
}
471+
472+
func (c cmdable) HGetEX(ctx context.Context, key string, fields ...string) *StringSliceCmd {
473+
args := []interface{}{"HGETEX", key, "FIELDS", len(fields)}
474+
for _, field := range fields {
475+
args = append(args, field)
476+
}
477+
cmd := NewStringSliceCmd(ctx, args...)
478+
_ = c(ctx, cmd)
479+
return cmd
480+
}
481+
482+
// ExpirationType represents an expiration option for the HGETEX command.
483+
type HGetEXExpirationType string
484+
485+
const (
486+
HGetEXExpirationEX HGetEXExpirationType = "EX"
487+
HGetEXExpirationPX HGetEXExpirationType = "PX"
488+
HGetEXExpirationEXAT HGetEXExpirationType = "EXAT"
489+
HGetEXExpirationPXAT HGetEXExpirationType = "PXAT"
490+
HGetEXExpirationPERSIST HGetEXExpirationType = "PERSIST"
491+
)
492+
493+
type HGetEXOptions struct {
494+
ExpirationType HGetEXExpirationType
495+
ExpirationVal int64
496+
}
497+
498+
func (c cmdable) HGetEXWithArgs(ctx context.Context, key string, options *HGetEXOptions, fields ...string) *StringSliceCmd {
499+
args := []interface{}{"HGETEX", key}
500+
if options.ExpirationType != "" {
501+
args = append(args, string(options.ExpirationType))
502+
if options.ExpirationType != HGetEXExpirationPERSIST {
503+
args = append(args, options.ExpirationVal)
504+
}
505+
}
506+
507+
args = append(args, "FIELDS", len(fields))
508+
for _, field := range fields {
509+
args = append(args, field)
510+
}
511+
512+
cmd := NewStringSliceCmd(ctx, args...)
513+
_ = c(ctx, cmd)
514+
return cmd
515+
}
516+
517+
type HSetEXCondition string
518+
519+
const (
520+
HSetEXFNX HSetEXCondition = "FNX" // Only set the fields if none of them already exist.
521+
HSetEXFXX HSetEXCondition = "FXX" // Only set the fields if all already exist.
522+
)
523+
524+
type HSetEXExpirationType string
525+
526+
const (
527+
HSetEXExpirationEX HSetEXExpirationType = "EX"
528+
HSetEXExpirationPX HSetEXExpirationType = "PX"
529+
HSetEXExpirationEXAT HSetEXExpirationType = "EXAT"
530+
HSetEXExpirationPXAT HSetEXExpirationType = "PXAT"
531+
HSetEXExpirationKEEPTTL HSetEXExpirationType = "KEEPTTL"
532+
)
533+
534+
type HSetEXOptions struct {
535+
Condition HSetEXCondition
536+
ExpirationType HSetEXExpirationType
537+
ExpirationVal int64
538+
}
539+
540+
func (c cmdable) HSetEX(ctx context.Context, key string, fieldsAndValues ...string) *IntCmd {
541+
args := []interface{}{"HSETEX", key, "FIELDS", len(fieldsAndValues) / 2}
542+
for _, field := range fieldsAndValues {
543+
args = append(args, field)
544+
}
545+
546+
cmd := NewIntCmd(ctx, args...)
547+
_ = c(ctx, cmd)
548+
return cmd
549+
}
550+
551+
func (c cmdable) HSetEXWithArgs(ctx context.Context, key string, options *HSetEXOptions, fieldsAndValues ...string) *IntCmd {
552+
args := []interface{}{"HSETEX", key}
553+
if options.Condition != "" {
554+
args = append(args, string(options.Condition))
555+
}
556+
if options.ExpirationType != "" {
557+
args = append(args, string(options.ExpirationType))
558+
if options.ExpirationType != HSetEXExpirationKEEPTTL {
559+
args = append(args, options.ExpirationVal)
560+
}
561+
}
562+
args = append(args, "FIELDS", len(fieldsAndValues)/2)
563+
for _, field := range fieldsAndValues {
564+
args = append(args, field)
565+
}
566+
567+
cmd := NewIntCmd(ctx, args...)
568+
_ = c(ctx, cmd)
569+
return cmd
570+
}

0 commit comments

Comments
 (0)