From 015411f5ce5217c9778f8e9daf77177d200ce2b5 Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Mon, 30 Oct 2017 03:39:51 -0500 Subject: [PATCH] add redis-compatible HSCAN/SSCAN/ZSCAN commands (#321) --- cmd/ledis-cli/const.go | 5 +++- doc/DiffRedis.md | 5 +--- doc/commands.json | 15 ++++++++++ doc/commands.md | 21 ++++++++++++++ server/cmd_scan.go | 62 ++++++++++++++++++++++++++++++----------- server/cmd_scan_test.go | 59 ++++++++++++++++++++++++++++++++++----- 6 files changed, 138 insertions(+), 29 deletions(-) diff --git a/cmd/ledis-cli/const.go b/cmd/ledis-cli/const.go index 21be6ed..66c05bb 100644 --- a/cmd/ledis-cli/const.go +++ b/cmd/ledis-cli/const.go @@ -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"}, diff --git a/doc/DiffRedis.md b/doc/DiffRedis.md index 516cc2c..2d44e7d 100644 --- a/doc/DiffRedis.md +++ b/doc/DiffRedis.md @@ -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 diff --git a/doc/commands.json b/doc/commands.json index de71f30..23c3c23 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -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", diff --git a/doc/commands.md b/doc/commands.md index 7e9d6a8..772bd81 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -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 diff --git a/server/cmd_scan.go b/server/cmd_scan.go index 0257948..5668e4d 100644 --- a/server/cmd_scan.go +++ b/server/cmd_scan.go @@ -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) } diff --git a/server/cmd_scan_test.go b/server/cmd_scan_test.go index 50dcbdf..03250fe 100644 --- a/server/cmd_scan_test.go +++ b/server/cmd_scan_test.go @@ -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) + } }