Skip to content

Commit f9fa4db

Browse files
committed
add scan command
1 parent ff67ac3 commit f9fa4db

File tree

6 files changed

+265
-11
lines changed

6 files changed

+265
-11
lines changed

database/keys.go

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,26 +59,36 @@ func execFlushDB(db *DB, args [][]byte) redis.Reply {
5959
return &protocol.OkReply{}
6060
}
6161

62-
// execType returns the type of entity, including: string, list, hash, set and zset
63-
func execType(db *DB, args [][]byte) redis.Reply {
64-
key := string(args[0])
62+
// returns the type of entity, including: string, list, hash, set and zset
63+
func getType(db *DB, key string) string {
6564
entity, exists := db.GetEntity(key)
6665
if !exists {
67-
return protocol.MakeStatusReply("none")
66+
return "none"
6867
}
6968
switch entity.Data.(type) {
7069
case []byte:
71-
return protocol.MakeStatusReply("string")
70+
return "string"
7271
case list.List:
73-
return protocol.MakeStatusReply("list")
72+
return "list"
7473
case dict.Dict:
75-
return protocol.MakeStatusReply("hash")
74+
return "hash"
7675
case *set.Set:
77-
return protocol.MakeStatusReply("set")
76+
return "set"
7877
case *sortedset.SortedSet:
79-
return protocol.MakeStatusReply("zset")
78+
return "zset"
79+
}
80+
return ""
81+
}
82+
83+
// execType returns the type of entity, including: string, list, hash, set and zset
84+
func execType(db *DB, args [][]byte) redis.Reply {
85+
key := string(args[0])
86+
result := getType(db, key)
87+
if len(result) > 0 {
88+
return protocol.MakeStatusReply(result)
89+
} else {
90+
return &protocol.UnknownErrReply{}
8091
}
81-
return &protocol.UnknownErrReply{}
8292
}
8393

8494
func prepareRename(args [][]byte) ([]string, []string) {
@@ -413,6 +423,57 @@ func execCopy(mdb *Server, conn redis.Connection, args [][]byte) redis.Reply {
413423
return protocol.MakeIntReply(1)
414424
}
415425

426+
// execScan return the result of the scan
427+
func execScan(db *DB, args [][]byte) redis.Reply {
428+
var count int = 10
429+
var pattern string = "*"
430+
var scanType string = ""
431+
if len(args) > 1 {
432+
for i := 1; i < len(args); i++ {
433+
arg := strings.ToLower(string(args[i]))
434+
if arg == "count" {
435+
count0, err := strconv.Atoi(string(args[i+1]))
436+
if err != nil {
437+
return &protocol.SyntaxErrReply{}
438+
}
439+
count = count0
440+
i++
441+
} else if arg == "match" {
442+
pattern = string(args[i+1])
443+
i++
444+
} else if arg == "type" {
445+
scanType = strings.ToLower(string(args[i+1]))
446+
i++
447+
} else {
448+
return &protocol.SyntaxErrReply{}
449+
}
450+
}
451+
}
452+
cursor, err := strconv.Atoi(string(args[0]))
453+
if err != nil {
454+
return protocol.MakeErrReply("ERR invalid cursor")
455+
}
456+
keysReply, nextCursor := db.data.DictScan(cursor, count, pattern)
457+
if nextCursor < 0 {
458+
return protocol.MakeErrReply("Invalid argument")
459+
}
460+
461+
if len(scanType) != 0 {
462+
for i := 0; i < len(keysReply); {
463+
if getType(db, string(keysReply[i])) != scanType {
464+
keysReply = append(keysReply[:i], keysReply[i+1:]...)
465+
} else {
466+
i++
467+
}
468+
}
469+
}
470+
result := make([]redis.Reply, 2)
471+
result[0] = protocol.MakeBulkReply([]byte(strconv.FormatInt(int64(nextCursor), 10)))
472+
result[1] = protocol.MakeMultiBulkReply(keysReply)
473+
474+
return protocol.MakeMultiRawReply(result)
475+
}
476+
416477
func init() {
417478
registerCommand("Del", execDel, writeAllKeys, undoDel, -2, flagWrite).
418479
attachCommandExtra([]string{redisFlagWrite}, 1, -1, 1)
@@ -444,4 +505,6 @@ func init() {
444505
attachCommandExtra([]string{redisFlagWrite, redisFlagFast}, 1, 1, 1)
445506
registerCommand("Keys", execKeys, noPrepare, nil, 2, flagReadOnly).
446507
attachCommandExtra([]string{redisFlagReadonly, redisFlagSortForScript}, 1, 1, 1)
508+
registerCommand("Scan", execScan, readAllKeys, nil, -2, flagReadOnly).
509+
attachCommandExtra([]string{redisFlagReadonly, redisFlagSortForScript}, 1, 1, 1)
447510
}

database/keys_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,3 +313,85 @@ func TestCopy(t *testing.T) {
313313
result = testMDB.Exec(conn, utils.ToCmdLine("ttl", destKey))
314314
asserts.AssertIntReplyGreaterThan(t, result, 0)
315315
}
316+
317+
func TestScan(t *testing.T) {
318+
testDB.Flush()
319+
for i := 0; i < 3; i++ {
320+
key := string(rune(i))
321+
value := key
322+
testDB.Exec(nil, utils.ToCmdLine("set", "a:"+key, value))
323+
}
324+
for i := 0; i < 3; i++ {
325+
key := string(rune(i))
326+
value := key
327+
testDB.Exec(nil, utils.ToCmdLine("set", "b:"+key, value))
328+
}
329+
330+
// test scan 0 when keys < 10
331+
result := testDB.Exec(nil, utils.ToCmdLine("scan", "0"))
332+
cursorStr := string(result.(*protocol.MultiRawReply).Replies[0].(*protocol.BulkReply).Arg)
333+
cursor, err := strconv.Atoi(cursorStr)
334+
if err == nil {
335+
if cursor != 0 {
336+
t.Errorf("expect cursor 0, actually %d", cursor)
337+
return
338+
}
339+
} else {
340+
t.Errorf("get scan result error")
341+
return
342+
}
343+
344+
// test scan 0 match a*
345+
result = testDB.Exec(nil, utils.ToCmdLine("scan", "0", "match", "a*"))
346+
returnKeys := result.(*protocol.MultiRawReply).Replies[1].(*protocol.MultiBulkReply).Args
347+
for i := range returnKeys {
348+
key := string(returnKeys[i])
349+
if key[0] != 'a' {
350+
t.Errorf("The key %s should match a*", key)
351+
return
352+
}
353+
}
354+
355+
// test scan 0 type string
356+
testDB.Exec(nil, utils.ToCmdLine("hset", "hashkey", "hashkey", "1"))
357+
result = testDB.Exec(nil, utils.ToCmdLine("scan", "0", "type", "string"))
358+
returnKeys = result.(*protocol.MultiRawReply).Replies[1].(*protocol.MultiBulkReply).Args
359+
for i := range returnKeys {
360+
key := string(returnKeys[i])
361+
if key == "hashkey" {
362+
t.Errorf("expect type string, found hash")
363+
return
364+
}
365+
}
366+
367+
// test returned cursor
368+
testDB.Flush()
369+
for i := 0; i < 100; i++ {
370+
key := string(rune(i))
371+
value := key
372+
testDB.Exec(nil, utils.ToCmdLine("set", "a"+key, value))
373+
}
374+
cursor = 0
375+
resultByte := make([][]byte, 0)
376+
for {
377+
scanCursor := strconv.Itoa(cursor)
378+
result = testDB.Exec(nil, utils.ToCmdLine("scan", scanCursor, "count", "20"))
379+
cursorStr := string(result.(*protocol.MultiRawReply).Replies[0].(*protocol.BulkReply).Arg)
380+
returnKeys = result.(*protocol.MultiRawReply).Replies[1].(*protocol.MultiBulkReply).Args
381+
resultByte = append(resultByte, returnKeys...)
382+
cursor, err = strconv.Atoi(cursorStr)
383+
if err == nil {
384+
if cursor == 0 {
385+
break
386+
}
387+
} else {
388+
t.Errorf("get scan result error")
389+
return
390+
}
391+
}
392+
resultByte = utils.RemoveDuplicates(resultByte)
393+
if len(resultByte) != 100 {
394+
t.Errorf("expect result num 100, actually %d", len(resultByte))
395+
return
396+
}
397+
}

datastruct/dict/concurrent.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dict
22

33
import (
4+
"github.com/hdt3213/godis/lib/wildcard"
45
"math"
56
"math/rand"
67
"sort"
@@ -435,3 +436,46 @@ func (dict *ConcurrentDict) RWUnLocks(writeKeys []string, readKeys []string) {
435436
}
436437
}
437438
}
439+
440+
func stringsToBytes(strSlice []string) [][]byte {
441+
byteSlice := make([][]byte, len(strSlice))
442+
for i, str := range strSlice {
443+
byteSlice[i] = []byte(str)
444+
}
445+
return byteSlice
446+
}
447+
448+
func (dict *ConcurrentDict) DictScan(cursor int, count int, pattern string) ([][]byte, int) {
449+
size := dict.Len()
450+
result := make([][]byte, 0)
451+
452+
if pattern == "*" && count >= size {
453+
return stringsToBytes(dict.Keys()), 0
454+
}
455+
456+
matchKey, err := wildcard.CompilePattern(pattern)
457+
if err != nil {
458+
return result, -1
459+
}
460+
461+
shardCount := len(dict.table)
462+
shardIndex := cursor
463+
464+
for shardIndex < shardCount {
465+
shard := dict.table[shardIndex]
466+
shard.mutex.RLock()
467+
defer shard.mutex.RUnlock()
468+
if len(result)+len(shard.m) > count && shardIndex > cursor {
469+
return result, shardIndex
470+
}
471+
472+
for key := range shard.m {
473+
if pattern == "*" || matchKey.IsMatch(key) {
474+
result = append(result, []byte(key))
475+
}
476+
}
477+
shardIndex++
478+
}
479+
480+
return result, 0
481+
}

datastruct/dict/concurrent_test.go

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ func TestConcurrentRemoveWithLock(t *testing.T) {
465465
}
466466
}
467467

468-
//change t.Error remove->forEach
468+
// change t.Error remove->forEach
469469
func TestConcurrentForEach(t *testing.T) {
470470
d := MakeConcurrent(0)
471471
size := 100
@@ -524,3 +524,51 @@ func TestConcurrentDict_Keys(t *testing.T) {
524524
t.Errorf("expect %d keys, actual: %d", size, len(d.Keys()))
525525
}
526526
}
527+
528+
func TestDictScan(t *testing.T) {
529+
d := MakeConcurrent(0)
530+
count := 100
531+
for i := 0; i < count; i++ {
532+
key := "kkk" + strconv.Itoa(i)
533+
d.Put(key, i)
534+
}
535+
for i := 0; i < count; i++ {
536+
key := "key" + strconv.Itoa(i)
537+
d.Put(key, i)
538+
}
539+
cursor := 0
540+
matchKey := "*"
541+
c := 20
542+
result := make([][]byte, 0)
543+
var returnKeys [][]byte
544+
for {
545+
returnKeys, cursor = d.DictScan(cursor, c, matchKey)
546+
result = append(result, returnKeys...)
547+
if cursor == 0 {
548+
break
549+
}
550+
}
551+
result = utils.RemoveDuplicates(result)
552+
if len(result) != count*2 {
553+
t.Errorf("scan command result number error: %d, should be %d ", len(result), count*2)
554+
}
555+
matchKey = "key*"
556+
cursor = 0
557+
mresult := make([][]byte, 0)
558+
for {
559+
returnKeys, cursor = d.DictScan(cursor, c, matchKey)
560+
mresult = append(mresult, returnKeys...)
561+
if cursor == 0 {
562+
break
563+
}
564+
}
565+
mresult = utils.RemoveDuplicates(mresult)
566+
if len(mresult) != count {
567+
t.Errorf("scan command result number error: %d, should be %d ", len(mresult), count)
568+
}
569+
matchKey = "no*"
570+
returnKeys, _ = d.DictScan(cursor, c, matchKey)
571+
if len(returnKeys) != 0 {
572+
t.Errorf("returnKeys should be empty")
573+
}
574+
}

lib/utils/utils.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,20 @@ func ConvertRange(start int64, end int64, size int64) (int, int) {
8484
}
8585
return int(start), int(end)
8686
}
87+
88+
// RemoveDuplicates removes duplicate byte slices from a 2D byte slice
89+
func RemoveDuplicates(input [][]byte) [][]byte {
90+
uniqueMap := make(map[string]struct{})
91+
var result [][]byte
92+
93+
for _, item := range input {
94+
// Use bytes.Buffer to convert byte slice to string
95+
key := string(item)
96+
if _, exists := uniqueMap[key]; !exists {
97+
uniqueMap[key] = struct{}{}
98+
result = append(result, item)
99+
}
100+
}
101+
102+
return result
103+
}

test.rdb

-202 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)