Implement HEXPIRETIME command (#176)

This commit is contained in:
Franta
2025-03-29 22:41:54 +01:00
committed by GitHub
parent 10181cf757
commit 38f9ca4ed7
7 changed files with 3096 additions and 4664 deletions
+2650 -4664
View File
File diff suppressed because it is too large Load Diff
+48
View File
@@ -0,0 +1,48 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# HEXPIRETIME
### Syntax
```
HEXPIRETIME key FIELDS numfields field [field...]
```
### Module
<span className="acl-category">hash</span>
### Categories
<span className="acl-category">fast</span>
<span className="acl-category">hash</span>
<span className="acl-category">read</span>
### Description
Returns the absolute Unix expiration timestamp (in seconds since epoch) of a hash key's field(s) that have a set expiration.
This introspection capability allows you to check at what time a given hash field will expire.
### Examples
<Tabs
defaultValue="go"
values={[
{ label: 'Go (Embedded)', value: 'go', },
{ label: 'CLI', value: 'cli', },
]}
>
<TabItem value="go">
Get the expiration time in seconds for fields in the hash:
```go
db, err := sugardb.NewSugarDB()
if err != nil {
log.Fatal(err)
}
TTLArray, err := db.HEXPIRETIME("key", field1, field2)
```
</TabItem>
<TabItem value="cli">
Get the expiration time in seconds for fields in the hash:
```
> HEXPIRETIME key FIELDS 2 field1 field2
```
</TabItem>
</Tabs>
+55
View File
@@ -879,6 +879,54 @@ func handleHPEXPIRETIME(params internal.HandlerFuncParams) ([]byte, error) {
return []byte(resp), nil
}
func handleHEXPIRETIME(params internal.HandlerFuncParams) ([]byte, error) {
keys, err := hexpiretimeKeyFunc(params.Command)
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(":-1\r\n"), 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)
}
for _, field := range fields {
f, ok := hash[field]
if !ok {
// Field doesn't exist
resp += ":-2\r\n"
continue
}
if f.ExpireAt == (time.Time{}) {
// No expiration set
resp += ":-1\r\n"
continue
}
// Calculate seconds
resp += fmt.Sprintf(":%d\r\n", f.ExpireAt.Unix())
}
return []byte(resp), nil
}
func Commands() []internal.Command {
return []internal.Command{
{
@@ -1053,5 +1101,12 @@ Return the string length of the values stored at the specified fields. 0 if the
KeyExtractionFunc: hpexpiretimeKeyFunc,
HandlerFunc: handleHPEXPIRETIME,
},
{ Command: "hexpiretime",
Module: constants.HashModule,
Categories: []string{constants.HashCategory, constants.ReadCategory, constants.FastCategory},
Sync: false,
KeyExtractionFunc: hexpiretimeKeyFunc,
HandlerFunc: handleHEXPIRETIME,
},
}
}
+172
View File
@@ -2665,4 +2665,176 @@ func Test_Hash(t *testing.T) {
})
}
})
t.Run("Test_HandleHEXPIRETIME", func(t *testing.T) {
t.Parallel()
conn, err := internal.GetConnection("localhost", port)
if err != nil {
t.Error(err)
return
}
defer func() {
_ = conn.Close()
}()
client := resp.NewConn(conn)
const fixedTimestamp = 1136189545
tests := []struct {
name string
key string
command []string
presetValue interface{}
setExpire bool
expireSeconds int64
expectedValue string
expectedError error
}{
{
name: "1. Single field with expiration",
key: "HExpireTimeKey1",
command: []string{"HEXPIRETIME", "HExpireTimeKey1", "FIELDS", "1", "field1"},
presetValue: hash.Hash{
"field1": hash.HashValue{
Value: "default1",
},
},
setExpire: true,
expireSeconds: 500,
expectedValue: fmt.Sprintf("[%d]", fixedTimestamp),
},
{
name: "2. Single field with no expiration",
key: "HExpireTimeKey2",
command: []string{"HEXPIRETIME", "HExpireTimeKey2", "FIELDS", "1", "field1"},
presetValue: hash.Hash{
"field1": hash.HashValue{
Value: "default1",
},
},
setExpire: false,
expectedValue: "[-1]",
},
{
name: "3. Multiple fields mixed",
key: "HExpireTimeKey3",
command: []string{"HEXPIRETIME", "HExpireTimeKey3", "FIELDS", "3", "field1", "field2", "nonexist"},
presetValue: hash.Hash{
"field1": hash.HashValue{
Value: "default1",
},
"field2": hash.HashValue{
Value: "default2",
},
},
setExpire: true,
expireSeconds: 500,
expectedValue: fmt.Sprintf("[%d %d -2]", fixedTimestamp, fixedTimestamp),
},
{
name: "4. Key does not exist",
key: "NonExistentKey",
command: []string{"HEXPIRETIME", "NonExistentKey", "FIELDS", "1", "field1"},
presetValue: nil,
setExpire: false,
expectedValue: "-1",
},
{
name: "5. Key is not a hash",
key: "HExpireTimeKey5",
command: []string{"HEXPIRETIME", "HExpireTimeKey5", "FIELDS", "1", "field1"},
presetValue: "string value",
setExpire: false,
expectedValue: "",
expectedError: errors.New("value at HExpireTimeKey5 is not a hash"),
},
{
name: "6. Invalid numfields format",
key: "HExpireTimeKey6",
command: []string{"HEXPIRETIME", "HExpireTimeKey6", "FIELDS", "notanumber", "field1"},
presetValue: hash.Hash{
"field1": hash.HashValue{
Value: "default1",
},
},
setExpire: false,
expectedValue: "",
expectedError: errors.New("expire time must be integer, was provided \"notanumber\""),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.presetValue != nil {
var command []resp.Value
switch v := test.presetValue.(type) {
case string:
command = []resp.Value{
resp.StringValue("SET"),
resp.StringValue(test.key),
resp.StringValue(v),
}
case hash.Hash:
command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)}
for key, value := range v {
command = append(command, resp.StringValue(key), resp.StringValue(value.Value.(string)))
}
}
if err = client.WriteArray(command); err != nil {
t.Error(err)
}
if _, _, err = client.ReadValue(); err != nil {
t.Error(err)
}
if test.setExpire {
if hash, ok := test.presetValue.(hash.Hash); ok {
for field := range hash {
expireCmd := []resp.Value{
resp.StringValue("HEXPIRE"),
resp.StringValue(test.key),
resp.StringValue(strconv.FormatInt(test.expireSeconds, 10)),
resp.StringValue("FIELDS"),
resp.StringValue("1"),
resp.StringValue(field),
}
if err = client.WriteArray(expireCmd); err != nil {
t.Error(err)
}
if _, _, err = client.ReadValue(); err != nil {
t.Error(err)
}
}
}
}
}
command := make([]resp.Value, len(test.command))
for i, v := range test.command {
command[i] = resp.StringValue(v)
}
if err = client.WriteArray(command); err != nil {
t.Error(err)
}
resp, _, err := client.ReadValue()
if err != nil {
t.Error(err)
}
if test.expectedError != nil {
if !strings.Contains(resp.Error().Error(), test.expectedError.Error()) {
t.Errorf("expected error %q, got %q", test.expectedError.Error(), resp.Error())
}
return
}
if resp.String() != test.expectedValue {
t.Errorf("Expected value %q but got %q", test.expectedValue, resp.String())
}
})
}
})
}
+16
View File
@@ -214,3 +214,19 @@ func hpexpiretimeKeyFunc(cmd []string) (internal.KeyExtractionFuncResult, error)
WriteKeys: make([]string, 0),
}, nil
}
func hexpiretimeKeyFunc(cmd []string) (internal.KeyExtractionFuncResult, error) {
if len(cmd) < 5 {
return internal.KeyExtractionFuncResult{}, errors.New(constants.WrongArgsResponse)
}
if cmd[2] != "FIELDS" {
return internal.KeyExtractionFuncResult{}, errors.New(constants.InvalidCmdResponse)
}
return internal.KeyExtractionFuncResult{
Channels: make([]string, 0),
ReadKeys: cmd[1:],
WriteKeys: make([]string, 0),
}, nil
}
+24
View File
@@ -447,3 +447,27 @@ func (server *SugarDB) HPExpireTime(key string, fields ...string) ([]int64, erro
}
return internal.ParseInteger64ArrayResponse(b)
}
// HExpireTime returns the absolute Unix timestamp in seconds for the given field(s) expiration time.
//
// Parameters:
//
// `key` - string - the key to the hash map.
//
// `fields` - ...string - a list of fields to check expiration time.
//
// Returns: an integer array representing the expiration timestamp in seconds for each field.
// If a field doesn't exist or has no expiry set, -1 is returned.
//
// Errors:
//
// "value at <key> is not a hash" - when the provided key is not a hash.
func (server *SugarDB) HExpireTime(key string, fields ...string) ([]int64, error) {
numFields := fmt.Sprintf("%v", len(fields))
cmd := append([]string{"HEXPIRETIME", key, "FIELDS", numFields}, fields...)
b, err := server.handleCommand(server.context, internal.EncodeCommand(cmd), nil, false, true)
if err != nil {
return nil, err
}
return internal.ParseInteger64ArrayResponse(b)
}
+131
View File
@@ -1293,4 +1293,135 @@ func TestSugarDB_Hash(t *testing.T) {
})
}
})
t.Run("Test_HandleHEXPIRETIME", func(t *testing.T) {
t.Parallel()
const fixedTimestamp int64 = 1136189545
var noOption ExpireOptions
tests := []struct {
name string
presetValue interface{}
key string
fields []string
want []int64
wantErr bool
setExpiry bool
}{
{
name: "1. Get expiration time for one field",
key: "HExpireTime_Key1",
presetValue: hash.Hash{
"field1": hash.HashValue{
Value: "value1",
},
},
fields: []string{"field1"},
want: []int64{fixedTimestamp},
wantErr: false,
setExpiry: true,
},
{
name: "2. Get expiration time for multiple fields",
key: "HExpireTime_Key2",
presetValue: hash.Hash{
"field1": hash.HashValue{
Value: "value1",
},
"field2": hash.HashValue{
Value: "value2",
},
"field3": hash.HashValue{
Value: "value3",
},
},
fields: []string{"field1", "field2", "field3"},
want: []int64{fixedTimestamp, fixedTimestamp, fixedTimestamp},
wantErr: false,
setExpiry: true,
},
{
name: "3. Mix of existing and non-existing fields",
key: "HExpireTime_Key3",
presetValue: hash.Hash{
"field1": hash.HashValue{
Value: "value1",
},
"field2": hash.HashValue{
Value: "value2",
},
},
fields: []string{"field1", "nonexistent", "field2"},
want: []int64{fixedTimestamp, -2, fixedTimestamp},
wantErr: false,
setExpiry: true,
},
{
name: "4. Fields with no expiration set",
key: "HExpireTime_Key4",
presetValue: hash.Hash{
"field1": hash.HashValue{Value: "value1"},
"field2": hash.HashValue{Value: "value2"},
},
fields: []string{"field1", "field2"},
want: []int64{-1, -1},
wantErr: false,
setExpiry: false,
},
{
name: "6. Key doesn't exist",
key: "HExpireTime_Key6",
presetValue: nil,
fields: []string{"field1"},
want: []int64{},
wantErr: false,
setExpiry: false,
},
{
name: "7. Key is not a hash",
key: "HExpireTime_Key7",
presetValue: "not a hash",
fields: []string{"field1"},
want: nil,
wantErr: true,
setExpiry: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if tt.presetValue != nil {
err := presetValue(server, context.Background(), tt.key, tt.presetValue)
if err != nil {
t.Error(err)
return
}
if hash, ok := tt.presetValue.(hash.Hash); ok && tt.setExpiry {
for _, field := range tt.fields {
if hashValue, exists := hash[field]; exists && hashValue.Value != nil {
_, err := server.HExpire(tt.key, 500, noOption, field)
if err != nil {
t.Error(err)
return
}
}
}
}
}
got, err := server.HExpireTime(tt.key, tt.fields...)
t.Logf("ExpireAt time: %v", got)
if (err != nil) != tt.wantErr {
t.Errorf("HExpireTime() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("HExpireTime() got = %v, want %v", got, tt.want)
}
})
}
})
}