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 package main
var helpCommands = [][]string{ var helpCommands = [][]string{
@ -43,6 +43,7 @@ var helpCommands = [][]string{
{"HMGET", "key field [field ...]", "Hash"}, {"HMGET", "key field [field ...]", "Hash"},
{"HMSET", "key field value [field value ...]", "Hash"}, {"HMSET", "key field value [field value ...]", "Hash"},
{"HPERSIST", "key", "Hash"}, {"HPERSIST", "key", "Hash"},
{"HSCAN", "key cursor [MATCH match] [COUNT count] [ASC|DESC]", "Hash"},
{"HSET", "key field value", "Hash"}, {"HSET", "key field value", "Hash"},
{"HTTL", "key", "Hash"}, {"HTTL", "key", "Hash"},
{"HVALS", "key", "Hash"}, {"HVALS", "key", "Hash"},
@ -96,6 +97,7 @@ var helpCommands = [][]string{
{"SMEMBERS", "key", "Set"}, {"SMEMBERS", "key", "Set"},
{"SPERSIST", "key", "Set"}, {"SPERSIST", "key", "Set"},
{"SREM", "key member [member ...]", "Set"}, {"SREM", "key member [member ...]", "Set"},
{"SSCAN", "key cursor [MATCH match] [COUNT count] [ASC|DESC]", "Set"},
{"STRLEN", "key", "KV"}, {"STRLEN", "key", "KV"},
{"STTL", "key", "Set"}, {"STTL", "key", "Set"},
{"SUNION", "key [key ...]", "Set"}, {"SUNION", "key [key ...]", "Set"},
@ -134,6 +136,7 @@ var helpCommands = [][]string{
{"ZREVRANGE", "key start stop [WITHSCORES]", "ZSet"}, {"ZREVRANGE", "key start stop [WITHSCORES]", "ZSet"},
{"ZREVRANGEBYSCORE", "key max min [WITHSCORES][LIMIT offset count]", "ZSet"}, {"ZREVRANGEBYSCORE", "key max min [WITHSCORES][LIMIT offset count]", "ZSet"},
{"ZREVRANK", "key member", "ZSet"}, {"ZREVRANK", "key member", "ZSet"},
{"ZSCAN", "key cursor [MATCH match] [COUNT count] [ASC|DESC]", "ZSet"},
{"ZSCORE", "key member", "ZSet"}, {"ZSCORE", "key member", "ZSet"},
{"ZTTL", "key", "ZSet"}, {"ZTTL", "key", "ZSet"},
{"ZUNIONSTORE", "destkey numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]", "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 ## 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] 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 ## DUMP

View File

@ -115,6 +115,11 @@
"group": "Hash", "group": "Hash",
"readonly": false "readonly": false
}, },
"HSCAN": {
"arguments": "key cursor [MATCH match] [COUNT count] [ASC|DESC]",
"group": "Hash",
"readonly": true
},
"HPERSIST": { "HPERSIST": {
"arguments": "key", "arguments": "key",
"group": "Hash", "group": "Hash",
@ -310,6 +315,11 @@
"group": "Set", "group": "Set",
"readonly": true "readonly": true
}, },
"SSCAN": {
"arguments": "key cursor [MATCH match] [COUNT count] [ASC|DESC]",
"group": "Set",
"readonly": true
},
"SREM": { "SREM": {
"arguments": "key member [member ...]", "arguments": "key member [member ...]",
"group": "Set", "group": "Set",
@ -450,6 +460,11 @@
"group": "ZSet", "group": "ZSet",
"readonly": true "readonly": true
}, },
"ZSCAN": {
"arguments": "key cursor [MATCH match] [COUNT count] [ASC|DESC]",
"group": "ZSet",
"readonly": true
},
"ZSCORE": { "ZSCORE": {
"arguments": "key member", "arguments": "key member",
"group": "ZSet", "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) - [HLEN key](#hlen-key)
- [HMGET key field [field ...]](#hmget-key-field-field-) - [HMGET key field [field ...]](#hmget-key-field-field-)
- [HMSET key field value [field value ...]](#hmset-key-field-value-field-value-) - [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) - [HSET key field value](#hset-key-field-value)
- [HVALS key](#hvals-key) - [HVALS key](#hvals-key)
- [HCLEAR key](#hclear-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) - [SISMEMBER key member](#sismember--key-member)
- [SMEMBERS key](#smembers-key) - [SMEMBERS key](#smembers-key)
- [SREM key member [member ...]](#srem--key-member-member-) - [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-) - [SUNION key [key ...]](#sunion-key-key-)
- [SUNIONSTORE destination key [key]](#sunionstore-destination-key-key) - [SUNIONSTORE destination key [key]](#sunionstore-destination-key-key)
- [SCLEAR key](#sclear-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) - [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) - [ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]](#zrevrangebyscore--key-max-min-withscores-limit-offset-count)
- [ZREVRANK key member](#zrevrank-key-member) - [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) - [ZSCORE key member](#zscore-key-member)
- [ZCLEAR key](#zclear-key) - [ZCLEAR key](#zclear-key)
- [ZMCLEAR key [key ...]](#zmclear-key-key-) - [ZMCLEAR key [key ...]](#zmclear-key-key-)
@ -730,6 +733,12 @@ ledis> HMGET myhash field1 field2
2) "world" 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 ### 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. 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" 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 ...] ### SUNION key [key ...]
Returns the members of the set resulting from the union of all the given sets. Returns the members of the set resulting from the union of all the given sets.
@ -2089,6 +2104,12 @@ ledis> ZSCORE myzset 'one'
1 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 ### ZCLEAR key
Delete the specified key Delete the specified key

View File

@ -1,6 +1,7 @@
package server package server
import ( import (
"bytes"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@ -10,7 +11,7 @@ import (
"github.com/siddontang/ledisdb/ledis" "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] cursor = args[0]
args = args[1:] args = args[1:]
@ -56,8 +57,21 @@ func parseScanArgs(args [][]byte) (cursor []byte, match string, count int, desc
return 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] // XSCAN type cursor [MATCH match] [COUNT count] [ASC|DESC]
func xscanCommand(c *client) error { func (scg scanCommandGroup) xscanCommand(c *client) error {
args := c.args args := c.args
if len(args) < 2 { if len(args) < 2 {
@ -80,7 +94,7 @@ func xscanCommand(c *client) error {
return fmt.Errorf("invalid key type %s", args[0]) 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 { if err != nil {
return err return err
@ -100,7 +114,7 @@ func xscanCommand(c *client) error {
data := make([]interface{}, 2) data := make([]interface{}, 2)
if len(ay) < count { if len(ay) < count {
data[0] = []byte("") data[0] = scg.lastCursor
} else { } else {
data[0] = ay[len(ay)-1] data[0] = ay[len(ay)-1]
} }
@ -110,7 +124,7 @@ func xscanCommand(c *client) error {
} }
// XHSCAN key cursor [MATCH match] [COUNT count] [ASC|DESC] // XHSCAN key cursor [MATCH match] [COUNT count] [ASC|DESC]
func xhscanCommand(c *client) error { func (scg scanCommandGroup) xhscanCommand(c *client) error {
args := c.args args := c.args
if len(args) < 2 { if len(args) < 2 {
@ -119,7 +133,7 @@ func xhscanCommand(c *client) error {
key := args[0] key := args[0]
cursor, match, count, desc, err := parseScanArgs(args[1:]) cursor, match, count, desc, err := scg.parseArgs(args[1:])
if err != nil { if err != nil {
return err return err
@ -139,7 +153,7 @@ func xhscanCommand(c *client) error {
data := make([]interface{}, 2) data := make([]interface{}, 2)
if len(ay) < count { if len(ay) < count {
data[0] = []byte("") data[0] = scg.lastCursor
} else { } else {
data[0] = ay[len(ay)-1].Field 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] // XSSCAN key cursor [MATCH match] [COUNT count] [ASC|DESC]
func xsscanCommand(c *client) error { func (scg scanCommandGroup) xsscanCommand(c *client) error {
args := c.args args := c.args
if len(args) < 2 { if len(args) < 2 {
@ -166,7 +180,7 @@ func xsscanCommand(c *client) error {
key := args[0] key := args[0]
cursor, match, count, desc, err := parseScanArgs(args[1:]) cursor, match, count, desc, err := scg.parseArgs(args[1:])
if err != nil { if err != nil {
return err return err
@ -186,7 +200,7 @@ func xsscanCommand(c *client) error {
data := make([]interface{}, 2) data := make([]interface{}, 2)
if len(ay) < count { if len(ay) < count {
data[0] = []byte("") data[0] = scg.lastCursor
} else { } else {
data[0] = ay[len(ay)-1] data[0] = ay[len(ay)-1]
} }
@ -198,7 +212,7 @@ func xsscanCommand(c *client) error {
} }
// XZSCAN key cursor [MATCH match] [COUNT count] [ASC|DESC] // XZSCAN key cursor [MATCH match] [COUNT count] [ASC|DESC]
func xzscanCommand(c *client) error { func (scg scanCommandGroup) xzscanCommand(c *client) error {
args := c.args args := c.args
if len(args) < 2 { if len(args) < 2 {
@ -207,7 +221,7 @@ func xzscanCommand(c *client) error {
key := args[0] key := args[0]
cursor, match, count, desc, err := parseScanArgs(args[1:]) cursor, match, count, desc, err := scg.parseArgs(args[1:])
if err != nil { if err != nil {
return err return err
@ -227,7 +241,7 @@ func xzscanCommand(c *client) error {
data := make([]interface{}, 2) data := make([]interface{}, 2)
if len(ay) < count { if len(ay) < count {
data[0] = []byte("") data[0] = scg.lastCursor
} else { } else {
data[0] = ay[len(ay)-1].Member data[0] = ay[len(ay)-1].Member
} }
@ -244,9 +258,23 @@ func xzscanCommand(c *client) error {
return nil return nil
} }
var (
xScanGroup = scanCommandGroup{nilCursorLedis, parseXScanArgs}
scanGroup = scanCommandGroup{nilCursorRedis, parseScanArgs}
)
var (
nilCursorLedis = []byte("")
nilCursorRedis = []byte("0")
)
func init() { func init() {
register("xscan", xscanCommand) register("hscan", scanGroup.xhscanCommand)
register("xhscan", xhscanCommand) register("sscan", scanGroup.xsscanCommand)
register("xsscan", xsscanCommand) register("zscan", scanGroup.xzscanCommand)
register("xzscan", 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) testListKeyScan(t, c)
testZSetKeyScan(t, c) testZSetKeyScan(t, c)
testSetKeyScan(t, c) testSetKeyScan(t, c)
} }
func checkScanValues(t *testing.T, ay interface{}, values ...interface{}) { 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 { for i, v := range a {
if string(v) != fmt.Sprintf("%v", values[i]) { 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 { } else {
checkScanValues(t, ay[1], 5, 6, 7, 8, 9) checkScanValues(t, ay[1], 5, 6, 7, 8, 9)
} }
} }
func testKVScan(t *testing.T, c *goredis.Client) { func testKVScan(t *testing.T, c *goredis.Client) {
@ -125,7 +123,7 @@ func testSetKeyScan(t *testing.T, c *goredis.Client) {
checkScan(t, c, "SET") checkScan(t, c, "SET")
} }
func TestHashScan(t *testing.T) { func TestXHashScan(t *testing.T) {
c := getTestConn() c := getTestConn()
defer c.Close() 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() c := getTestConn()
defer c.Close() defer c.Close()
@ -155,10 +169,25 @@ func TestSetScan(t *testing.T) {
} else { } else {
checkScanValues(t, ay[1], "a", "b") 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() c := getTestConn()
defer c.Close() defer c.Close()
@ -172,5 +201,21 @@ func TestZSetScan(t *testing.T) {
} else { } else {
checkScanValues(t, ay[1], "a", 1, "b", 2) 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)
}
} }