Skip to content
This repository has been archived by the owner on Oct 22, 2023. It is now read-only.

Commit

Permalink
Implementing Count functionality in ZSCAN
Browse files Browse the repository at this point in the history
Based on the implementation for the COUNT parameter in the SSCAN implementation:
alicebob#287
  • Loading branch information
BarakSilverfort committed Sep 27, 2023
1 parent ba7b20d commit 4938be5
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 13 deletions.
41 changes: 30 additions & 11 deletions cmd_sorted_set.go
Expand Up @@ -4,6 +4,7 @@ package miniredis

import (
"errors"
"fmt"
"math"
"sort"
"strconv"
Expand Down Expand Up @@ -1448,6 +1449,7 @@ func (m *Miniredis) cmdZscan(c *server.Peer, cmd string, args []string) {
var opts struct {
key string
cursor int
count int
withMatch bool
match string
}
Expand All @@ -1465,12 +1467,18 @@ func (m *Miniredis) cmdZscan(c *server.Peer, cmd string, args []string) {
c.WriteError(msgSyntaxError)
return
}
if _, err := strconv.Atoi(args[1]); err != nil {
count, err := strconv.Atoi(args[1])
if err != nil {
setDirty(c)
c.WriteError(msgInvalidInt)
return
}
// We do nothing with count.
if count <= 0 {
setDirty(c)
c.WriteError(msgSyntaxError)
return
}
opts.count = count
args = args[2:]
continue
}
Expand All @@ -1492,14 +1500,6 @@ func (m *Miniredis) cmdZscan(c *server.Peer, cmd string, args []string) {

withTx(m, c, func(c *server.Peer, ctx *connCtx) {
db := m.db(ctx.selectedDB)
// Paging is not implementend, all results are returned for cursor 0.
if opts.cursor != 0 {
// Invalid cursor.
c.WriteLen(2)
c.WriteBulk("0") // no next cursor
c.WriteLen(0) // no elements
return
}
if db.exists(opts.key) && db.t(opts.key) != "zset" {
c.WriteError(ErrWrongType.Error())
return
Expand All @@ -1510,8 +1510,27 @@ func (m *Miniredis) cmdZscan(c *server.Peer, cmd string, args []string) {
members, _ = matchKeys(members, opts.match)
}

low := opts.cursor
high := low + opts.count
// validate high is correct
if high > len(members) || high == 0 {
high = len(members)
}
if opts.cursor > high {
// invalid cursor
c.WriteLen(2)
c.WriteBulk("0") // no next cursor
c.WriteLen(0) // no elements
return
}
cursorValue := low + opts.count
if cursorValue >= len(members) {
cursorValue = 0 // no next cursor
}
members = members[low:high]

c.WriteLen(2)
c.WriteBulk("0") // no next cursor
c.WriteBulk(fmt.Sprintf("%d", cursorValue))
// HSCAN gives key, values.
c.WriteLen(len(members) * 2)
for _, k := range members {
Expand Down
59 changes: 58 additions & 1 deletion cmd_sorted_set_test.go
Expand Up @@ -1550,17 +1550,74 @@ func TestZscan(t *testing.T) {
"ZSCAN", "set", "0", "COUNT",
proto.Error(msgSyntaxError),
)
mustDo(t, c,
"ZSCAN", "set", "0", "COUNT", "0",
proto.Error(msgSyntaxError),
)
mustDo(t, c,
"ZSCAN", "set", "0", "COUNT", "noint",
proto.Error(msgInvalidInt),
)

mustDo(t, c,
"ZSCAN", "set", "0", "COUNT", "-3",
proto.Error(msgInvalidInt),
)
s.Set("str", "value")
mustDo(t, c,
"ZSCAN", "str", "0",
proto.Error(msgWrongType),
)
})

s.ZAdd("largeset", 1.0, "v1")
s.ZAdd("largeset", 2.0, "v2")
s.ZAdd("largeset", 3.0, "v3")
s.ZAdd("largeset", 4.0, "v4")
s.ZAdd("largeset", 5.0, "v5")
s.ZAdd("largeset", 6.0, "v6")
s.ZAdd("largeset", 7.0, "v7")
s.ZAdd("largeset", 8.0, "v8")

mustDo(t, c,
"ZSCAN", "largeset", "0", "COUNT", "3",
proto.Array(
proto.String("3"),
proto.Array(
proto.String("v1"),
proto.String("1"),
proto.String("v2"),
proto.String("2"),
proto.String("v3"),
proto.String("3"),
),
),
)
mustDo(t, c,
"ZSCAN", "largeset", "3", "COUNT", "3",
proto.Array(
proto.String("6"),
proto.Array(
proto.String("v4"),
proto.String("4"),
proto.String("v5"),
proto.String("5"),
proto.String("v6"),
proto.String("6"),
),
),
)
mustDo(t, c,
"ZSCAN", "largeset", "6", "COUNT", "3",
proto.Array(
proto.String("0"),
proto.Array(
proto.String("v7"),
proto.String("7"),
proto.String("v8"),
proto.String("8"),
),
),
)
}

func TestZunionstore(t *testing.T) {
Expand Down
6 changes: 5 additions & 1 deletion integration/sorted_set_test.go
Expand Up @@ -628,7 +628,6 @@ func TestSortedSetIncyby(t *testing.T) {
}

func TestZscan(t *testing.T) {
skip(t)
testRaw(t, func(c *client) {
// No set yet
c.Do("ZSCAN", "h", "0")
Expand All @@ -638,6 +637,9 @@ func TestZscan(t *testing.T) {
c.Do("ZSCAN", "h", "0", "COUNT", "12")
c.Do("ZSCAN", "h", "0", "cOuNt", "12")

// ZSCAN may return a higher count of items than requested (See https://redis.io/docs/manual/keyspace/), so we must query all items.
c.Do("ZSCAN", "h", "0", "COUNT", "10") // cursor differs

c.Do("ZADD", "h", "2.0", "anotherkey")
c.Do("ZSCAN", "h", "0", "MATCH", "anoth*")
c.Do("ZSCAN", "h", "0", "MATCH", "anoth*", "COUNT", "100")
Expand All @@ -652,6 +654,8 @@ func TestZscan(t *testing.T) {
c.Error("wrong number", "ZSCAN", "noint")
c.Error("not an integer", "ZSCAN", "h", "0", "COUNT", "noint")
c.Error("syntax error", "ZSCAN", "h", "0", "COUNT")
c.Error("syntax error", "ZSCAN", "h", "0", "COUNT", "0")
c.Error("syntax error", "ZSCAN", "h", "0", "COUNT", "-1")
c.Error("syntax error", "ZSCAN", "h", "0", "MATCH")
c.Error("syntax error", "ZSCAN", "h", "0", "garbage")
c.Error("syntax error", "ZSCAN", "h", "0", "COUNT", "12", "MATCH", "foo", "garbage")
Expand Down

0 comments on commit 4938be5

Please sign in to comment.