add redis-compatible HSCAN/SSCAN/ZSCAN commands (#321)

This commit is contained in:
Glen De Cauwsemaecker 2017-10-30 03:39:51 -05:00 committed by siddontang
parent 57a07edd1a
commit 015411f5ce
6 changed files with 138 additions and 29 deletions

View File

@ -1,4 +1,4 @@
//This file was generated by .tools/generate_commands.py on Sat Mar 14 2015 08:58:32 +0800
//This file was generated by .tools/generate_commands.py on Sat Oct 28 2017 18:15:49 -0500
package main
var helpCommands = [][]string{
@ -43,6 +43,7 @@ var helpCommands = [][]string{
{"HMGET", "key field [field ...]", "Hash"},
{"HMSET", "key field value [field value ...]", "Hash"},
{"HPERSIST", "key", "Hash"},
{"HSCAN", "key cursor [MATCH match] [COUNT count] [ASC|DESC]", "Hash"},
{"HSET", "key field value", "Hash"},
{"HTTL", "key", "Hash"},
{"HVALS", "key", "Hash"},
@ -96,6 +97,7 @@ var helpCommands = [][]string{
{"SMEMBERS", "key", "Set"},
{"SPERSIST", "key", "Set"},
{"SREM", "key member [member ...]", "Set"},
{"SSCAN", "key cursor [MATCH match] [COUNT count] [ASC|DESC]", "Set"},
{"STRLEN", "key", "KV"},
{"STTL", "key", "Set"},
{"SUNION", "key [key ...]", "Set"},
@ -134,6 +136,7 @@ var helpCommands = [][]string{
{"ZREVRANGE", "key start stop [WITHSCORES]", "ZSet"},
{"ZREVRANGEBYSCORE", "key max min [WITHSCORES][LIMIT offset count]", "ZSet"},
{"ZREVRANK", "key member", "ZSet"},
{"ZSCAN", "key cursor [MATCH match] [COUNT count] [ASC|DESC]", "ZSet"},
{"ZSCORE", "key member", "ZSet"},
{"ZTTL", "key", "ZSet"},
{"ZUNIONSTORE", "destkey numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]", "ZSet"},

View File

@ -32,13 +32,10 @@ ZSet only support int64 score, not double in Redis.
## Scan
LedisDB supplies `xscan`, `xhscan`, `xsscan`, `xzscan` to fetch data iteratively and reverse iteratively.
LedisDB supplies `xscan` instead of `scan` to fetch keys iteratively and reverse iteratively.
```
XSCAN type cursor [MATCH match] [COUNT count]
XHSCAN key cursor [MATCH match] [COUNT count]
XSSCAN key cursor [MATCH match] [COUNT count]
XZSCAN key cursor [MATCH match] [COUNT count]
```
## DUMP

View File

@ -115,6 +115,11 @@
"group": "Hash",
"readonly": false
},
"HSCAN": {
"arguments": "key cursor [MATCH match] [COUNT count] [ASC|DESC]",
"group": "Hash",
"readonly": true
},
"HPERSIST": {
"arguments": "key",
"group": "Hash",
@ -310,6 +315,11 @@
"group": "Set",
"readonly": true
},
"SSCAN": {
"arguments": "key cursor [MATCH match] [COUNT count] [ASC|DESC]",
"group": "Set",
"readonly": true
},
"SREM": {
"arguments": "key member [member ...]",
"group": "Set",
@ -450,6 +460,11 @@
"group": "ZSet",
"readonly": true
},
"ZSCAN": {
"arguments": "key cursor [MATCH match] [COUNT count] [ASC|DESC]",
"group": "ZSet",
"readonly": true
},
"ZSCORE": {
"arguments": "key member",
"group": "ZSet",

View File

@ -48,6 +48,7 @@ Most of the Ledisdb's commands are the same as Redis's, you can see the redis co
- [HLEN key](#hlen-key)
- [HMGET key field [field ...]](#hmget-key-field-field-)
- [HMSET key field value [field value ...]](#hmset-key-field-value-field-value-)
- [HSCAN key cursor [MATCH match] [COUNT count] [ASC|DESC]](#hscan-key-cursor-match-match-count-count-asc|desc)
- [HSET key field value](#hset-key-field-value)
- [HVALS key](#hvals-key)
- [HCLEAR key](#hclear-key)
@ -88,6 +89,7 @@ Most of the Ledisdb's commands are the same as Redis's, you can see the redis co
- [SISMEMBER key member](#sismember--key-member)
- [SMEMBERS key](#smembers-key)
- [SREM key member [member ...]](#srem--key-member-member-)
- [SSCAN key cursor [MATCH match] [COUNT count] [ASC|DESC]](#sscan-key-cursor-match-match-count-count-asc|desc)
- [SUNION key [key ...]](#sunion-key-key-)
- [SUNIONSTORE destination key [key]](#sunionstore-destination-key-key)
- [SCLEAR key](#sclear-key)
@ -112,6 +114,7 @@ Most of the Ledisdb's commands are the same as Redis's, you can see the redis co
- [ZREVRANGE key start stop [WITHSCORES]](#zrevrange-key-start-stop-withscores)
- [ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]](#zrevrangebyscore--key-max-min-withscores-limit-offset-count)
- [ZREVRANK key member](#zrevrank-key-member)
- [ZSCAN key cursor [MATCH match] [COUNT count] [ASC|DESC]](#zscan-key-cursor-match-match-count-count-asc|desc)
- [ZSCORE key member](#zscore-key-member)
- [ZCLEAR key](#zclear-key)
- [ZMCLEAR key [key ...]](#zmclear-key-key-)
@ -730,6 +733,12 @@ ledis> HMGET myhash field1 field2
2) "world"
```
### HSCAN key cursor [MATCH match] [COUNT count] [ASC|DESC]
Same like XHSCAN, but made redis compatible.
Meaning that the initial cursor has to be `"0"`,
and the final cursor will be `"0"` as well.
### HSET key field value
Sets field in the hash stored at key to value. If key does not exists, a new hash key is created.
@ -1489,6 +1498,12 @@ ledis> SMEMBERS myset
2) "two"
```
### SSCAN key cursor [MATCH match] [COUNT count] [ASC|DESC]
Same like XSSCAN, but made redis compatible.
Meaning that the initial cursor has to be `"0"`,
and the final cursor will be `"0"` as well.
### SUNION key [key ...]
Returns the members of the set resulting from the union of all the given sets.
@ -2089,6 +2104,12 @@ ledis> ZSCORE myzset 'one'
1
```
### ZSCAN key cursor [MATCH match] [COUNT count] [ASC|DESC]
Same like XZSCAN, but made redis compatible.
Meaning that the initial cursor has to be `"0"`,
and the final cursor will be `"0"` as well.
### ZCLEAR key
Delete the specified key

View File

@ -1,6 +1,7 @@
package server
import (
"bytes"
"fmt"
"strconv"
"strings"
@ -10,7 +11,7 @@ import (
"github.com/siddontang/ledisdb/ledis"
)
func parseScanArgs(args [][]byte) (cursor []byte, match string, count int, desc bool, err error) {
func parseXScanArgs(args [][]byte) (cursor []byte, match string, count int, desc bool, err error) {
cursor = args[0]
args = args[1:]
@ -56,8 +57,21 @@ func parseScanArgs(args [][]byte) (cursor []byte, match string, count int, desc
return
}
func parseScanArgs(args [][]byte) (cursor []byte, match string, count int, desc bool, err error) {
cursor, match, count, desc, err = parseXScanArgs(args)
if bytes.Compare(cursor, nilCursorRedis) == 0 {
cursor = nilCursorLedis
}
return
}
type scanCommandGroup struct {
lastCursor []byte
parseArgs func(args [][]byte) (cursor []byte, match string, count int, desc bool, err error)
}
// XSCAN type cursor [MATCH match] [COUNT count] [ASC|DESC]
func xscanCommand(c *client) error {
func (scg scanCommandGroup) xscanCommand(c *client) error {
args := c.args
if len(args) < 2 {
@ -80,7 +94,7 @@ func xscanCommand(c *client) error {
return fmt.Errorf("invalid key type %s", args[0])
}
cursor, match, count, desc, err := parseScanArgs(args[1:])
cursor, match, count, desc, err := scg.parseArgs(args[1:])
if err != nil {
return err
@ -100,7 +114,7 @@ func xscanCommand(c *client) error {
data := make([]interface{}, 2)
if len(ay) < count {
data[0] = []byte("")
data[0] = scg.lastCursor
} else {
data[0] = ay[len(ay)-1]
}
@ -110,7 +124,7 @@ func xscanCommand(c *client) error {
}
// XHSCAN key cursor [MATCH match] [COUNT count] [ASC|DESC]
func xhscanCommand(c *client) error {
func (scg scanCommandGroup) xhscanCommand(c *client) error {
args := c.args
if len(args) < 2 {
@ -119,7 +133,7 @@ func xhscanCommand(c *client) error {
key := args[0]
cursor, match, count, desc, err := parseScanArgs(args[1:])
cursor, match, count, desc, err := scg.parseArgs(args[1:])
if err != nil {
return err
@ -139,7 +153,7 @@ func xhscanCommand(c *client) error {
data := make([]interface{}, 2)
if len(ay) < count {
data[0] = []byte("")
data[0] = scg.lastCursor
} else {
data[0] = ay[len(ay)-1].Field
}
@ -157,7 +171,7 @@ func xhscanCommand(c *client) error {
}
// XSSCAN key cursor [MATCH match] [COUNT count] [ASC|DESC]
func xsscanCommand(c *client) error {
func (scg scanCommandGroup) xsscanCommand(c *client) error {
args := c.args
if len(args) < 2 {
@ -166,7 +180,7 @@ func xsscanCommand(c *client) error {
key := args[0]
cursor, match, count, desc, err := parseScanArgs(args[1:])
cursor, match, count, desc, err := scg.parseArgs(args[1:])
if err != nil {
return err
@ -186,7 +200,7 @@ func xsscanCommand(c *client) error {
data := make([]interface{}, 2)
if len(ay) < count {
data[0] = []byte("")
data[0] = scg.lastCursor
} else {
data[0] = ay[len(ay)-1]
}
@ -198,7 +212,7 @@ func xsscanCommand(c *client) error {
}
// XZSCAN key cursor [MATCH match] [COUNT count] [ASC|DESC]
func xzscanCommand(c *client) error {
func (scg scanCommandGroup) xzscanCommand(c *client) error {
args := c.args
if len(args) < 2 {
@ -207,7 +221,7 @@ func xzscanCommand(c *client) error {
key := args[0]
cursor, match, count, desc, err := parseScanArgs(args[1:])
cursor, match, count, desc, err := scg.parseArgs(args[1:])
if err != nil {
return err
@ -227,7 +241,7 @@ func xzscanCommand(c *client) error {
data := make([]interface{}, 2)
if len(ay) < count {
data[0] = []byte("")
data[0] = scg.lastCursor
} else {
data[0] = ay[len(ay)-1].Member
}
@ -244,9 +258,23 @@ func xzscanCommand(c *client) error {
return nil
}
var (
xScanGroup = scanCommandGroup{nilCursorLedis, parseXScanArgs}
scanGroup = scanCommandGroup{nilCursorRedis, parseScanArgs}
)
var (
nilCursorLedis = []byte("")
nilCursorRedis = []byte("0")
)
func init() {
register("xscan", xscanCommand)
register("xhscan", xhscanCommand)
register("xsscan", xsscanCommand)
register("xzscan", xzscanCommand)
register("hscan", scanGroup.xhscanCommand)
register("sscan", scanGroup.xsscanCommand)
register("zscan", scanGroup.xzscanCommand)
register("xscan", xScanGroup.xscanCommand)
register("xhscan", xScanGroup.xhscanCommand)
register("xsscan", xScanGroup.xsscanCommand)
register("xzscan", xScanGroup.xzscanCommand)
}

View File

@ -32,7 +32,6 @@ func TestScan(t *testing.T) {
testListKeyScan(t, c)
testZSetKeyScan(t, c)
testSetKeyScan(t, c)
}
func checkScanValues(t *testing.T, ay interface{}, values ...interface{}) {
@ -47,7 +46,7 @@ func checkScanValues(t *testing.T, ay interface{}, values ...interface{}) {
for i, v := range a {
if string(v) != fmt.Sprintf("%v", values[i]) {
t.Fatal(fmt.Sprintf("%d %s != %v", string(v), values[i]))
t.Fatal(fmt.Sprintf("%d %s != %v", i, string(v), values[i]))
}
}
}
@ -72,7 +71,6 @@ func checkScan(t *testing.T, c *goredis.Client, tp string) {
} else {
checkScanValues(t, ay[1], 5, 6, 7, 8, 9)
}
}
func testKVScan(t *testing.T, c *goredis.Client) {
@ -125,7 +123,7 @@ func testSetKeyScan(t *testing.T, c *goredis.Client) {
checkScan(t, c, "SET")
}
func TestHashScan(t *testing.T) {
func TestXHashScan(t *testing.T) {
c := getTestConn()
defer c.Close()
@ -141,7 +139,23 @@ func TestHashScan(t *testing.T) {
}
}
func TestSetScan(t *testing.T) {
func TestHashScan(t *testing.T) {
c := getTestConn()
defer c.Close()
key := "scan_hash"
c.Do("HMSET", key, "a", 1, "b", 2)
if ay, err := goredis.Values(c.Do("HSCAN", key, "0")); err != nil {
t.Fatal(err)
} else if len(ay) != 2 {
t.Fatal(len(ay))
} else {
checkScanValues(t, ay[1], "a", 1, "b", 2)
}
}
func TestXSetScan(t *testing.T) {
c := getTestConn()
defer c.Close()
@ -155,10 +169,25 @@ func TestSetScan(t *testing.T) {
} else {
checkScanValues(t, ay[1], "a", "b")
}
}
func TestZSetScan(t *testing.T) {
func TestSetScan(t *testing.T) {
c := getTestConn()
defer c.Close()
key := "scan_set"
c.Do("SADD", key, "a", "b")
if ay, err := goredis.Values(c.Do("SSCAN", key, "0")); err != nil {
t.Fatal(err)
} else if len(ay) != 2 {
t.Fatal(len(ay))
} else {
checkScanValues(t, ay[1], "a", "b")
}
}
func TestXZSetScan(t *testing.T) {
c := getTestConn()
defer c.Close()
@ -172,5 +201,21 @@ func TestZSetScan(t *testing.T) {
} else {
checkScanValues(t, ay[1], "a", 1, "b", 2)
}
}
func TestZSetScan(t *testing.T) {
c := getTestConn()
defer c.Close()
key := "scan_zset"
c.Do("ZADD", key, 1, "a", 2, "b")
if ay, err := goredis.Values(c.Do("XZSCAN", key, "0")); err != nil {
t.Fatal(err)
} else if len(ay) != 2 {
t.Fatal(len(ay))
} else {
checkScanValues(t, ay[1], "a", 1, "b", 2)
}
}