Skip to content

Support new hash commands: HGETDEL, HGETEX, HSETEX #3305

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 142 additions & 1 deletion commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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() {
Expand Down
2 changes: 0 additions & 2 deletions example/hset-struct/go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand Down
116 changes: 115 additions & 1 deletion hash_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Loading