impr: command - dbsize, ttl, type

This commit is contained in:
Anton
2024-05-04 20:54:30 +05:00
parent 1a54e3205b
commit 91ba6db411
9 changed files with 352 additions and 0 deletions

View File

@@ -23,6 +23,8 @@ func Parse(args [][]byte) (redis.Cmd, error) {
// server // server
case "command": case "command":
return server.ParseOK(b) return server.ParseOK(b)
case "dbsize":
return server.ParseDBSize(b)
case "flushdb": case "flushdb":
return key.ParseFlushDB(b) return key.ParseFlushDB(b)
case "info": case "info":
@@ -59,6 +61,10 @@ func Parse(args [][]byte) (redis.Cmd, error) {
return key.ParseRenameNX(b) return key.ParseRenameNX(b)
case "scan": case "scan":
return key.ParseScan(b) return key.ParseScan(b)
case "ttl":
return key.ParseTTL(b)
case "type":
return key.ParseType(b)
// list // list
case "lindex": case "lindex":

View File

@@ -0,0 +1,44 @@
package key
import (
"time"
"github.com/nalgeon/redka/internal/core"
"github.com/nalgeon/redka/internal/redis"
)
// Returns the expiration time in seconds of a key.
// TTL key
// https://redis.io/commands/ttl
type TTL struct {
redis.BaseCmd
key string
}
func ParseTTL(b redis.BaseCmd) (*TTL, error) {
cmd := &TTL{BaseCmd: b}
if len(cmd.Args()) != 1 {
return cmd, redis.ErrInvalidArgNum
}
cmd.key = string(cmd.Args()[0])
return cmd, nil
}
func (cmd *TTL) Run(w redis.Writer, red redis.Redka) (any, error) {
k, err := red.Key().Get(cmd.key)
if err == core.ErrNotFound {
w.WriteInt(-2)
return -2, nil
}
if err != nil {
w.WriteError(cmd.Error(err))
return nil, err
}
if k.ETime == nil {
w.WriteInt(-1)
return -1, nil
}
ttl := int(*k.ETime/1000 - time.Now().Unix())
w.WriteInt(ttl)
return ttl, nil
}

View File

@@ -0,0 +1,85 @@
package key
import (
"testing"
"time"
"github.com/nalgeon/redka/internal/redis"
"github.com/nalgeon/redka/internal/testx"
)
func TestTTLParse(t *testing.T) {
tests := []struct {
cmd string
key string
err error
}{
{
cmd: "ttl",
key: "",
err: redis.ErrInvalidArgNum,
},
{
cmd: "ttl name",
key: "name",
err: nil,
},
{
cmd: "ttl name age",
key: "",
err: redis.ErrInvalidArgNum,
},
}
for _, test := range tests {
t.Run(test.cmd, func(t *testing.T) {
cmd, err := redis.Parse(ParseTTL, test.cmd)
testx.AssertEqual(t, err, test.err)
if err == nil {
testx.AssertEqual(t, cmd.key, test.key)
}
})
}
}
func TestTTLExec(t *testing.T) {
t.Run("has ttl", func(t *testing.T) {
db, red := getDB(t)
defer db.Close()
_ = db.Str().SetExpires("name", "alice", 60*time.Second)
cmd := redis.MustParse(ParseTTL, "ttl name")
conn := redis.NewFakeConn()
res, err := cmd.Run(conn, red)
testx.AssertNoErr(t, err)
testx.AssertEqual(t, res, 60)
testx.AssertEqual(t, conn.Out(), "60")
})
t.Run("no ttl", func(t *testing.T) {
db, red := getDB(t)
defer db.Close()
_ = db.Str().Set("name", "alice")
cmd := redis.MustParse(ParseTTL, "ttl name")
conn := redis.NewFakeConn()
res, err := cmd.Run(conn, red)
testx.AssertNoErr(t, err)
testx.AssertEqual(t, res, -1)
testx.AssertEqual(t, conn.Out(), "-1")
})
t.Run("not found", func(t *testing.T) {
db, red := getDB(t)
defer db.Close()
cmd := redis.MustParse(ParseTTL, "ttl name")
conn := redis.NewFakeConn()
res, err := cmd.Run(conn, red)
testx.AssertNoErr(t, err)
testx.AssertEqual(t, res, -2)
testx.AssertEqual(t, conn.Out(), "-2")
})
}

View File

@@ -0,0 +1,37 @@
package key
import (
"github.com/nalgeon/redka/internal/core"
"github.com/nalgeon/redka/internal/redis"
)
// Determines the type of value stored at a key.
// TYPE key
// https://redis.io/commands/type
type Type struct {
redis.BaseCmd
key string
}
func ParseType(b redis.BaseCmd) (*Type, error) {
cmd := &Type{BaseCmd: b}
if len(cmd.Args()) != 1 {
return cmd, redis.ErrInvalidArgNum
}
cmd.key = string(cmd.Args()[0])
return cmd, nil
}
func (cmd *Type) Run(w redis.Writer, red redis.Redka) (any, error) {
k, err := red.Key().Get(cmd.key)
if err == core.ErrNotFound {
w.WriteString("none")
return "none", nil
}
if err != nil {
w.WriteError(cmd.Error(err))
return nil, err
}
w.WriteString(k.TypeName())
return k.TypeName(), nil
}

View File

@@ -0,0 +1,74 @@
package key
import (
"testing"
"github.com/nalgeon/redka/internal/redis"
"github.com/nalgeon/redka/internal/testx"
)
func TestTypeParse(t *testing.T) {
tests := []struct {
cmd string
key string
err error
}{
{
cmd: "type",
key: "",
err: redis.ErrInvalidArgNum,
},
{
cmd: "type name",
key: "name",
err: nil,
},
{
cmd: "type name age",
key: "",
err: redis.ErrInvalidArgNum,
},
}
for _, test := range tests {
t.Run(test.cmd, func(t *testing.T) {
cmd, err := redis.Parse(ParseType, test.cmd)
testx.AssertEqual(t, err, test.err)
if err == nil {
testx.AssertEqual(t, cmd.key, test.key)
}
})
}
}
func TestTypeExec(t *testing.T) {
db, red := getDB(t)
defer db.Close()
_ = db.Str().Set("kstr", "string")
_, _ = db.List().PushBack("klist", "list")
_, _ = db.Hash().Set("khash", "field", "hash")
_, _ = db.ZSet().Add("kzset", "zset", 1)
tests := []struct {
key string
want string
}{
{key: "kstr", want: "string"},
{key: "klist", want: "list"},
{key: "khash", want: "hash"},
{key: "kzset", want: "zset"},
{key: "knone", want: "none"},
}
for _, test := range tests {
t.Run(test.key, func(t *testing.T) {
cmd := redis.MustParse(ParseType, "type "+test.key)
conn := redis.NewFakeConn()
res, err := cmd.Run(conn, red)
testx.AssertNoErr(t, err)
testx.AssertEqual(t, res, test.want)
testx.AssertEqual(t, conn.Out(), test.want)
})
}
}

View File

@@ -0,0 +1,28 @@
package server
import "github.com/nalgeon/redka/internal/redis"
// Returns the number of keys in the database.
// DBSIZE
// https://redis.io/commands/dbsize
type DBSize struct {
redis.BaseCmd
}
func ParseDBSize(b redis.BaseCmd) (*DBSize, error) {
cmd := &DBSize{BaseCmd: b}
if len(cmd.Args()) != 0 {
return cmd, redis.ErrInvalidArgNum
}
return cmd, nil
}
func (cmd *DBSize) Run(w redis.Writer, red redis.Redka) (any, error) {
n, err := red.Key().Len()
if err != nil {
w.WriteError(cmd.Error(err))
return nil, err
}
w.WriteInt(n)
return n, nil
}

View File

@@ -0,0 +1,60 @@
package server
import (
"testing"
"github.com/nalgeon/redka/internal/redis"
"github.com/nalgeon/redka/internal/testx"
)
func TestDBSizeParse(t *testing.T) {
tests := []struct {
cmd string
err error
}{
{
cmd: "dbsize",
err: nil,
},
{
cmd: "dbsize name",
err: redis.ErrInvalidArgNum,
},
}
for _, test := range tests {
t.Run(test.cmd, func(t *testing.T) {
_, err := redis.Parse(ParseDBSize, test.cmd)
testx.AssertEqual(t, err, test.err)
})
}
}
func TestDBSizeExec(t *testing.T) {
t.Run("dbsize", func(t *testing.T) {
db, red := getDB(t)
defer db.Close()
_ = db.Str().Set("name", "alice")
_ = db.Str().Set("age", 25)
cmd := redis.MustParse(ParseDBSize, "dbsize")
conn := redis.NewFakeConn()
res, err := cmd.Run(conn, red)
testx.AssertNoErr(t, err)
testx.AssertEqual(t, res, 2)
testx.AssertEqual(t, conn.Out(), "2")
})
t.Run("empty", func(t *testing.T) {
db, red := getDB(t)
defer db.Close()
cmd := redis.MustParse(ParseDBSize, "dbsize")
conn := redis.NewFakeConn()
res, err := cmd.Run(conn, red)
testx.AssertNoErr(t, err)
testx.AssertEqual(t, res, 0)
testx.AssertEqual(t, conn.Out(), "0")
})
}

View File

@@ -0,0 +1,17 @@
package server
import (
"testing"
"github.com/nalgeon/redka"
"github.com/nalgeon/redka/internal/redis"
)
func getDB(tb testing.TB) (*redka.DB, redis.Redka) {
tb.Helper()
db, err := redka.Open(":memory:", nil)
if err != nil {
tb.Fatal(err)
}
return db, redis.RedkaDB(db)
}

View File

@@ -40,6 +40,7 @@ type RKey interface {
ExpireAt(key string, at time.Time) error ExpireAt(key string, at time.Time) error
Get(key string) (core.Key, error) Get(key string) (core.Key, error)
Keys(pattern string) ([]core.Key, error) Keys(pattern string) ([]core.Key, error)
Len() (int, error)
Persist(key string) error Persist(key string) error
Random() (core.Key, error) Random() (core.Key, error)
Rename(key, newKey string) error Rename(key, newKey string) error