From e44040c9281a64cdef8d56dc435c8dae652fe947 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Fri, 10 Jul 2026 12:15:30 +0300 Subject: [PATCH 1/4] add COPY keyspace command --- internal/server/cmd_keyspace.go | 38 +++++++++++++++++++++++++++++++++ internal/server/commands.go | 2 ++ internal/server/server_test.go | 9 ++++++++ 3 files changed, 49 insertions(+) create mode 100644 internal/server/cmd_keyspace.go diff --git a/internal/server/cmd_keyspace.go b/internal/server/cmd_keyspace.go new file mode 100644 index 0000000..2645e73 --- /dev/null +++ b/internal/server/cmd_keyspace.go @@ -0,0 +1,38 @@ +package server + +import ( + "fmt" + "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() +} diff --git a/internal/server/commands.go b/internal/server/commands.go index 4070c9f..4b6282c 100644 --- a/internal/server/commands.go +++ b/internal/server/commands.go @@ -43,6 +43,7 @@ func (s *Server) registerCommands() { "TYPE": (*conn).cmdType, // strings +<<<<<<< HEAD "GET": (*conn).cmdGet, "SET": (*conn).cmdSet, "SETNX": (*conn).cmdSetNX, @@ -60,6 +61,7 @@ func (s *Server) registerCommands() { "INCRBYFLOAT": (*conn).cmdIncrByFloat, "MGET": (*conn).cmdMGet, "MSET": (*conn).cmdMSet, + "COPY": (*conn).cmdCopy, // hashes "HSET": (*conn).cmdHSet, diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 5c05d05..f1668a8 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -91,6 +91,15 @@ 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) { From c72ddcf1d6943844e38a0e7e0cbc6fd2d4c98fde Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Fri, 10 Jul 2026 16:53:32 +0300 Subject: [PATCH 2/4] add OBJECT ENCODING keyspace command --- internal/server/cmd_keyspace.go | 9 +++++++++ internal/server/commands.go | 28 ++++++++++++++-------------- internal/server/server.go | 8 ++++++++ 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/internal/server/cmd_keyspace.go b/internal/server/cmd_keyspace.go index 2645e73..5fc9a61 100644 --- a/internal/server/cmd_keyspace.go +++ b/internal/server/cmd_keyspace.go @@ -36,3 +36,12 @@ func (c *conn) cmdCopy(args []string) error { } 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])) +} diff --git a/internal/server/commands.go b/internal/server/commands.go index 4b6282c..c65a7d8 100644 --- a/internal/server/commands.go +++ b/internal/server/commands.go @@ -29,21 +29,21 @@ 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 -<<<<<<< HEAD "GET": (*conn).cmdGet, "SET": (*conn).cmdSet, "SETNX": (*conn).cmdSetNX, @@ -61,7 +61,7 @@ func (s *Server) registerCommands() { "INCRBYFLOAT": (*conn).cmdIncrByFloat, "MGET": (*conn).cmdMGet, "MSET": (*conn).cmdMSet, - "COPY": (*conn).cmdCopy, + "COPY": (*conn).cmdCopy, // hashes "HSET": (*conn).cmdHSet, diff --git a/internal/server/server.go b/internal/server/server.go index ddd88d9..416bc59 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -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.") From 6bd7f9d3b4637ef60cc2bccac961b9a0b3a28bf7 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Mon, 13 Jul 2026 16:33:50 +0300 Subject: [PATCH 3/4] add SORT keyspace command --- docs/commands.md | 6 ++- dump.rdb | Bin 0 -> 120 bytes internal/server/cmd_keyspace.go | 74 ++++++++++++++++++++++++++++++++ internal/server/commands.go | 1 + internal/server/server_test.go | 25 ++++++++--- 5 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 dump.rdb diff --git a/docs/commands.md b/docs/commands.md index 8c3a8f7..6aa6a1b 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -35,6 +35,7 @@ conventions; arguments in `<>` are required, `[]` optional. RAMen is single | `PERSIST ` | Remove the TTL. | | `KEYS ` | Glob-match keys (`*`, `?`, `[...]`). | | `TYPE ` | `string`/`hash`/`list`/`set`/`zset`/`vector`/`none`. | +| `OBJECT ENCODING ` | `string`/`hash`/`list`/`set`/`zset`/`vector`/`none`. | ## Strings @@ -45,6 +46,7 @@ conventions; arguments in `<>` are required, `[]` optional. RAMen is single | `SETEX ` | Set with a TTL in seconds; rejects a non-positive TTL. | | `PSETEX ` | Set with a TTL in milliseconds; rejects a non-positive TTL. | | `GET ` | Get a string. | +| `COPY [REPLACE]`| Copies from source to destination. `REPLACE` overwrites an existing destination. | | `GETSET ` | Set and return the old value. | | `APPEND ` | Append; returns new length. | | `STRLEN ` | Length of the string in bytes; `0` if missing. | @@ -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 @@ -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 diff --git a/dump.rdb b/dump.rdb new file mode 100644 index 0000000000000000000000000000000000000000..baf6df0ece91f029ab18ccd12a257dbc913ea646 GIT binary patch literal 120 zcmV-;0EholMMOzcFfcJO`U-YoY-?qCEp}yc1voA+E;;%Na%E&`b1hDb~$evVc`UrJ%Wn?XFWo^Ppt_}eD2w`t#En;DFWxxRb0Q&&|5(aZ)Z*paG0Rj&X a0000400aR60RjOA0ssHC=cnrgE=#^gN-7!v literal 0 HcmV?d00001 diff --git a/internal/server/cmd_keyspace.go b/internal/server/cmd_keyspace.go index 5fc9a61..9469eea 100644 --- a/internal/server/cmd_keyspace.go +++ b/internal/server/cmd_keyspace.go @@ -2,6 +2,8 @@ package server import ( "fmt" + "slices" + "strconv" "strings" "github.com/Rohit-Dnath/RAMen/internal/store" @@ -45,3 +47,75 @@ func (c *conn) cmdObjectEncoding(args []string) error { // 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, arg) + } + + if hasDesc { + res = append([]string{"DESC"}, res...) + } + 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) + } + + slices.Sort(items) + if len(args) < 3 { + return c.writeStringArray(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:]) + + for i := 0; i < len(args); i++ { + + switch strings.ToUpper(args[i]) { + case "DESC": + slices.Reverse(items) + case "LIMIT": + if i+2 >= len(args) { + return c.wrongArgs("sort limit") + } + start, err := strconv.Atoi(args[i+1]) + if err != nil { + c.writeError("invalid limit index " + args[i+1]) + } + end, err := strconv.Atoi(args[i+2]) + if err != nil { + c.writeError("invalid limit index " + args[i+2]) + } + + if end == -1 { + end = len(items) + } + + items = items[start:end] + i += 2 + case "ASC": + case "ALPHA": + default: + return c.writeError(fmt.Sprintf("syntax error, %s", strings.ToUpper(args[i]))) + } + } + + return c.writeStringArray(items) +} diff --git a/internal/server/commands.go b/internal/server/commands.go index c65a7d8..4a417b4 100644 --- a/internal/server/commands.go +++ b/internal/server/commands.go @@ -89,6 +89,7 @@ func (s *Server) registerCommands() { "LREM": (*conn).cmdLRem, "LTRIM": (*conn).cmdLTrim, "LINSERT": (*conn).cmdLInsert, + "SORT": (*conn).cmdSort, // sets "SADD": (*conn).cmdSAdd, diff --git a/internal/server/server_test.go b/internal/server/server_test.go index f1668a8..7ca6e58 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -106,15 +106,30 @@ 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", "4", "2", "3", "1") + if r := mustDo(t, cli, "LLEN", "nums"); r != int64(4) { 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) != 4 || r[0] != "4" || r[1] != "2" || r[2] != "3" || r[3] != "1" { t.Fatalf("LRANGE = %v", r) } - + r = mustDo(t, cli, "SORT", "nums").([]any) + if len(r) != 4 || r[0] != "1" || r[1] != "2" || r[2] != "3" || r[3] != "4" { + t.Fatalf("SORT = %v", r) + } + r = mustDo(t, cli, "SORT", "nums", "DESC").([]any) + if len(r) != 4 || r[0] != "4" || r[1] != "3" || r[2] != "2" || r[3] != "1" { + t.Fatalf("SORT = %v", r) + } + r = mustDo(t, cli, "SORT", "nums", "DESC", "LIMIT", "0", "2").([]any) + if len(r) != 2 || r[0] != "4" || r[1] != "3" { + t.Fatalf("SORT = %v", r) + } + r = mustDo(t, cli, "SORT", "nums", "LIMIT", "0", "2", "DESC").([]any) + if len(r) != 2 || r[0] != "4" || r[1] != "3" { + 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) From 9dce5851188be15a7dc1c7e25deccd42542225e6 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Wed, 15 Jul 2026 12:34:30 +0300 Subject: [PATCH 4/4] fix: SORT limit index bound, default sort on number --- dump.rdb | Bin 120 -> 0 bytes internal/server/cmd_keyspace.go | 70 +++++++++++++++++++++++++------- internal/server/server_test.go | 28 +++++++++---- 3 files changed, 75 insertions(+), 23 deletions(-) delete mode 100644 dump.rdb diff --git a/dump.rdb b/dump.rdb deleted file mode 100644 index baf6df0ece91f029ab18ccd12a257dbc913ea646..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 120 zcmV-;0EholMMOzcFfcJO`U-YoY-?qCEp}yc1voA+E;;%Na%E&`b1hDb~$evVc`UrJ%Wn?XFWo^Ppt_}eD2w`t#En;DFWxxRb0Q&&|5(aZ)Z*paG0Rj&X a0000400aR60RjOA0ssHC=cnrgE=#^gN-7!v diff --git a/internal/server/cmd_keyspace.go b/internal/server/cmd_keyspace.go index 9469eea..8a0a12d 100644 --- a/internal/server/cmd_keyspace.go +++ b/internal/server/cmd_keyspace.go @@ -3,6 +3,7 @@ package server import ( "fmt" "slices" + "sort" "strconv" "strings" @@ -58,7 +59,7 @@ func orderSortArgs(args []string) []string { continue } - res = append(res, arg) + res = append(res, strings.ToUpper(arg)) } if hasDesc { @@ -67,6 +68,24 @@ func orderSortArgs(args []string) []string { 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") @@ -77,41 +96,62 @@ func (c *conn) cmdSort(args []string) error { return c.storeErr(err) } - slices.Sort(items) - if len(args) < 3 { - return c.writeStringArray(items) - } + // 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] + }) - switch strings.ToUpper(args[i]) { + 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") } - start, err := strconv.Atoi(args[i+1]) + offset, err := strconv.Atoi(args[i+1]) if err != nil { - c.writeError("invalid limit index " + args[i+1]) + return c.writeError("invalid limit index " + args[i+1]) } - end, err := strconv.Atoi(args[i+2]) + count, err := strconv.Atoi(args[i+2]) if err != nil { - c.writeError("invalid limit index " + args[i+2]) + return c.writeError("invalid limit index " + args[i+2]) } - if end == -1 { - end = len(items) - } + if offset >= len(items) { + items = nil + } else { + if count < 0 { + count = len(items) - offset + } - items = items[start:end] + end := min(offset+count, len(items)) + items = items[offset:end] + } i += 2 case "ASC": - case "ALPHA": default: return c.writeError(fmt.Sprintf("syntax error, %s", strings.ToUpper(args[i]))) } diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 7ca6e58..80dfc42 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -106,28 +106,40 @@ func TestDataStructures(t *testing.T) { cli, cleanup := startTestServer(t) defer cleanup() - mustDo(t, cli, "RPUSH", "nums", "4", "2", "3", "1") - if r := mustDo(t, cli, "LLEN", "nums"); r != int64(4) { + 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", "nums", "0", "-1").([]any) - if len(r) != 4 || r[0] != "4" || r[1] != "2" || r[2] != "3" || r[3] != "1" { + 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) != 4 || r[0] != "1" || r[1] != "2" || r[2] != "3" || r[3] != "4" { + 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) != 4 || r[0] != "4" || r[1] != "3" || r[2] != "2" || r[3] != "1" { + 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", "0", "2").([]any) - if len(r) != 2 || r[0] != "4" || r[1] != "3" { + 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] != "4" || r[1] != "3" { + if len(r) != 2 || r[0] != "10" || r[1] != "2" { t.Fatalf("SORT = %v", r) } mustDo(t, cli, "HSET", "h", "f1", "v1", "f2", "v2")