diff --git a/commands_test.go b/commands_test.go index 681fe470..60462170 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,155 @@ 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") + // Setup: create a hash with three fields. + 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()) + // Depending on your implementation, missing fields may return nil or empty strings. + // Adjust the expectation accordingly. + 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") + // Setup: create a hash. + err := client.HSet(ctx, "myhash", "f1", "val1", "f2", "val2").Err() + Expect(err).NotTo(HaveOccurred()) + + // Call HGETEX with EX option and 60 seconds TTL. + res, err := client.HGetEXWithArgs(ctx, "myhash", redis.HGetEXExpirationEX, 60, "f1", "f2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]string{"val1", "val2"})) + // Optionally, verify TTL if your implementation exposes it. + }) + + It("should HGETEX with PERSIST option", Label("hash", "HGETEX"), func() { + SkipBeforeRedisVersion(8, "requires Redis 8.x") + // Setup: create a hash. + err := client.HSet(ctx, "myhash", "f1", "val1", "f2", "val2").Err() + Expect(err).NotTo(HaveOccurred()) + + // Call HGETEX with PERSIST (no TTL value needed). + res, err := client.HGetEXWithArgs(ctx, "myhash", redis.HGetEXExpirationPERSIST, 0, "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(8, "requires Redis 8.x") + // Setup: create a hash. + 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() + res, err := client.HGetEXWithArgs(ctx, "myhash", redis.HGetEXExpirationEXAT, expireAt, "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(8, "requires Redis 8.x") + // Ensure the key is removed. + client.Del(ctx, "myhash") + + // FNX: set field only if it does not exist. + opt := redis.HSetXOptions{ + Condition: redis.HSetEXFNX, + ExpirationType: redis.HSetEXExpirationEX, + ExpirationVal: 60, + } + res, err := client.HSetEXWithArgs(ctx, "myhash", &opt, "f1", "val1").Result() + Expect(err).NotTo(HaveOccurred()) + // Expect the field to be set. + Expect(res).To(Equal(int64(1))) + + opt = redis.HSetXOptions{ + Condition: redis.HSetEXFNX, + ExpirationType: redis.HSetEXExpirationEX, + ExpirationVal: 60, + } + // Attempt to set the same field again with FNX. + res, err = client.HSetEXWithArgs(ctx, "myhash", &opt, "f1", "val2").Result() + Expect(err).NotTo(HaveOccurred()) + // Since the field already exists, no update occurs. + Expect(res).To(Equal(int64(0))) + }) + + It("should HSETEX with FXX condition", Label("hash", "HSETEX"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + // Setup: ensure field f2 exists. + client.Del(ctx, "myhash") + err := client.HSet(ctx, "myhash", "f2", "val1").Err() + Expect(err).NotTo(HaveOccurred()) + + opt := redis.HSetXOptions{ + Condition: redis.HSetEXFXX, + ExpirationType: redis.HSetEXExpirationEX, + ExpirationVal: 60, + } + // FXX: update field only if it exists. + res, err := client.HSetEXWithArgs(ctx, "myhash", &opt, "f2", "val2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(int64(1))) + opt = redis.HSetXOptions{ + Condition: redis.HSetEXFXX, + ExpirationType: redis.HSetEXExpirationEX, + ExpirationVal: 60, + } + // FXX on a non-existing field (f3) should not set the field. + 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(8, "requires Redis 8.x") + // Remove key if it exists. + client.Del(ctx, "myhash") + opt := redis.HSetXOptions{ + ExpirationType: redis.HSetEXExpirationEX, + ExpirationVal: 60, + } + // Set multiple fields at once (no condition). + res, err := client.HSetEXWithArgs(ctx, "myhash", &opt, "f1", "val1", "f2", "val2").Result() + Expect(err).NotTo(HaveOccurred()) + // Assume 1 indicates all fields were set. + Expect(res).To(Equal(int64(1))) + + // Verify that both fields are set. + 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/hash_commands.go b/hash_commands.go index e5bac6f9..74b22d81 100644 --- a/hash_commands.go +++ b/hash_commands.go @@ -21,7 +21,7 @@ type HashCmdable interface { 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 HSetXOptions, fieldsAndValues ...string) *IntCmd + HSetEXWithArgs(ctx context.Context, key string, options *HSetXOptions, 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 @@ -545,7 +545,7 @@ func (c cmdable) HSetEX(ctx context.Context, key string, fieldsAndValues ...stri return cmd } -func (c cmdable) HSetEXWithArgs(ctx context.Context, key string, options HSetXOptions, fieldsAndValues ...string) *IntCmd { +func (c cmdable) HSetEXWithArgs(ctx context.Context, key string, options *HSetXOptions, fieldsAndValues ...string) *IntCmd { // Start with the command name and key. args := []interface{}{"HSETEX", key} if options.Condition != "" {