Skip to content
Open
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
6 changes: 5 additions & 1 deletion docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ conventions; arguments in `<>` are required, `[]` optional. RAMen is single
| `PERSIST <key>` | Remove the TTL. |
| `KEYS <pattern>` | Glob-match keys (`*`, `?`, `[...]`). |
| `TYPE <key>` | `string`/`hash`/`list`/`set`/`zset`/`vector`/`none`. |
| `OBJECT ENCODING <key>` | `string`/`hash`/`list`/`set`/`zset`/`vector`/`none`. |

## Strings

Expand All @@ -45,6 +46,7 @@ conventions; arguments in `<>` are required, `[]` optional. RAMen is single
| `SETEX <key> <seconds> <value>` | Set with a TTL in seconds; rejects a non-positive TTL. |
| `PSETEX <key> <ms> <value>` | Set with a TTL in milliseconds; rejects a non-positive TTL. |
| `GET <key>` | Get a string. |
| `COPY <source> <destination> [REPLACE]`| Copies from source to destination. `REPLACE` overwrites an existing destination. |
| `GETSET <key> <value>` | Set and return the old value. |
| `APPEND <key> <value>` | Append; returns new length. |
| `STRLEN <key>` | Length of the string in bytes; `0` if missing. |
Expand All @@ -71,7 +73,7 @@ HGETALL user:1

## Lists

`LPUSH`, `RPUSH`, `LPOP`, `RPOP`, `LLEN`, `LINDEX`, `LRANGE`, `LSET`, `LREM`, `LTRIM`, `LINSERT`.
`LPUSH`, `RPUSH`, `LPOP`, `RPOP`, `LLEN`, `LINDEX`, `LRANGE`, `LSET`, `LREM`, `LTRIM`, `LINSERT`, `SORT`.

```
RPUSH q a b c
Expand All @@ -81,6 +83,8 @@ LSET q 0 A # overwrite index 0 (errors if the key or index is missin
LREM q 1 A # remove up to 1 "A" from the head; -1 would start at the tail, 0 removes all
LTRIM q 0 1 # keep only indexes 0..1, drop the rest
LINSERT q BEFORE b Z # insert Z before the first "b"; returns new length, or -1 if b is missing
SORT q
SORT q DESC|ASC LIMIT 0 -1
```

## Sets
Expand Down
161 changes: 161 additions & 0 deletions internal/server/cmd_keyspace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package server

import (
"fmt"
"slices"
"sort"
"strconv"
"strings"

"github.com/Rohit-Dnath/RAMen/internal/store"
)

func (c *conn) cmdCopy(args []string) error {
if len(args) < 3 || len(args) > 4 {
return c.wrongArgs("copy")
}

src, dst := args[1], args[2]
replace := len(args) == 4 && strings.ToUpper(args[3]) == "REPLACE"

v, ok, err := c.s.store.Get(src)
if err != nil {
return c.storeErr(err)
}
if !ok {
return c.storeErr(fmt.Errorf("invalid source to copy from"))
}

_, ok, err = c.s.store.Get(dst)
if err != nil {
return c.storeErr(err)
}
if ok && replace == false {
return c.storeErr(fmt.Errorf("0"))
}

if c.s.store.Set(dst, v, store.SetOptions{}) {
return c.writeSimple("1")
}
return c.writeNull()
}

func (c *conn) cmdObjectEncoding(args []string) error {
if len(args) != 3 {
return c.wrongArgs("object encoding")
}

// currently behaves similar to `type` command because store doesn't use different internal representation
return c.writeSimple(c.s.store.Type(args[2]))
}

func orderSortArgs(args []string) []string {
res := []string{}
hasDesc := false

for _, arg := range args {
if strings.ToUpper(arg) == "DESC" {
hasDesc = true
continue
}

res = append(res, strings.ToUpper(arg))
}

if hasDesc {
res = append([]string{"DESC"}, res...)
}
return res
}

func checkArray(vals []string) bool {
for _, val := range vals {
_, err := strconv.ParseFloat(val, 64)
if err != nil {
return false
}
}
return true
}

func numsToString(nums []float64) []string {
res := []string{}
for _, num := range nums {
res = append(res, strconv.FormatFloat(num, 'f', -1, 64))
}
return res
}

func (c *conn) cmdSort(args []string) error {
if len(args) < 2 {
return c.wrongArgs("sort")
}

items, err := c.s.store.LRange(args[1], 0, -1)
if err != nil {
return c.storeErr(err)
}

// since default sort is by number all item values have to be a number
numSortable := checkArray(items)

// order of execution for the args matters even though order layout maybe unordered
// example: LIMIT start end DESC output is actually DESC LIMIT start end
args = orderSortArgs(args[2:])

isAlpha := slices.Contains(args, "ALPHA")
if numSortable && !isAlpha {
// only for internal purpose
args = append([]string{"NUMERIC"}, args...)
}

for i := 0; i < len(args); i++ {
switch args[i] {
case "NUMERIC":
nums := []float64{}
for _, val := range items {
n, _ := strconv.ParseFloat(val, 64)
nums = append(nums, n)
}

sort.Slice(nums, func(i, j int) bool {
return nums[i] < nums[j]
})

items = numsToString(nums)
case "ALPHA":
slices.Sort(items)
case "DESC":
slices.Reverse(items)
case "LIMIT":
if i+2 >= len(args) {
return c.wrongArgs("sort limit")
}
offset, err := strconv.Atoi(args[i+1])
if err != nil {
return c.writeError("invalid limit index " + args[i+1])
}
count, err := strconv.Atoi(args[i+2])
if err != nil {
return c.writeError("invalid limit index " + args[i+2])
}

if offset >= len(items) {
items = nil
} else {
if count < 0 {
count = len(items) - offset
}

end := min(offset+count, len(items))
items = items[offset:end]
}
i += 2
case "ASC":
default:
return c.writeError(fmt.Sprintf("syntax error, %s", strings.ToUpper(args[i])))
}
}

return c.writeStringArray(items)
}
27 changes: 15 additions & 12 deletions internal/server/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,19 @@ func (s *Server) registerCommands() {
"BGSAVE": (*conn).cmdSave,

// generic keyspace
"DEL": (*conn).cmdDel,
"EXISTS": (*conn).cmdExists,
"EXPIRE": (*conn).cmdExpire,
"PEXPIRE": (*conn).cmdPExpire,
"EXPIREAT": (*conn).cmdExpireAt,
"PEXPIREAT": (*conn).cmdPExpireAt,
"TTL": (*conn).cmdTTL,
"PTTL": (*conn).cmdPTTL,
"EXPIRETIME": (*conn).cmdExpireTime,
"PERSIST": (*conn).cmdPersist,
"KEYS": (*conn).cmdKeys,
"TYPE": (*conn).cmdType,
"DEL": (*conn).cmdDel,
"EXISTS": (*conn).cmdExists,
"EXPIRE": (*conn).cmdExpire,
"PEXPIRE": (*conn).cmdPExpire,
"EXPIREAT": (*conn).cmdExpireAt,
"PEXPIREAT": (*conn).cmdPExpireAt,
"TTL": (*conn).cmdTTL,
"PTTL": (*conn).cmdPTTL,
"EXPIRETIME": (*conn).cmdExpireTime,
"PERSIST": (*conn).cmdPersist,
"KEYS": (*conn).cmdKeys,
"TYPE": (*conn).cmdType,
"OBJECT ENCODING": (*conn).cmdObjectEncoding,

// strings
"GET": (*conn).cmdGet,
Expand All @@ -60,6 +61,7 @@ func (s *Server) registerCommands() {
"INCRBYFLOAT": (*conn).cmdIncrByFloat,
"MGET": (*conn).cmdMGet,
"MSET": (*conn).cmdMSet,
"COPY": (*conn).cmdCopy,

// hashes
"HSET": (*conn).cmdHSet,
Expand Down Expand Up @@ -87,6 +89,7 @@ func (s *Server) registerCommands() {
"LREM": (*conn).cmdLRem,
"LTRIM": (*conn).cmdLTrim,
"LINSERT": (*conn).cmdLInsert,
"SORT": (*conn).cmdSort,

// sets
"SADD": (*conn).cmdSAdd,
Expand Down
8 changes: 8 additions & 0 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ func (s *Server) dispatchCommand(c *conn, args []string) error {
s.stats.Commands.Add(1)
name := strings.ToUpper(args[0])

// multiple length commands
switch strings.ToUpper(name) {
case "OBJECT":
if len(args) > 1 {
name += " " + strings.ToUpper(args[1])
}
}

// When not authenticated, only AUTH (and QUIT) are permitted.
if s.cfg.Password != "" && !c.authed && name != "AUTH" && name != "QUIT" {
return c.writeError("NOAUTH Authentication required.")
Expand Down
46 changes: 41 additions & 5 deletions internal/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,21 +91,57 @@ func TestCoreCommands(t *testing.T) {
if r, _ := cli.Do("GET", "foo"); r != nil {
t.Fatalf("GET after DEL = %v", r)
}
mustDo(t, cli, "SET", "src", "value")
mustError(t, cli, "COPY", "src1", "dst")
if r := mustDo(t, cli, "COPY", "src", "dst"); r != "1" {
t.Fatalf("COPY = %v", r)
}
mustError(t, cli, "COPY", "src", "dst")
if r := mustDo(t, cli, "COPY", "src", "dst", "REPLACE"); r != "1" {
t.Fatalf("COPY with REPLACE = %v", r)
}
}

func TestDataStructures(t *testing.T) {
cli, cleanup := startTestServer(t)
defer cleanup()

mustDo(t, cli, "RPUSH", "l", "a", "b", "c")
if r := mustDo(t, cli, "LLEN", "l"); r != int64(3) {
mustDo(t, cli, "RPUSH", "nums", "1", "10", "2")
if r := mustDo(t, cli, "LLEN", "nums"); r != int64(3) {
t.Fatalf("LLEN = %v", r)
}
r := mustDo(t, cli, "LRANGE", "l", "0", "-1").([]any)
if len(r) != 3 || r[0] != "a" || r[2] != "c" {
r := mustDo(t, cli, "LRANGE", "nums", "0", "-1").([]any)
if len(r) != 3 || r[0] != "1" || r[1] != "10" || r[2] != "2" {
t.Fatalf("LRANGE = %v", r)
}

r = mustDo(t, cli, "SORT", "nums").([]any)
if len(r) != 3 || r[0] != "1" || r[1] != "2" || r[2] != "10" {
t.Fatalf("SORT = %v", r)
}
r = mustDo(t, cli, "SORT", "nums", "ALPHA").([]any)
if len(r) != 3 || r[0] != "1" || r[1] != "10" || r[2] != "2" {
t.Fatalf("SORT = %v", r)
}
r = mustDo(t, cli, "SORT", "nums", "LIMIT", "1", "2").([]any)
if len(r) != 2 || r[0] != "2" || r[1] != "10" {
t.Fatalf("SORT = %v", r)
}
r = mustDo(t, cli, "SORT", "nums", "LIMIT", "1", "100").([]any)
if len(r) != 2 || r[0] != "2" || r[1] != "10" {
t.Fatalf("SORT = %v", r)
}
r = mustDo(t, cli, "SORT", "nums", "DESC").([]any)
if len(r) != 3 || r[0] != "10" || r[1] != "2" || r[2] != "1" {
t.Fatalf("SORT = %v", r)
}
r = mustDo(t, cli, "SORT", "nums", "DESC", "LIMIT", "1", "2").([]any)
if len(r) != 2 || r[0] != "2" || r[1] != "1" {
t.Fatalf("SORT = %v", r)
}
r = mustDo(t, cli, "SORT", "nums", "LIMIT", "0", "2", "DESC").([]any)
if len(r) != 2 || r[0] != "10" || r[1] != "2" {
t.Fatalf("SORT = %v", r)
}
mustDo(t, cli, "HSET", "h", "f1", "v1", "f2", "v2")
if r := mustDo(t, cli, "HGET", "h", "f1"); r != "v1" {
t.Fatalf("HGET = %v", r)
Expand Down
Loading