Skip to content

Commit

Permalink
added HEXPIRE and HTTL to hash API
Browse files Browse the repository at this point in the history
  • Loading branch information
osteensco committed Nov 18, 2024
1 parent d42a3d1 commit 681666f
Show file tree
Hide file tree
Showing 6 changed files with 4,521 additions and 4,260 deletions.
8,482 changes: 4,246 additions & 4,236 deletions coverage/coverage.out

Large diffs are not rendered by default.

53 changes: 32 additions & 21 deletions internal/modules/hash/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -619,16 +619,6 @@ func handleHEXPIRE(params internal.HandlerFuncParams) ([]byte, error) {
return nil, err
}
key := keys.WriteKeys[0]
keyExists := params.KeysExist(params.Context, keys.WriteKeys)[key]

if !keyExists {
return []byte(":-2\r\n"), nil
}

hash, ok := params.GetValues(params.Context, []string{key})[key].(Hash)
if !ok {
return nil, fmt.Errorf("value at %s is not a hash", key)
}

// HEXPIRE key seconds [NX | XX | GT | LT] FIELDS numfields field
cmdargs := keys.WriteKeys[1:]
Expand Down Expand Up @@ -660,6 +650,20 @@ func handleHEXPIRE(params internal.HandlerFuncParams) ([]byte, error) {
// build out response
resp := "*" + fmt.Sprintf("%v", len(fields)) + "\r\n"

// handle not hash or bad key
keyExists := params.KeysExist(params.Context, keys.WriteKeys)[key]
if !keyExists {
for i := numfields; i > 0; i-- {
resp = resp + ":-2\r\n"
}
return []byte(resp), nil
}

hash, ok := params.GetValues(params.Context, []string{key})[key].(Hash)
if !ok {
return nil, fmt.Errorf("value of key %s is not a hash", key)
}

// handle expire time of 0 seconds
if seconds == 0 {
for i := numfields; i > 0; i-- {
Expand Down Expand Up @@ -753,7 +757,7 @@ func handleHEXPIRE(params internal.HandlerFuncParams) ([]byte, error) {

}
default:
return nil, fmt.Errorf("unknown option %s", strings.ToUpper(params.Command[3]))
return nil, fmt.Errorf("unknown option %s, must be one of 'NX', 'XX', 'GT', 'LT'.", strings.ToUpper(params.Command[3]))
}
} else {
for _, f := range fields {
Expand Down Expand Up @@ -781,26 +785,32 @@ func handleHTTL(params internal.HandlerFuncParams) ([]byte, error) {
if err != nil {
return nil, err
}

cmdargs := keys.ReadKeys[2:]
numfields, err := strconv.ParseInt(cmdargs[0], 10, 64)
if err != nil {
return nil, errors.New(fmt.Sprintf("expire time must be integer, was provided %q", cmdargs[0]))
}

fields := cmdargs[1 : numfields+1]
// init array response
resp := "*" + fmt.Sprintf("%v", len(fields)) + "\r\n"

// handle bad key
key := keys.ReadKeys[0]
keyExists := params.KeysExist(params.Context, keys.ReadKeys)[key]

if !keyExists {
return []byte(":-2\r\n"), nil
resp = resp + ":-2\r\n"
return []byte(resp), nil
}

// handle not a hash
hash, ok := params.GetValues(params.Context, []string{key})[key].(Hash)
if !ok {
return nil, fmt.Errorf("value at %s is not a hash", key)
}

cmdargs := keys.ReadKeys[2:]
numfields, err := strconv.ParseInt(cmdargs[0], 10, 64)
if err != nil {
return nil, errors.New(fmt.Sprintf("expire time must be integer, was provided %q", cmdargs[0]))
}

fields := cmdargs[1 : numfields+1]
resp := "*" + fmt.Sprintf("%v", len(fields)) + "\r\n"
// build out response
for _, field := range fields {
f, ok := hash[field]
if !ok {
Expand All @@ -815,6 +825,7 @@ func handleHTTL(params internal.HandlerFuncParams) ([]byte, error) {

}

// array response
return []byte(resp), nil
}

Expand Down
4 changes: 2 additions & 2 deletions internal/modules/hash/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2119,7 +2119,7 @@ func Test_Hash(t *testing.T) {
key: "HexpireKeyNOTEXIST",
presetValue: nil,
command: []string{"HEXPIRE", "HexpireKeyNOTEXIST", "100", "FIELDS", "1", "HexpireKNEField1"},
expectedValue: "-2",
expectedValue: "[-2]",
expectedError: nil,
},
{
Expand Down Expand Up @@ -2336,7 +2336,7 @@ func Test_Hash(t *testing.T) {
command: []string{"HTTL", "HTTLKeyNOTEXIST", "FIELDS", "1", "HTTLK1Field1"},
presetValue: nil,
setExpire: false,
expectedValue: "-2",
expectedValue: "[-2]",
expectedError: nil,
},
{
Expand Down
2 changes: 2 additions & 0 deletions sugardb/api_generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ const (
// LT - Only set the expiry time if the new expiry time is less than the current one.
//
// NX, GT, and LT are mutually exclusive. XX can additionally be passed in with either GT or LT.
//
// Hash only: NX, XX, GT, and LT are all mutually exclusive.
type ExpireOptions interface {
IsExOpt() ExOpt
}
Expand Down
34 changes: 33 additions & 1 deletion sugardb/api_hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
package sugardb

import (
"github.com/echovault/sugardb/internal"
"fmt"
"strconv"

"github.com/echovault/sugardb/internal"
)

// HRandFieldOptions modifies the behaviour of the HRandField function.
Expand Down Expand Up @@ -354,3 +356,33 @@ func (server *SugarDB) HDel(key string, fields ...string) (int, error) {
}
return internal.ParseIntegerResponse(b)
}

func (server *SugarDB) HExpire(key string, seconds int, ExOpt ExpireOptions, fields ...string) ([]int, error) {
secs := fmt.Sprintf("%v", seconds)
cmd := []string{"HEXPIRE", key, secs}
if ExOpt != nil {
ExpireOption := fmt.Sprintf("%v", ExOpt)
cmd = append(cmd, ExpireOption)
}

numFields := fmt.Sprintf("%v", len(fields))
fieldsArray := append([]string{"FIELDS", numFields}, fields...)

cmd = append(cmd, fieldsArray...)
b, err := server.handleCommand(server.context, internal.EncodeCommand(cmd), nil, false, true)
if err != nil {
return nil, err
}
return internal.ParseIntegerArrayResponse(b)
}

func (server *SugarDB) HTTL(key string, fields ...string) ([]int, error) {
numFields := fmt.Sprintf("%v", len(fields))

cmd := append([]string{"HTTL", key, "FIELDS", numFields}, fields...)
b, err := server.handleCommand(server.context, internal.EncodeCommand(cmd), nil, false, true)
if err != nil {
return nil, err
}
return internal.ParseIntegerArrayResponse(b)
}
206 changes: 206 additions & 0 deletions sugardb/api_hash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"reflect"
"slices"
"testing"
"time"

"github.com/echovault/sugardb/internal/modules/hash"
)
Expand Down Expand Up @@ -934,3 +935,208 @@ func TestSugarDB_HMGet(t *testing.T) {
})
}
}

func TestSugarDB_HExpire(t *testing.T) {
server := createSugarDB()
tests := []struct {
name string
presetValue interface{}
key string
fields []string
expireOption ExpireOptions
want []int
wantErr bool
}{
{
name: "1. Set Expiration from existing hash.",
key: "HExpireKey1",
presetValue: hash.Hash{
"field1": {Value: "value1"},
"field2": {Value: 365},
"field3": {Value: 3.142},
},
fields: []string{"field1", "field2", "field3"},
want: []int{1, 1, 1},
wantErr: false,
},
{
name: "2. Return -2 when attempting to get from non-existed key",
presetValue: nil,
key: "HExpireKey2",
fields: []string{"field1"},
want: []int{-2},
wantErr: false,
},
{
name: "3. Error when trying to get from a value that is not a hash map",
presetValue: "Default Value",
key: "HExpireKey3",
fields: []string{"field1"},
want: nil,
wantErr: true,
},
{
name: "4. Set Expiration with option NX.",
key: "HExpireKey4",
presetValue: hash.Hash{
"field1": {Value: "value1"},
"field2": {Value: 365},
"field3": {Value: 3.142},
},
fields: []string{"field1", "field2", "field3"},
expireOption: NX,
want: []int{1, 1, 1},
wantErr: false,
},
{
name: "5. Set Expiration with option XX.",
key: "HExpireKey5",
presetValue: hash.Hash{
"field1": {Value: "value1"},
"field2": {Value: 365},
"field3": {Value: 3.142},
},
fields: []string{"field1", "field2", "field3"},
expireOption: XX,
want: []int{0, 0, 0},
wantErr: false,
},
{
name: "6. Set Expiration with option GT.",
key: "HExpireKey6",
presetValue: hash.Hash{
"field1": {Value: "value1"},
"field2": {Value: 365},
"field3": {Value: 3.142},
},
fields: []string{"field1", "field2", "field3"},
expireOption: GT,
want: []int{0, 0, 0},
wantErr: false,
},
{
name: "7. Set Expiration with option LT.",
key: "HExpireKey7",
presetValue: hash.Hash{
"field1": {Value: "value1"},
"field2": {Value: 365},
"field3": {Value: 3.142},
},
fields: []string{"field1", "field2", "field3"},
expireOption: LT,
want: []int{1, 1, 1},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.presetValue != nil {
err := presetValue(server, context.Background(), tt.key, tt.presetValue)
if err != nil {
t.Error(err)
return
}
}
got, err := server.HExpire(tt.key, 5, tt.expireOption, tt.fields...)
if (err != nil) != tt.wantErr {
t.Errorf("HExpire() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("HExpire() got = %v, want %v", got, tt.want)
}
})
}
}

func TestSugarDB_HTTL(t *testing.T) {
server := createSugarDB()
tests := []struct {
name string
presetValue interface{}
key string
fields []string
want []int
wantErr bool
}{
{
name: "1. Get TTL for one field when expireTime is set.",
key: "HExpireKey1",
presetValue: hash.Hash{
"field1": {Value: "value1", ExpireAt: server.clock.Now().Add(time.Duration(500) * time.Second)},
},
fields: []string{"field1"},
want: []int{500},
wantErr: false,
},
{
name: "2. Get TTL for multiple fields when expireTime is set.",
presetValue: hash.Hash{
"field1": {Value: "value1", ExpireAt: server.clock.Now().Add(time.Duration(500) * time.Second)},
"field2": {Value: "value2", ExpireAt: server.clock.Now().Add(time.Duration(500) * time.Second)},
"field3": {Value: "value3", ExpireAt: server.clock.Now().Add(time.Duration(500) * time.Second)},
},
key: "HExpireKey2",
fields: []string{"field1", "field2", "field3"},
want: []int{500, 500, 500},
wantErr: false,
},
{
name: "3. Get TTL for one field when expireTime is not set.",
presetValue: hash.Hash{
"field1": {Value: "value1"},
},
key: "HExpireKey3",
fields: []string{"field1"},
want: []int{-1},
wantErr: false,
},
{
name: "4. Get TTL for multiple fields when expireTime is not set.",
key: "HExpireKey4",
presetValue: hash.Hash{
"field1": {Value: "value1"},
"field2": {Value: 365},
"field3": {Value: 3.142},
},
fields: []string{"field1", "field2", "field3"},
want: []int{-1, -1, -1},
wantErr: false,
},
{
name: "5. Try to get TTL for key that doesn't exist.",
key: "HExpireKey5",
presetValue: nil,
fields: []string{"field1"},
want: []int{-2},
wantErr: false,
},
{
name: "6. Try to get TTL for key that isn't a hash.",
key: "HExpireKey6",
presetValue: "not a hash",
fields: []string{"field1", "field2", "field3"},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.presetValue != nil {
err := presetValue(server, context.Background(), tt.key, tt.presetValue)
if err != nil {
t.Error(err)
return
}
}
got, err := server.HTTL(tt.key, tt.fields...)
if (err != nil) != tt.wantErr {
t.Errorf("HExpire() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("HExpire() got = %v, want %v", got, tt.want)
}
})
}
}

0 comments on commit 681666f

Please sign in to comment.