From 91ba6db411871f98576c9e824b3fa8045e4df071 Mon Sep 17 00:00:00 2001 From: Anton Date: Sat, 4 May 2024 20:54:30 +0500 Subject: [PATCH] impr: command - dbsize, ttl, type --- internal/command/command.go | 6 ++ internal/command/key/ttl.go | 44 +++++++++++++ internal/command/key/ttl_test.go | 85 ++++++++++++++++++++++++++ internal/command/key/type.go | 37 +++++++++++ internal/command/key/type_test.go | 74 ++++++++++++++++++++++ internal/command/server/dbsize.go | 28 +++++++++ internal/command/server/dbsize_test.go | 60 ++++++++++++++++++ internal/command/server/server_test.go | 17 ++++++ internal/redis/redka.go | 1 + 9 files changed, 352 insertions(+) create mode 100644 internal/command/key/ttl.go create mode 100644 internal/command/key/ttl_test.go create mode 100644 internal/command/key/type.go create mode 100644 internal/command/key/type_test.go create mode 100644 internal/command/server/dbsize.go create mode 100644 internal/command/server/dbsize_test.go create mode 100644 internal/command/server/server_test.go diff --git a/internal/command/command.go b/internal/command/command.go index b523744..c1027cc 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -23,6 +23,8 @@ func Parse(args [][]byte) (redis.Cmd, error) { // server case "command": return server.ParseOK(b) + case "dbsize": + return server.ParseDBSize(b) case "flushdb": return key.ParseFlushDB(b) case "info": @@ -59,6 +61,10 @@ func Parse(args [][]byte) (redis.Cmd, error) { return key.ParseRenameNX(b) case "scan": return key.ParseScan(b) + case "ttl": + return key.ParseTTL(b) + case "type": + return key.ParseType(b) // list case "lindex": diff --git a/internal/command/key/ttl.go b/internal/command/key/ttl.go new file mode 100644 index 0000000..0525cbc --- /dev/null +++ b/internal/command/key/ttl.go @@ -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 +} diff --git a/internal/command/key/ttl_test.go b/internal/command/key/ttl_test.go new file mode 100644 index 0000000..2c3684e --- /dev/null +++ b/internal/command/key/ttl_test.go @@ -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") + }) +} diff --git a/internal/command/key/type.go b/internal/command/key/type.go new file mode 100644 index 0000000..7f61933 --- /dev/null +++ b/internal/command/key/type.go @@ -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 +} diff --git a/internal/command/key/type_test.go b/internal/command/key/type_test.go new file mode 100644 index 0000000..25094d6 --- /dev/null +++ b/internal/command/key/type_test.go @@ -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) + }) + } +} diff --git a/internal/command/server/dbsize.go b/internal/command/server/dbsize.go new file mode 100644 index 0000000..f0bea5b --- /dev/null +++ b/internal/command/server/dbsize.go @@ -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 +} diff --git a/internal/command/server/dbsize_test.go b/internal/command/server/dbsize_test.go new file mode 100644 index 0000000..4b6ed8f --- /dev/null +++ b/internal/command/server/dbsize_test.go @@ -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") + }) +} diff --git a/internal/command/server/server_test.go b/internal/command/server/server_test.go new file mode 100644 index 0000000..3501150 --- /dev/null +++ b/internal/command/server/server_test.go @@ -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) +} diff --git a/internal/redis/redka.go b/internal/redis/redka.go index b4f991a..4023f00 100644 --- a/internal/redis/redka.go +++ b/internal/redis/redka.go @@ -40,6 +40,7 @@ type RKey interface { ExpireAt(key string, at time.Time) error Get(key string) (core.Key, error) Keys(pattern string) ([]core.Key, error) + Len() (int, error) Persist(key string) error Random() (core.Key, error) Rename(key, newKey string) error