Skip to content

Commit

Permalink
Add key prefix matching to KEYS command (#237)
Browse files Browse the repository at this point in the history
Related to #234 and !236.

This is the implementation that was requested in the original issue. I updated KEYS command to be redis-valid and implemented prefix search. There is also a rather interesting test, I could you use some feedback here.

I noticed that it might not be possible to reduce the complexity of the KEYS command. Because even if you use Scan, you will have to store the counter of all found keys before you do WriteBulk of the actual keys.

@prologic here is what you probably had in mind:

```
s.db.Scan([]byte(prefix), func(key []byte) error {
	conn.WriteBulk(key)
	return nil
})
```

But there is no way to call `conn.WriteArray(n)` with the number of keys until you iterate through all of them, hence the second loop over found keys.

Co-authored-by: Ivan Elfimov <[email protected]>
Co-authored-by: James Mills <[email protected]>
Reviewed-on: https://git.mills.io/prologic/bitcask/pulls/237
Reviewed-by: James Mills <[email protected]>
Co-authored-by: biozz <biozz@[email protected]>
Co-committed-by: biozz <biozz@[email protected]>
  • Loading branch information
3 people committed Sep 20, 2021
1 parent 2279245 commit 21a824e
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 3 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ Yash Chandra <[email protected]>
Yury Fedorov orlangure
o2gy84 <[email protected]>
garsue <[email protected]>
biozz <[email protected]>
37 changes: 34 additions & 3 deletions cmd/bitcaskd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,41 @@ func (s *server) handleGet(cmd redcon.Command, conn redcon.Conn) {
}

func (s *server) handleKeys(cmd redcon.Command, conn redcon.Conn) {
conn.WriteArray(s.db.Len())
for key := range s.db.Keys() {
conn.WriteBulk(key)
if len(cmd.Args) != 2 {
conn.WriteError("ERR wrong number of arguments for '" + string(cmd.Args[0]) + "' command")
return
}

pattern := string(cmd.Args[1])

// Fast-track condition for improved speed
if pattern == "*" {
conn.WriteArray(s.db.Len())
for key := range s.db.Keys() {
conn.WriteBulk(key)
}
return
}

// Prefix handling
if strings.Count(pattern, "*") == 1 && strings.HasSuffix(pattern, "*") {
prefix := strings.ReplaceAll(pattern, "*", "")
count := 0
keys := make([][]byte, 0)
s.db.Scan([]byte(prefix), func(key []byte) error {
keys = append(keys, key)
count++
return nil
})
conn.WriteArray(count)
for _, key := range keys {
conn.WriteBulk(key)
}
return
}

// No results means empty array
conn.WriteArray(0)
}

func (s *server) handleExists(cmd redcon.Command, conn redcon.Conn) {
Expand Down
102 changes: 102 additions & 0 deletions cmd/bitcaskd/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package main

import (
"net"
"strconv"
"testing"

"github.com/tidwall/redcon"
)

func TestHandleKeys(t *testing.T) {
s, err := newServer(":61234", "./test.db")
if err != nil {
t.Fatalf("Unable to create server: %v", err)
}
s.db.Put([]byte("foo"), []byte("bar"))
testCases := []TestCase{
{
Command: redcon.Command{
Raw: []byte("KEYS *"),
Args: [][]byte{[]byte("KEYS"), []byte("*")},
},
Expected: "1,foo",
},
{
Command: redcon.Command{
Raw: []byte("KEYS fo*"),
Args: [][]byte{[]byte("KEYS"), []byte("fo*")},
},
Expected: "1,foo",
},
{
Command: redcon.Command{
Raw: []byte("KEYS ba*"),
Args: [][]byte{[]byte("KEYS"), []byte("ba*")},
},
Expected: "0",
},
{
Command: redcon.Command{
Raw: []byte("KEYS *oo"),
Args: [][]byte{[]byte("KEYS"), []byte("*oo")},
},
Expected: "0",
},
}
for _, testCase := range testCases {
conn := DummyConn{}
s.handleKeys(testCase.Command, &conn)
if testCase.Expected != conn.Result {
t.Fatalf("s.handleKeys failed: expected '%s', got '%s'", testCase.Expected, conn.Result)
}
}
}

type TestCase struct {
Command redcon.Command
Expected string
}

type DummyConn struct {
Result string
}

func (dc *DummyConn) RemoteAddr() string {
return ""
}
func (dc *DummyConn) Close() error {
return nil
}
func (dc *DummyConn) WriteError(msg string) {}
func (dc *DummyConn) WriteString(str string) {}
func (dc *DummyConn) WriteBulk(bulk []byte) {
dc.Result += "," + string(bulk)
}
func (dc *DummyConn) WriteBulkString(bulk string) {}
func (dc *DummyConn) WriteInt(num int) {}
func (dc *DummyConn) WriteInt64(num int64) {}
func (dc *DummyConn) WriteUint64(num uint64) {}
func (dc *DummyConn) WriteArray(count int) {
dc.Result = strconv.Itoa(count)
}
func (dc *DummyConn) WriteNull() {}
func (dc *DummyConn) WriteRaw(data []byte) {}
func (dc *DummyConn) WriteAny(any interface{}) {}
func (dc *DummyConn) Context() interface{} {
return nil
}
func (dc *DummyConn) SetContext(v interface{}) {}
func (dc *DummyConn) SetReadBuffer(bytes int) {}
func (dc *DummyConn) Detach() redcon.DetachedConn {
return nil
}
func (dc *DummyConn) ReadPipeline() []redcon.Command {
return nil
}
func (dc *DummyConn) PeekPipeline() []redcon.Command {
return nil
}
func (dc *DummyConn) NetConn() net.Conn {
return nil
}

0 comments on commit 21a824e

Please sign in to comment.