mirror of
https://github.com/EchoVault/SugarDB.git
synced 2025-11-01 03:42:54 +08:00
Add commands HEXPIRE and HTTL (#148)
* Implemented HEXPIRE and HTTL commands - @osteensco
This commit is contained in:
@@ -240,6 +240,7 @@ Benchmark script options:
|
|||||||
## HASH
|
## HASH
|
||||||
* [HDEL](https://sugardb.io/docs/commands/hash/hdel)
|
* [HDEL](https://sugardb.io/docs/commands/hash/hdel)
|
||||||
* [HEXISTS](https://sugardb.io/docs/commands/hash/hexists)
|
* [HEXISTS](https://sugardb.io/docs/commands/hash/hexists)
|
||||||
|
* [HEXPIRE](https://sugardb.io/docs/commands/hash/hexpire)
|
||||||
* [HGET](https://sugardb.io/docs/commands/hash/hget)
|
* [HGET](https://sugardb.io/docs/commands/hash/hget)
|
||||||
* [HGETALL](https://sugardb.io/docs/commands/hash/hgetall)
|
* [HGETALL](https://sugardb.io/docs/commands/hash/hgetall)
|
||||||
* [HINCRBY](https://sugardb.io/docs/commands/hash/hincrby)
|
* [HINCRBY](https://sugardb.io/docs/commands/hash/hincrby)
|
||||||
@@ -251,6 +252,7 @@ Benchmark script options:
|
|||||||
* [HSET](https://sugardb.io/docs/commands/hash/hset)
|
* [HSET](https://sugardb.io/docs/commands/hash/hset)
|
||||||
* [HSETNX](https://sugardb.io/docs/commands/hash/hsetnx)
|
* [HSETNX](https://sugardb.io/docs/commands/hash/hsetnx)
|
||||||
* [HSTRLEN](https://sugardb.io/docs/commands/hash/hstrlen)
|
* [HSTRLEN](https://sugardb.io/docs/commands/hash/hstrlen)
|
||||||
|
* [HTTL](https://sugardb.io/docs/commands/hash/httl)
|
||||||
* [HVALS](https://sugardb.io/docs/commands/hash/hvals)
|
* [HVALS](https://sugardb.io/docs/commands/hash/hvals)
|
||||||
|
|
||||||
<a name="commands-list"></a>
|
<a name="commands-list"></a>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
48
docs/docs/commands/hash/hexpire.mdx
Normal file
48
docs/docs/commands/hash/hexpire.mdx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import Tabs from '@theme/Tabs';
|
||||||
|
import TabItem from '@theme/TabItem';
|
||||||
|
|
||||||
|
# HEXPIRE
|
||||||
|
|
||||||
|
### Syntax
|
||||||
|
```
|
||||||
|
HEXPIRE key seconds [NX | XX | GT | LT] 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">write</span>
|
||||||
|
|
||||||
|
### Description
|
||||||
|
Set an expiration (TTL or time to live) in seconds on one or more fields of a given hash key.
|
||||||
|
You must specify at least one field. Field(s) will automatically be deleted from the hash key when their TTLs expire.
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
defaultValue="go"
|
||||||
|
values={[
|
||||||
|
{ label: 'Go (Embedded)', value: 'go', },
|
||||||
|
{ label: 'CLI', value: 'cli', },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<TabItem value="go">
|
||||||
|
Set the expiration in seconds for fields in the hash:
|
||||||
|
```go
|
||||||
|
db, err := sugardb.NewSugarDB()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
respArray, err := db.HExpire("key", 500, nil, field1, field2)
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="cli">
|
||||||
|
Set the expiration in seconds for fields in the hash:
|
||||||
|
```
|
||||||
|
> HEXPIRE key 500 FIELDS 2 field1 field2
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
48
docs/docs/commands/hash/httl.mdx
Normal file
48
docs/docs/commands/hash/httl.mdx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import Tabs from '@theme/Tabs';
|
||||||
|
import TabItem from '@theme/TabItem';
|
||||||
|
|
||||||
|
# HTTL
|
||||||
|
|
||||||
|
### Syntax
|
||||||
|
```
|
||||||
|
HTTL 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 remaining TTL (time to live) of a hash key's field(s) that have a set expiration.
|
||||||
|
This introspection capability allows you to check how many seconds a given hash field will continue to be part of the hash key.
|
||||||
|
|
||||||
|
### 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.HTTL("key", field1, field2)
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="cli">
|
||||||
|
Get the expiration time in seconds for fields in the hash:
|
||||||
|
```
|
||||||
|
> HTTL key FIELDS 2 field1 field2
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
@@ -54,8 +54,10 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
OkResponse = "+OK\r\n"
|
OkResponse = "+OK\r\n"
|
||||||
WrongArgsResponse = "wrong number of arguments"
|
WrongArgsResponse = "wrong number of arguments"
|
||||||
|
MissingArgResponse = "missing argument %s"
|
||||||
|
InvalidCmdResponse = "invalid command provided"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -17,12 +17,14 @@ package hash
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/echovault/sugardb/internal"
|
|
||||||
"github.com/echovault/sugardb/internal/constants"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/echovault/sugardb/internal"
|
||||||
|
"github.com/echovault/sugardb/internal/constants"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleHSET(params internal.HandlerFuncParams) ([]byte, error) {
|
func handleHSET(params internal.HandlerFuncParams) ([]byte, error) {
|
||||||
@@ -611,6 +613,222 @@ func handleHDEL(params internal.HandlerFuncParams) ([]byte, error) {
|
|||||||
return []byte(fmt.Sprintf(":%d\r\n", count)), nil
|
return []byte(fmt.Sprintf(":%d\r\n", count)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleHEXPIRE(params internal.HandlerFuncParams) ([]byte, error) {
|
||||||
|
keys, err := hexpireKeyFunc(params.Command)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
key := keys.WriteKeys[0]
|
||||||
|
|
||||||
|
// HEXPIRE key seconds [NX | XX | GT | LT] FIELDS numfields field
|
||||||
|
cmdargs := keys.WriteKeys[1:]
|
||||||
|
seconds, err := strconv.ParseInt(cmdargs[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New(fmt.Sprintf("seconds must be integer, was provided %q", cmdargs[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIELDS argument provides starting index to work off of to grab fields
|
||||||
|
var fieldsIdx int
|
||||||
|
if cmdargs[1] == "FIELDS" {
|
||||||
|
fieldsIdx = 1
|
||||||
|
} else if cmdargs[2] == "FIELDS" {
|
||||||
|
fieldsIdx = 2
|
||||||
|
} else {
|
||||||
|
return nil, errors.New(fmt.Sprintf(constants.MissingArgResponse, "FIELDS"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// index through numfields
|
||||||
|
numfields, err := strconv.ParseInt(cmdargs[fieldsIdx+1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New(fmt.Sprintf("numberfields must be integer, was provided %q", cmdargs[fieldsIdx+1]))
|
||||||
|
}
|
||||||
|
endIdx := fieldsIdx + 2 + int(numfields)
|
||||||
|
fields := cmdargs[fieldsIdx+2 : endIdx]
|
||||||
|
|
||||||
|
expireAt := params.GetClock().Now().Add(time.Duration(seconds) * time.Second)
|
||||||
|
|
||||||
|
// 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-- {
|
||||||
|
resp = resp + ":2\r\n"
|
||||||
|
}
|
||||||
|
return []byte(resp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if fieldsIdx == 2 {
|
||||||
|
// Handle expire options
|
||||||
|
switch strings.ToLower(cmdargs[1]) {
|
||||||
|
case "nx":
|
||||||
|
for _, f := range fields {
|
||||||
|
_, ok := hash[f]
|
||||||
|
if !ok {
|
||||||
|
resp = resp + ":-2\r\n"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
currentExpireAt := hash[f].ExpireAt
|
||||||
|
if currentExpireAt != (time.Time{}) {
|
||||||
|
resp = resp + ":0\r\n"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = params.SetHashExpiry(params.Context, key, f, expireAt)
|
||||||
|
if err != nil {
|
||||||
|
return []byte(resp), err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = resp + ":1\r\n"
|
||||||
|
|
||||||
|
}
|
||||||
|
case "xx":
|
||||||
|
for _, f := range fields {
|
||||||
|
_, ok := hash[f]
|
||||||
|
if !ok {
|
||||||
|
resp = resp + ":-2\r\n"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
currentExpireAt := hash[f].ExpireAt
|
||||||
|
if currentExpireAt == (time.Time{}) {
|
||||||
|
resp = resp + ":0\r\n"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = params.SetHashExpiry(params.Context, key, f, expireAt)
|
||||||
|
if err != nil {
|
||||||
|
return []byte(resp), err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = resp + ":1\r\n"
|
||||||
|
|
||||||
|
}
|
||||||
|
case "gt":
|
||||||
|
for _, f := range fields {
|
||||||
|
_, ok := hash[f]
|
||||||
|
if !ok {
|
||||||
|
resp = resp + ":-2\r\n"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
currentExpireAt := hash[f].ExpireAt
|
||||||
|
//TODO
|
||||||
|
if currentExpireAt == (time.Time{}) || expireAt.Before(currentExpireAt) {
|
||||||
|
resp = resp + ":0\r\n"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = params.SetHashExpiry(params.Context, key, f, expireAt)
|
||||||
|
if err != nil {
|
||||||
|
return []byte(resp), err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = resp + ":1\r\n"
|
||||||
|
|
||||||
|
}
|
||||||
|
case "lt":
|
||||||
|
for _, f := range fields {
|
||||||
|
_, ok := hash[f]
|
||||||
|
if !ok {
|
||||||
|
resp = resp + ":-2\r\n"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
currentExpireAt := hash[f].ExpireAt
|
||||||
|
if currentExpireAt != (time.Time{}) && currentExpireAt.Before(expireAt) {
|
||||||
|
resp = resp + ":0\r\n"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = params.SetHashExpiry(params.Context, key, f, expireAt)
|
||||||
|
if err != nil {
|
||||||
|
return []byte(resp), err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = resp + ":1\r\n"
|
||||||
|
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
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 {
|
||||||
|
_, ok := hash[f]
|
||||||
|
if !ok {
|
||||||
|
resp = resp + ":-2\r\n"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = params.SetHashExpiry(params.Context, key, f, expireAt)
|
||||||
|
if err != nil {
|
||||||
|
return []byte(resp), err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = resp + ":1\r\n"
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Array resp
|
||||||
|
return []byte(resp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleHTTL(params internal.HandlerFuncParams) ([]byte, error) {
|
||||||
|
keys, err := httlKeyFunc(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 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// build out response
|
||||||
|
for _, field := range fields {
|
||||||
|
f, ok := hash[field]
|
||||||
|
if !ok {
|
||||||
|
resp = resp + ":-2\r\n"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if f.ExpireAt == (time.Time{}) {
|
||||||
|
resp = resp + ":-1\r\n"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resp = resp + fmt.Sprintf(":%d\r\n", int(f.ExpireAt.Sub(params.GetClock().Now()).Round(time.Second).Seconds()))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// array response
|
||||||
|
return []byte(resp), nil
|
||||||
|
}
|
||||||
|
|
||||||
func Commands() []internal.Command {
|
func Commands() []internal.Command {
|
||||||
return []internal.Command{
|
return []internal.Command{
|
||||||
{
|
{
|
||||||
@@ -744,14 +962,23 @@ Return the string length of the values stored at the specified fields. 0 if the
|
|||||||
KeyExtractionFunc: hdelKeyFunc,
|
KeyExtractionFunc: hdelKeyFunc,
|
||||||
HandlerFunc: handleHDEL,
|
HandlerFunc: handleHDEL,
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// Command: "hexpire",
|
Command: "hexpire",
|
||||||
// Module: constants.HashModule,
|
Module: constants.HashModule,
|
||||||
// Categories: []string{constants.HashCategory, constants.WriteCategory, constants.FastCategory},
|
Categories: []string{constants.HashCategory, constants.WriteCategory, constants.FastCategory},
|
||||||
// Description: `(HEXPIRE key seconds [NX | XX | GT | LT] FIELDS numfields field [field ...]) Sets the expiration, in seconds, of a field in a hash.`,
|
Description: `(HEXPIRE key seconds [NX | XX | GT | LT] FIELDS numfields field [field ...]) Sets the expiration, in seconds, of a field in a hash.`,
|
||||||
// Sync: true,
|
Sync: true,
|
||||||
// KeyExtractionFunc: hexpireKeyFunc,
|
KeyExtractionFunc: hexpireKeyFunc,
|
||||||
// HandlerFunc: handleHEXPIRE,
|
HandlerFunc: handleHEXPIRE,
|
||||||
// },
|
},
|
||||||
|
{
|
||||||
|
Command: "httl",
|
||||||
|
Module: constants.HashModule,
|
||||||
|
Categories: []string{constants.HashCategory, constants.ReadCategory, constants.FastCategory},
|
||||||
|
Description: `HTTL key FIELDS numfields field [field ...] Returns the remaining TTL (time to live) of a hash key's field(s) that have a set expiration.`,
|
||||||
|
Sync: true,
|
||||||
|
KeyExtractionFunc: httlKeyFunc,
|
||||||
|
HandlerFunc: handleHTTL,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,15 @@ package hash_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/echovault/sugardb/internal"
|
"github.com/echovault/sugardb/internal"
|
||||||
|
"github.com/echovault/sugardb/internal/clock"
|
||||||
"github.com/echovault/sugardb/internal/config"
|
"github.com/echovault/sugardb/internal/config"
|
||||||
"github.com/echovault/sugardb/internal/constants"
|
"github.com/echovault/sugardb/internal/constants"
|
||||||
"github.com/echovault/sugardb/internal/modules/hash"
|
"github.com/echovault/sugardb/internal/modules/hash"
|
||||||
@@ -30,6 +33,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Test_Hash(t *testing.T) {
|
func Test_Hash(t *testing.T) {
|
||||||
|
mockClock := clock.NewClock()
|
||||||
port, err := internal.GetFreePort()
|
port, err := internal.GetFreePort()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
@@ -1931,4 +1935,561 @@ func Test_Hash(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Test_HandleHEXPIRE", 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)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
key string
|
||||||
|
presetValue hash.Hash
|
||||||
|
command []string
|
||||||
|
expectedValue string
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "1. Set expiration for all keys in hash, no options.",
|
||||||
|
key: "HexpireKey1",
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HexpireK1Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
},
|
||||||
|
"HexpireK1Field2": hash.HashValue{
|
||||||
|
Value: "default2",
|
||||||
|
},
|
||||||
|
"HexpireK1Field3": hash.HashValue{
|
||||||
|
Value: "default3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
command: []string{"HEXPIRE", "HexpireKey1", "5", "FIELDS", "3", "HexpireK1Field1", "HexpireK1Field2", "HexpireK1Field3"},
|
||||||
|
expectedValue: "[1 1 1]",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "2. Set expiration for one key in hash, no options.",
|
||||||
|
key: "HexpireKey2",
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HexpireK2Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
command: []string{"HEXPIRE", "HexpireKey2", "5", "FIELDS", "1", "HexpireK2Field1"},
|
||||||
|
expectedValue: "[1]",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "3. Set expiration, expireTime already populated, no options.",
|
||||||
|
key: "HexpireKey3",
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HexpireK3Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
ExpireAt: mockClock.Now().Add(500 * time.Second),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
command: []string{"HEXPIRE", "HexpireKey3", "100", "FIELDS", "1", "HexpireK3Field1"},
|
||||||
|
expectedValue: "[1]",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "4. Set expiration, option NX with no expire time currently set.",
|
||||||
|
key: "HexpireKey4",
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HexpireK4Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
command: []string{"HEXPIRE", "HexpireKey4", "5", "NX", "FIELDS", "1", "HexpireK4Field1"},
|
||||||
|
expectedValue: "[1]",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "5. Set expiration, option NX with an expire time already set.",
|
||||||
|
key: "HexpireKey5",
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HexpireK5Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
ExpireAt: mockClock.Now().Add(500 * time.Second),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
command: []string{"HEXPIRE", "HexpireKey5", "100", "NX", "FIELDS", "1", "HexpireK5Field1"},
|
||||||
|
expectedValue: "[0]",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "6. Set expiration, option XX with no expire time currently set.",
|
||||||
|
key: "HexpireKey6",
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HexpireK6Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
command: []string{"HEXPIRE", "HexpireKey6", "5", "XX", "FIELDS", "1", "HexpireK6Field1"},
|
||||||
|
expectedValue: "[0]",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "7. Set expiration, option XX with expire time already set.",
|
||||||
|
key: "HexpireKey7",
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HexpireK7Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
ExpireAt: mockClock.Now().Add(500 * time.Second),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
command: []string{"HEXPIRE", "HexpireKey7", "100", "XX", "FIELDS", "1", "HexpireK7Field1"},
|
||||||
|
expectedValue: "[1]",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "8. Set expiration, option GT with expire time less than one provided.",
|
||||||
|
key: "HexpireKey8",
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HexpireK8Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
ExpireAt: mockClock.Now().Add(500 * time.Second),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
command: []string{"HEXPIRE", "HexpireKey8", "1000", "GT", "FIELDS", "1", "HexpireK8Field1"},
|
||||||
|
expectedValue: "[1]",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "9. Set expiration, option GT with expire time greater than one provided.",
|
||||||
|
key: "HexpireKey9",
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HexpireK9Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
ExpireAt: mockClock.Now().Add(500 * time.Second),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
command: []string{"HEXPIRE", "HexpireKey9", "100", "GT", "FIELDS", "1", "HexpireK9Field1"},
|
||||||
|
expectedValue: "[0]",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "10. Set expiration, option LT with expire time less than one provided.",
|
||||||
|
key: "HexpireKey10",
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HexpireK10Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
ExpireAt: mockClock.Now().Add(500 * time.Second),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
command: []string{"HEXPIRE", "HexpireKey10", "1000", "LT", "FIELDS", "1", "HexpireK10Field1"},
|
||||||
|
expectedValue: "[0]",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "11. Set expiration, option LT with expire time greater than one provided.",
|
||||||
|
key: "HexpireKey11",
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HexpireK11Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
ExpireAt: mockClock.Now().Add(500 * time.Second),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
command: []string{"HEXPIRE", "HexpireKey11", "100", "LT", "FIELDS", "1", "HexpireK11Field1"},
|
||||||
|
expectedValue: "[1]",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "12. Set expiration, provide 0 seconds.",
|
||||||
|
key: "HexpireKey12",
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HexpireK12Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
command: []string{"HEXPIRE", "HexpireKey12", "0", "FIELDS", "1", "HexpireK12Field1"},
|
||||||
|
expectedValue: "[2]",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "13. Attempt to set expiration for non existent key.",
|
||||||
|
key: "HexpireKeyNOTEXIST",
|
||||||
|
presetValue: nil,
|
||||||
|
command: []string{"HEXPIRE", "HexpireKeyNOTEXIST", "100", "FIELDS", "1", "HexpireKNEField1"},
|
||||||
|
expectedValue: "[-2]",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "14. Attempt to set expiration for field that doesn't exist.",
|
||||||
|
key: "HexpireKey14",
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HexpireK14Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
command: []string{"HEXPIRE", "HexpireKey14", "100", "FIELDS", "2", "HexpireK14BadField1", "HexpireK14Field1"},
|
||||||
|
expectedValue: "[-2 1]",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "15. Set expiration, command wrong length.",
|
||||||
|
key: "HexpireKey15",
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HexpireK15Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
command: []string{"HEXPIRE", "HexpireKey15", "100", "1", "HexpireK15Field1"},
|
||||||
|
expectedError: errors.New("Error wrong number of arguments"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "16. Set expiration, command filed numfields is not a number.",
|
||||||
|
key: "HexpireKey16",
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HexpireK16Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
command: []string{"HEXPIRE", "HexpireKey16", "100", "FIELDS", "one", "HexpireK16Field1"},
|
||||||
|
expectedError: errors.New("Error numberfields must be integer, was provided \"one\""),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
// set key with preset value
|
||||||
|
if test.presetValue != nil {
|
||||||
|
var command []resp.Value
|
||||||
|
var expected string
|
||||||
|
|
||||||
|
command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)}
|
||||||
|
for key, value := range test.presetValue {
|
||||||
|
command = append(command, []resp.Value{
|
||||||
|
resp.StringValue(key),
|
||||||
|
resp.StringValue(value.Value.(string))}...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
expected = strconv.Itoa(len(test.presetValue))
|
||||||
|
|
||||||
|
if err = client.WriteArray(command); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
res, _, err := client.ReadValue()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.EqualFold(res.String(), expected) {
|
||||||
|
t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// preset Expire Time
|
||||||
|
for field, value := range test.presetValue {
|
||||||
|
if value.ExpireAt != (time.Time{}) {
|
||||||
|
cmd := []resp.Value{
|
||||||
|
resp.StringValue("HEXPIRE"),
|
||||||
|
resp.StringValue(test.key),
|
||||||
|
resp.StringValue("500"),
|
||||||
|
resp.StringValue("FIELDS"),
|
||||||
|
resp.StringValue("1"),
|
||||||
|
resp.StringValue(field),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.WriteArray(cmd); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
res, _, err := client.ReadValue()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if res.String() != "[1]" {
|
||||||
|
t.Errorf("Error presetting expire time - Key: %s, Field: %s, response: %s", test.key, field, res.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// run HEXPIRE command
|
||||||
|
command := make([]resp.Value, len(test.command))
|
||||||
|
for i, c := range test.command {
|
||||||
|
command[i] = resp.StringValue(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.WriteArray(command); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
res, _, err := client.ReadValue()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.expectedError != nil {
|
||||||
|
if !strings.Contains(res.Error().Error(), test.expectedError.Error()) {
|
||||||
|
t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.String() != test.expectedValue {
|
||||||
|
t.Errorf("expected response %q, got %q", test.expectedValue, res.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Test_HandleHTTL", 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)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
command []string
|
||||||
|
key string
|
||||||
|
presetValue interface{}
|
||||||
|
setExpire bool
|
||||||
|
expectedValue string
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "1. Get TTL for one field when expireTime is set.",
|
||||||
|
key: "HTTLKey1",
|
||||||
|
command: []string{"HTTL", "HTTLKey1", "FIELDS", "1", "HTTLK1Field1"},
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HTTLK1Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setExpire: true,
|
||||||
|
expectedValue: "[5]",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "2. Get TTL for multiple fields when expireTime is set.",
|
||||||
|
key: "HTTLKey2",
|
||||||
|
command: []string{"HTTL", "HTTLKey2", "FIELDS", "3", "HTTLK2Field1", "HTTLK2Field2", "HTTLK2Field3"},
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HTTLK2Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
},
|
||||||
|
"HTTLK2Field2": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
},
|
||||||
|
"HTTLK2Field3": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setExpire: true,
|
||||||
|
expectedValue: "[5 5 5]",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "3. Get TTL for one field when expireTime is not set.",
|
||||||
|
key: "HTTLKey3",
|
||||||
|
command: []string{"HTTL", "HTTLKey3", "FIELDS", "1", "HTTLK3Field1"},
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HTTLK3Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setExpire: false,
|
||||||
|
expectedValue: "[-1]",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "4. Get TTL for multiple fields when expireTime is not set.",
|
||||||
|
key: "HTTLKey4",
|
||||||
|
command: []string{"HTTL", "HTTLKey4", "FIELDS", "3", "HTTLK4Field1", "HTTLK4Field2", "HTTLK4Field3"},
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HTTLK4Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
},
|
||||||
|
"HTTLK4Field2": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
},
|
||||||
|
"HTTLK4Field3": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setExpire: false,
|
||||||
|
expectedValue: "[-1 -1 -1]",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "5. Try to get TTL for key that doesn't exist.",
|
||||||
|
key: "HTTLKeyNOTEXIST",
|
||||||
|
command: []string{"HTTL", "HTTLKeyNOTEXIST", "FIELDS", "1", "HTTLK1Field1"},
|
||||||
|
presetValue: nil,
|
||||||
|
setExpire: false,
|
||||||
|
expectedValue: "[-2]",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "6. Try to get TTL for key that isn't a hash.",
|
||||||
|
key: "HTTLKey6",
|
||||||
|
command: []string{"HTTL", "HTTLKey6", "FIELDS", "1", "HTTLK6Field1"},
|
||||||
|
presetValue: "NotaHash",
|
||||||
|
setExpire: false,
|
||||||
|
expectedError: errors.New("Error value at HTTLKey6 is not a hash"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "7. Command missing 'FIELDS'.",
|
||||||
|
key: "HTTLKey7",
|
||||||
|
command: []string{"HTTL", "HTTLKey7", "1", "HTTLK7Field1"},
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HTTLK7Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setExpire: false,
|
||||||
|
expectedError: errors.New("Error wrong number of arguments"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "8. Command numfields provided isn't a number.",
|
||||||
|
key: "HTTLKey8",
|
||||||
|
command: []string{"HTTL", "HTTLKey8", "FIELDS", "one", "HTTLK8Field1"},
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HTTLK8Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setExpire: false,
|
||||||
|
expectedError: errors.New("Error expire time must be integer, was provided \"one\""),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "9. Command missing numfields.",
|
||||||
|
key: "HTTLKey9",
|
||||||
|
command: []string{"HTTL", "HTTLKey9", "FIELDS", "HTTLK9Field1"},
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HTTLK9Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setExpire: false,
|
||||||
|
expectedError: errors.New("Error wrong number of arguments"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "10. Command FIELDS index contains something else.",
|
||||||
|
key: "HTTLKey10",
|
||||||
|
command: []string{"HTTL", "HTTLKey10", "NOTFIELDS", "1", "HTTLK10Field1"},
|
||||||
|
presetValue: hash.Hash{
|
||||||
|
"HTTLK10Field1": hash.HashValue{
|
||||||
|
Value: "default1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setExpire: false,
|
||||||
|
expectedError: errors.New("Error invalid command provided"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
// set preset values
|
||||||
|
if test.presetValue != nil {
|
||||||
|
var command []resp.Value
|
||||||
|
var expected string
|
||||||
|
|
||||||
|
switch test.presetValue.(type) {
|
||||||
|
case string:
|
||||||
|
command = []resp.Value{
|
||||||
|
resp.StringValue("SET"),
|
||||||
|
resp.StringValue(test.key),
|
||||||
|
resp.StringValue(test.presetValue.(string)),
|
||||||
|
}
|
||||||
|
expected = "ok"
|
||||||
|
case hash.Hash:
|
||||||
|
command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)}
|
||||||
|
for key, value := range test.presetValue.(hash.Hash) {
|
||||||
|
command = append(command, []resp.Value{
|
||||||
|
resp.StringValue(key),
|
||||||
|
resp.StringValue(value.Value.(string))}...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
expected = strconv.Itoa(len(test.presetValue.(hash.Hash)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.WriteArray(command); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, _, err := client.ReadValue()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.EqualFold(res.String(), expected) {
|
||||||
|
t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.setExpire {
|
||||||
|
// set expire times
|
||||||
|
command := make([]resp.Value, len(test.presetValue.(hash.Hash))+5)
|
||||||
|
command[0] = resp.StringValue("HEXPIRE")
|
||||||
|
command[1] = resp.StringValue(test.key)
|
||||||
|
command[2] = resp.StringValue("5")
|
||||||
|
command[3] = resp.StringValue("FIELDS")
|
||||||
|
command[4] = resp.StringValue(fmt.Sprintf("%v", (len(test.presetValue.(hash.Hash)))))
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for k, _ := range test.presetValue.(hash.Hash) {
|
||||||
|
command[5+i] = resp.StringValue(k)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.WriteArray(command); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
_, _, err := client.ReadValue()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// read TTL
|
||||||
|
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 \"%s\", got \"%s\"", test.expectedError.Error(), resp.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.String() != test.expectedValue {
|
||||||
|
t.Errorf("Expected value %v but got %v", test.expectedValue, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package hash
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/echovault/sugardb/internal"
|
"github.com/echovault/sugardb/internal"
|
||||||
"github.com/echovault/sugardb/internal/constants"
|
"github.com/echovault/sugardb/internal/constants"
|
||||||
)
|
)
|
||||||
@@ -169,3 +170,31 @@ func hdelKeyFunc(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
|||||||
WriteKeys: cmd[1:2],
|
WriteKeys: cmd[1:2],
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hexpireKeyFunc(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||||
|
if len(cmd) < 6 {
|
||||||
|
return internal.KeyExtractionFuncResult{}, errors.New(constants.WrongArgsResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
return internal.KeyExtractionFuncResult{
|
||||||
|
Channels: make([]string, 0),
|
||||||
|
ReadKeys: make([]string, 0),
|
||||||
|
WriteKeys: cmd[1:],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func httlKeyFunc(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
|
||||||
|
}
|
||||||
|
|||||||
@@ -140,6 +140,8 @@ type HandlerFuncParams struct {
|
|||||||
KeysExist func(ctx context.Context, keys []string) map[string]bool
|
KeysExist func(ctx context.Context, keys []string) map[string]bool
|
||||||
// GetExpiry returns the expiry time of a key.
|
// GetExpiry returns the expiry time of a key.
|
||||||
GetExpiry func(ctx context.Context, key string) time.Time
|
GetExpiry func(ctx context.Context, key string) time.Time
|
||||||
|
// GetHashExpiry returns the expiry time of a field in a key whose value is a hash.
|
||||||
|
GetHashExpiry func(ctx context.Context, key string, field string) time.Time
|
||||||
// DeleteKey deletes the specified key. Returns an error if the deletion was unsuccessful.
|
// DeleteKey deletes the specified key. Returns an error if the deletion was unsuccessful.
|
||||||
DeleteKey func(ctx context.Context, key string) error
|
DeleteKey func(ctx context.Context, key string) error
|
||||||
// GetValues retrieves the values from the specified keys.
|
// GetValues retrieves the values from the specified keys.
|
||||||
@@ -147,8 +149,10 @@ type HandlerFuncParams struct {
|
|||||||
GetValues func(ctx context.Context, keys []string) map[string]interface{}
|
GetValues func(ctx context.Context, keys []string) map[string]interface{}
|
||||||
// SetValues sets each of the keys with their corresponding values in the provided map.
|
// SetValues sets each of the keys with their corresponding values in the provided map.
|
||||||
SetValues func(ctx context.Context, entries map[string]interface{}) error
|
SetValues func(ctx context.Context, entries map[string]interface{}) error
|
||||||
// Set expiry sets the expiry time of the key.
|
// SetExpiry sets the expiry time of the key.
|
||||||
SetExpiry func(ctx context.Context, key string, expire time.Time, touch bool)
|
SetExpiry func(ctx context.Context, key string, expire time.Time, touch bool)
|
||||||
|
// SetHashExpiry sets the expiry time of a field in a key whose value is a hash.
|
||||||
|
SetHashExpiry func(ctx context.Context, key string, field string, expire time.Time) error
|
||||||
// GetClock gets the clock used by the server.
|
// GetClock gets the clock used by the server.
|
||||||
// Use this when making use of time methods like .Now and .After.
|
// Use this when making use of time methods like .Now and .After.
|
||||||
// This inversion of control is a helper for testing as the clock is automatically mocked in tests.
|
// This inversion of control is a helper for testing as the clock is automatically mocked in tests.
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ const (
|
|||||||
// LT - Only set the expiry time if the new expiry time is less than the current one.
|
// 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.
|
// 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 {
|
type ExpireOptions interface {
|
||||||
IsExOpt() ExOpt
|
IsExOpt() ExOpt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,10 @@
|
|||||||
package sugardb
|
package sugardb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/echovault/sugardb/internal"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/echovault/sugardb/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HRandFieldOptions modifies the behaviour of the HRandField function.
|
// HRandFieldOptions modifies the behaviour of the HRandField function.
|
||||||
@@ -354,3 +356,70 @@ func (server *SugarDB) HDel(key string, fields ...string) (int, error) {
|
|||||||
}
|
}
|
||||||
return internal.ParseIntegerResponse(b)
|
return internal.ParseIntegerResponse(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HExpire sets the expiration for the provided field(s) in a hash map.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
//
|
||||||
|
// `key` - string - the key to the hash map.
|
||||||
|
//
|
||||||
|
// `seconds` - int - number of seconds until expiration.
|
||||||
|
//
|
||||||
|
// `ExOpt` - ExpireOptions - One of NX, XX, GT, LT.
|
||||||
|
//
|
||||||
|
// `fields` - ...string - a list of fields to set expiration of.
|
||||||
|
//
|
||||||
|
// Returns: an integer array representing the outcome of the commmand for each field.
|
||||||
|
// - Integer reply: -2 if no such field exists in the provided hash key, or the provided key does not exist.
|
||||||
|
// - Integer reply: 0 if the specified NX | XX | GT | LT condition has not been met.
|
||||||
|
// - Integer reply: 1 if the expiration time was set/updated.
|
||||||
|
// - Integer reply: 2 when HEXPIRE/HPEXPIRE is called with 0 seconds
|
||||||
|
//
|
||||||
|
// Errors:
|
||||||
|
//
|
||||||
|
// "value of key <key> is not a hash" - when the provided key is not a hash.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTL gets the expiration for the provided field(s) in a hash map.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
//
|
||||||
|
// `key` - string - the key to the hash map.
|
||||||
|
//
|
||||||
|
// `fields` - ...string - a list of fields to get TTL for.
|
||||||
|
//
|
||||||
|
// Returns: an integer array representing the outcome of the commmand for each field.
|
||||||
|
// - Integer reply: the TTL in seconds.
|
||||||
|
// - Integer reply: -2 if no such field exists in the provided hash key, or the provided key does not exist.
|
||||||
|
// - Integer reply: -1 if the field exists but has no associated expiration set.
|
||||||
|
//
|
||||||
|
// Errors:
|
||||||
|
//
|
||||||
|
// "value of key <key> is not a hash" - when the provided key is not a hash.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/echovault/sugardb/internal/modules/hash"
|
"github.com/echovault/sugardb/internal/modules/hash"
|
||||||
)
|
)
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -31,6 +32,7 @@ import (
|
|||||||
"github.com/echovault/sugardb/internal"
|
"github.com/echovault/sugardb/internal"
|
||||||
"github.com/echovault/sugardb/internal/constants"
|
"github.com/echovault/sugardb/internal/constants"
|
||||||
"github.com/echovault/sugardb/internal/eviction"
|
"github.com/echovault/sugardb/internal/eviction"
|
||||||
|
"github.com/echovault/sugardb/internal/modules/hash"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SwapDBs swaps every TCP client connection from database1 over to database2.
|
// SwapDBs swaps every TCP client connection from database1 over to database2.
|
||||||
@@ -146,6 +148,22 @@ func (server *SugarDB) getExpiry(ctx context.Context, key string) time.Time {
|
|||||||
return entry.ExpireAt
|
return entry.ExpireAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (server *SugarDB) getHashExpiry(ctx context.Context, key string, field string) time.Time {
|
||||||
|
server.storeLock.RLock()
|
||||||
|
defer server.storeLock.RUnlock()
|
||||||
|
|
||||||
|
database := ctx.Value("Database").(int)
|
||||||
|
|
||||||
|
entry, ok := server.store[database][key]
|
||||||
|
if !ok {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := entry.Value.(hash.Hash)
|
||||||
|
|
||||||
|
return hash[field].ExpireAt
|
||||||
|
}
|
||||||
|
|
||||||
func (server *SugarDB) getValues(ctx context.Context, keys []string) map[string]interface{} {
|
func (server *SugarDB) getValues(ctx context.Context, keys []string) map[string]interface{} {
|
||||||
server.storeLock.Lock()
|
server.storeLock.Lock()
|
||||||
defer server.storeLock.Unlock()
|
defer server.storeLock.Unlock()
|
||||||
@@ -278,6 +296,30 @@ func (server *SugarDB) setExpiry(ctx context.Context, key string, expireAt time.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (server *SugarDB) setHashExpiry(ctx context.Context, key string, field string, expireAt time.Time) error {
|
||||||
|
server.storeLock.Lock()
|
||||||
|
defer server.storeLock.Unlock()
|
||||||
|
|
||||||
|
database := ctx.Value("Database").(int)
|
||||||
|
|
||||||
|
hashmap, ok := server.store[database][key].Value.(hash.Hash)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("setHashExpiry can only be used on keys whose value is a Hash")
|
||||||
|
}
|
||||||
|
hashmap[field] = hash.HashValue{
|
||||||
|
Value: hashmap[field].Value,
|
||||||
|
ExpireAt: expireAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
server.keysWithExpiry.rwMutex.Lock()
|
||||||
|
if !slices.Contains(server.keysWithExpiry.keys[database], key) {
|
||||||
|
server.keysWithExpiry.keys[database] = append(server.keysWithExpiry.keys[database], key)
|
||||||
|
}
|
||||||
|
server.keysWithExpiry.rwMutex.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (server *SugarDB) deleteKey(ctx context.Context, key string) error {
|
func (server *SugarDB) deleteKey(ctx context.Context, key string) error {
|
||||||
database := ctx.Value("Database").(int)
|
database := ctx.Value("Database").(int)
|
||||||
|
|
||||||
@@ -646,6 +688,31 @@ func (server *SugarDB) evictKeysWithExpiredTTL(ctx context.Context) error {
|
|||||||
server.storeLock.Lock()
|
server.storeLock.Lock()
|
||||||
defer server.storeLock.Unlock()
|
defer server.storeLock.Unlock()
|
||||||
for _, k := range keys {
|
for _, k := range keys {
|
||||||
|
|
||||||
|
// handle keys within a hash type value
|
||||||
|
value := server.store[database][k].Value
|
||||||
|
t := reflect.TypeOf(value)
|
||||||
|
if t.Kind() == reflect.Map {
|
||||||
|
|
||||||
|
hashkey, ok := server.store[database][k].Value.(hash.Hash)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Hash value should contain type HashValue, but type %s was found.", t.Elem().Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range hashkey {
|
||||||
|
if v.ExpireAt.Before(time.Now()) {
|
||||||
|
delete(hashkey, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if key is expired, move on if it's not
|
||||||
|
ExpireTime := server.store[database][k].ExpireAt
|
||||||
|
if ExpireTime.Before(time.Now()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Delete the expired key
|
// Delete the expired key
|
||||||
deletedCount += 1
|
deletedCount += 1
|
||||||
if !server.isInCluster() {
|
if !server.isInCluster() {
|
||||||
|
|||||||
@@ -44,9 +44,11 @@ func (server *SugarDB) getHandlerFuncParams(ctx context.Context, cmd []string, c
|
|||||||
Connection: conn,
|
Connection: conn,
|
||||||
KeysExist: server.keysExist,
|
KeysExist: server.keysExist,
|
||||||
GetExpiry: server.getExpiry,
|
GetExpiry: server.getExpiry,
|
||||||
|
GetHashExpiry: server.getHashExpiry,
|
||||||
GetValues: server.getValues,
|
GetValues: server.getValues,
|
||||||
SetValues: server.setValues,
|
SetValues: server.setValues,
|
||||||
SetExpiry: server.setExpiry,
|
SetExpiry: server.setExpiry,
|
||||||
|
SetHashExpiry: server.setHashExpiry,
|
||||||
TakeSnapshot: server.takeSnapshot,
|
TakeSnapshot: server.takeSnapshot,
|
||||||
GetLatestSnapshotTime: server.getLatestSnapshotTime,
|
GetLatestSnapshotTime: server.getLatestSnapshotTime,
|
||||||
RewriteAOF: server.rewriteAOF,
|
RewriteAOF: server.rewriteAOF,
|
||||||
|
|||||||
Reference in New Issue
Block a user