Skip to content
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

Add commands HEXPIRE and HTTL #148

Merged
merged 8 commits into from
Nov 22, 2024
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ Benchmark script options:
## HASH
* [HDEL](https://sugardb.io/docs/commands/hash/hdel)
* [HEXISTS](https://sugardb.io/docs/commands/hash/hexists)
* [HEXPIRE](https://sugardb.io/docs/commands/hash/hexpire)
* [HGET](https://sugardb.io/docs/commands/hash/hget)
* [HGETALL](https://sugardb.io/docs/commands/hash/hgetall)
* [HINCRBY](https://sugardb.io/docs/commands/hash/hincrby)
Expand All @@ -251,6 +252,7 @@ Benchmark script options:
* [HSET](https://sugardb.io/docs/commands/hash/hset)
* [HSETNX](https://sugardb.io/docs/commands/hash/hsetnx)
* [HSTRLEN](https://sugardb.io/docs/commands/hash/hstrlen)
* [HTTL](https://sugardb.io/docs/commands/hash/httl)
* [HVALS](https://sugardb.io/docs/commands/hash/hvals)

<a name="commands-list"></a>
Expand Down
5,714 changes: 2,857 additions & 2,857 deletions coverage/coverage.out

Large diffs are not rendered by default.

48 changes: 48 additions & 0 deletions docs/docs/commands/hash/hexpire.mdx
Original file line number Diff line number Diff line change
@@ -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 changes: 48 additions & 0 deletions docs/docs/commands/hash/httl.mdx
Original file line number Diff line number Diff line change
@@ -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>
6 changes: 4 additions & 2 deletions internal/constants/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,10 @@ const (
)

const (
OkResponse = "+OK\r\n"
WrongArgsResponse = "wrong number of arguments"
OkResponse = "+OK\r\n"
WrongArgsResponse = "wrong number of arguments"
MissingArgResponse = "missing argument %s"
InvalidCmdResponse = "invalid command provided"
)

const (
Expand Down
249 changes: 238 additions & 11 deletions internal/modules/hash/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ package hash
import (
"errors"
"fmt"
"github.com/echovault/sugardb/internal"
"github.com/echovault/sugardb/internal/constants"
"math/rand"
"slices"
"strconv"
"strings"
"time"

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

func handleHSET(params internal.HandlerFuncParams) ([]byte, error) {
Expand Down Expand Up @@ -611,6 +613,222 @@ func handleHDEL(params internal.HandlerFuncParams) ([]byte, error) {
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 {
return []internal.Command{
{
Expand Down Expand Up @@ -744,14 +962,23 @@ Return the string length of the values stored at the specified fields. 0 if the
KeyExtractionFunc: hdelKeyFunc,
HandlerFunc: handleHDEL,
},
// {
// Command: "hexpire",
// Module: constants.HashModule,
// 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.`,
// Sync: true,
// KeyExtractionFunc: hexpireKeyFunc,
// HandlerFunc: handleHEXPIRE,
// },
{
Command: "hexpire",
Module: constants.HashModule,
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.`,
Sync: true,
KeyExtractionFunc: hexpireKeyFunc,
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,
},
}
}
Loading