diff --git a/cmd_sorted_set.go b/cmd_sorted_set.go index 6a70b89f..5b373d47 100644 --- a/cmd_sorted_set.go +++ b/cmd_sorted_set.go @@ -4,6 +4,7 @@ package miniredis import ( "errors" + "math" "sort" "strconv" "strings" @@ -431,23 +432,24 @@ func (m *Miniredis) cmdZlexcount(c *server.Peer, cmd string, args []string) { return } - var opts struct { - Key string - Min string - MinIncl bool - Max string - MaxIncl bool - } - - opts.Key = args[0] - if ok := optLexrange(c, args[1], &opts.Min, &opts.MinIncl); !ok { - return - } - if ok := optLexrange(c, args[2], &opts.Max, &opts.MaxIncl); !ok { - return + var opts = struct { + Key string + Min string + Max string + }{ + Key: args[0], + Min: args[1], + Max: args[2], } withTx(m, c, func(c *server.Peer, ctx *connCtx) { + min, minIncl, minErr := parseLexrange(opts.Min) + max, maxIncl, maxErr := parseLexrange(opts.Max) + if minErr != nil || maxErr != nil { + c.WriteError(msgInvalidRangeItem) + return + } + db := m.db(ctx.selectedDB) if !db.exists(opts.Key) { @@ -463,7 +465,7 @@ func (m *Miniredis) cmdZlexcount(c *server.Peer, cmd string, args []string) { members := db.ssetMembers(opts.Key) // Just key sort. If scores are not the same we don't care. sort.Strings(members) - members = withLexRange(members, opts.Min, opts.MinIncl, opts.Max, opts.MaxIncl) + members = withLexRange(members, min, minIncl, max, maxIncl) c.WriteInt(len(members)) }) @@ -485,23 +487,41 @@ func (m *Miniredis) cmdZrange(c *server.Peer, cmd string, args []string) { var opts struct { Key string - Start int - End int + Min string + Max string WithScores bool + ByScore bool + ByLex bool Reverse bool + WithLimit bool + Offset string + Count string } - opts.Key = args[0] - if ok := optInt(c, args[1], &opts.Start); !ok { - return - } - if ok := optInt(c, args[2], &opts.End); !ok { - return - } + opts.Key, opts.Min, opts.Max = args[0], args[1], args[2] args = args[3:] for len(args) > 0 { switch strings.ToLower(args[0]) { + case "byscore": + opts.ByScore = true + args = args[1:] + case "bylex": + opts.ByLex = true + args = args[1:] + case "rev": + opts.Reverse = true + args = args[1:] + case "limit": + opts.WithLimit = true + args = args[1:] + if len(args) < 2 { + c.WriteError(msgSyntaxError) + return + } + opts.Offset = args[0] + opts.Count = args[1] + args = args[2:] case "withscores": opts.WithScores = true args = args[1:] @@ -512,40 +532,49 @@ func (m *Miniredis) cmdZrange(c *server.Peer, cmd string, args []string) { } withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(opts.Key) { - c.WriteLen(0) - return - } - - if db.t(opts.Key) != "zset" { - c.WriteError(ErrWrongType.Error()) - return - } - - members := db.ssetMembers(opts.Key) - if opts.Reverse { - reverseSlice(members) - } - rs, re := redisRange(len(members), opts.Start, opts.End, false) - if opts.WithScores { - c.WriteLen((re - rs) * 2) - } else { - c.WriteLen(re - rs) - } - for _, el := range members[rs:re] { - c.WriteBulk(el) - if opts.WithScores { - c.WriteFloat(db.ssetScore(opts.Key, el)) + switch { + case opts.ByScore && opts.ByLex: + c.WriteError(msgSyntaxError) + case opts.ByScore: + runRangeByScore(m, c, ctx, optsRangeByScore{ + Key: opts.Key, + Min: opts.Min, + Max: opts.Max, + Reverse: opts.Reverse, + WithLimit: opts.WithLimit, + Offset: opts.Offset, + Count: opts.Count, + WithScores: opts.WithScores, + }) + case opts.ByLex: + runRangeByLex(m, c, ctx, optsRangeByLex{ + Key: opts.Key, + Min: opts.Min, + Max: opts.Max, + Reverse: opts.Reverse, + WithLimit: opts.WithLimit, + Offset: opts.Offset, + Count: opts.Count, + WithScores: opts.WithScores, + }) + default: + if opts.WithLimit { + c.WriteError(msgLimitCombination) + return } + runRange(m, c, ctx, optsRange{ + Key: opts.Key, + Min: opts.Min, + Max: opts.Max, + Reverse: opts.Reverse, + WithScores: opts.WithScores, + }) } }) } // ZREVRANGE func (m *Miniredis) cmdZrevrange(c *server.Peer, cmd string, args []string) { - reverse := true if len(args) < 3 { setDirty(c) c.WriteError(errWrongNumber(cmd)) @@ -558,19 +587,11 @@ func (m *Miniredis) cmdZrevrange(c *server.Peer, cmd string, args []string) { return } - var opts struct { - Key string - Start int - End int - WithScores bool - } - - opts.Key = args[0] - if ok := optInt(c, args[1], &opts.Start); !ok { - return - } - if ok := optInt(c, args[2], &opts.End); !ok { - return + var opts = optsRange{ + Reverse: true, + Key: args[0], + Min: args[1], + Max: args[2], } args = args[3:] @@ -585,35 +606,8 @@ func (m *Miniredis) cmdZrevrange(c *server.Peer, cmd string, args []string) { } } - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(opts.Key) { - c.WriteLen(0) - return - } - - if db.t(opts.Key) != "zset" { - c.WriteError(ErrWrongType.Error()) - return - } - - members := db.ssetMembers(opts.Key) - if reverse { - reverseSlice(members) - } - rs, re := redisRange(len(members), opts.Start, opts.End, false) - if opts.WithScores { - c.WriteLen((re - rs) * 2) - } else { - c.WriteLen(re - rs) - } - for _, el := range members[rs:re] { - c.WriteBulk(el) - if opts.WithScores { - c.WriteFloat(db.ssetScore(opts.Key, el)) - } - } + withTx(m, c, func(c *server.Peer, cctx *connCtx) { + runRange(m, c, cctx, opts) }) } @@ -631,24 +625,11 @@ func (m *Miniredis) makeCmdZrangebylex(reverse bool) server.Cmd { if m.checkPubsub(c, cmd) { return } - - var opts struct { - Key string - Min string - MinIncl bool - Max string - MaxIncl bool - WithLimit bool - LimitStart int - LimitEnd int - } - - opts.Key = args[0] - if ok := optLexrange(c, args[1], &opts.Min, &opts.MinIncl); !ok { - return - } - if ok := optLexrange(c, args[2], &opts.Max, &opts.MaxIncl); !ok { - return + opts := optsRangeByLex{ + Reverse: reverse, + Key: args[0], + Min: args[1], + Max: args[2], } args = args[3:] @@ -661,12 +642,8 @@ func (m *Miniredis) makeCmdZrangebylex(reverse bool) server.Cmd { c.WriteError(msgSyntaxError) return } - if ok := optInt(c, args[0], &opts.LimitStart); !ok { - return - } - if ok := optInt(c, args[1], &opts.LimitEnd); !ok { - return - } + opts.Offset = args[0] + opts.Count = args[1] args = args[2:] continue default: @@ -677,56 +654,8 @@ func (m *Miniredis) makeCmdZrangebylex(reverse bool) server.Cmd { } } - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(opts.Key) { - c.WriteLen(0) - return - } - - if db.t(opts.Key) != "zset" { - c.WriteError(ErrWrongType.Error()) - return - } - - members := db.ssetMembers(opts.Key) - // Just key sort. If scores are not the same we don't care. - sort.Strings(members) - min, max := opts.Min, opts.Max - minIncl, maxIncl := opts.MinIncl, opts.MaxIncl - if reverse { - min, max = max, min - minIncl, maxIncl = maxIncl, minIncl - } - members = withLexRange(members, min, minIncl, max, maxIncl) - if reverse { - reverseSlice(members) - } - - // Apply LIMIT ranges. That's . Unlike RANGE. - if opts.WithLimit { - if opts.LimitStart < 0 { - members = nil - } else { - if opts.LimitStart < len(members) { - members = members[opts.LimitStart:] - } else { - // out of range - members = nil - } - if opts.LimitEnd >= 0 { - if len(members) > opts.LimitEnd { - members = members[:opts.LimitEnd] - } - } - } - } - - c.WriteLen(len(members)) - for _, el := range members { - c.WriteBulk(el) - } + withTx(m, c, func(c *server.Peer, cctx *connCtx) { + runRangeByLex(m, c, cctx, opts) }) } } @@ -746,50 +675,29 @@ func (m *Miniredis) makeCmdZrangebyscore(reverse bool) server.Cmd { return } - key := args[0] - min, minIncl, err := parseFloatRange(args[1]) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidMinMax) - return - } - max, maxIncl, err := parseFloatRange(args[2]) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidMinMax) - return + var opts = optsRangeByScore{ + Reverse: reverse, + Key: args[0], + Min: args[1], + Max: args[2], } args = args[3:] - withScores := false - withLimit := false - limitStart := 0 - limitEnd := 0 for len(args) > 0 { if strings.ToLower(args[0]) == "limit" { - withLimit = true + opts.WithLimit = true args = args[1:] if len(args) < 2 { c.WriteError(msgSyntaxError) return } - limitStart, err = strconv.Atoi(args[0]) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - limitEnd, err = strconv.Atoi(args[1]) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } + opts.Offset = args[0] + opts.Count = args[1] args = args[2:] continue } if strings.ToLower(args[0]) == "withscores" { - withScores = true + opts.WithScores = true args = args[1:] continue } @@ -798,59 +706,8 @@ func (m *Miniredis) makeCmdZrangebyscore(reverse bool) server.Cmd { return } - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(key) { - c.WriteLen(0) - return - } - - if db.t(key) != "zset" { - c.WriteError(ErrWrongType.Error()) - return - } - - members := db.ssetElements(key) - if reverse { - min, max = max, min - minIncl, maxIncl = maxIncl, minIncl - } - members = withSSRange(members, min, minIncl, max, maxIncl) - if reverse { - reverseElems(members) - } - - // Apply LIMIT ranges. That's . Unlike RANGE. - if withLimit { - if limitStart < 0 { - members = ssElems{} - } else { - if limitStart < len(members) { - members = members[limitStart:] - } else { - // out of range - members = ssElems{} - } - if limitEnd >= 0 { - if len(members) > limitEnd { - members = members[:limitEnd] - } - } - } - } - - if withScores { - c.WriteLen(len(members) * 2) - } else { - c.WriteLen(len(members)) - } - for _, el := range members { - c.WriteBulk(el.member) - if withScores { - c.WriteFloat(el.score) - } - } + withTx(m, c, func(c *server.Peer, cctx *connCtx) { + runRangeByScore(m, c, cctx, opts) }) } } @@ -952,23 +809,24 @@ func (m *Miniredis) cmdZremrangebylex(c *server.Peer, cmd string, args []string) return } - var opts struct { - Key string - Min string - MinIncl bool - Max string - MaxIncl bool - } - - opts.Key = args[0] - if ok := optLexrange(c, args[1], &opts.Min, &opts.MinIncl); !ok { - return - } - if ok := optLexrange(c, args[2], &opts.Max, &opts.MaxIncl); !ok { - return + var opts = struct { + Key string + Min string + Max string + }{ + Key: args[0], + Min: args[1], + Max: args[2], } withTx(m, c, func(c *server.Peer, ctx *connCtx) { + min, minIncl, minErr := parseLexrange(opts.Min) + max, maxIncl, maxErr := parseLexrange(opts.Max) + if minErr != nil || maxErr != nil { + c.WriteError(msgInvalidRangeItem) + return + } + db := m.db(ctx.selectedDB) if !db.exists(opts.Key) { @@ -984,7 +842,7 @@ func (m *Miniredis) cmdZremrangebylex(c *server.Peer, cmd string, args []string) members := db.ssetMembers(opts.Key) // Just key sort. If scores are not the same we don't care. sort.Strings(members) - members = withLexRange(members, opts.Min, opts.MinIncl, opts.Max, opts.MaxIncl) + members = withLexRange(members, min, minIncl, max, maxIncl) for _, el := range members { db.ssetRem(opts.Key, el) @@ -1143,8 +1001,15 @@ func parseFloatRange(s string) (float64, bool, error) { s = s[1:] inclusive = false } - f, err := strconv.ParseFloat(s, 64) - return f, inclusive, err + switch strings.ToLower(s) { + case "+inf": + return math.Inf(+1), true, nil + case "-inf": + return math.Inf(-1), true, nil + default: + f, err := strconv.ParseFloat(s, 64) + return f, inclusive, err + } } // withSSRange limits a list of sorted set elements by the ZRANGEBYSCORE range @@ -1726,3 +1591,240 @@ func (m *Miniredis) cmdZrandmember(c *server.Peer, cmd string, args []string) { c.WriteStrings(members) }) } + +type optsRange struct { + Key string + Min string + Max string + Reverse bool + WithScores bool +} + +func runRange(m *Miniredis, c *server.Peer, cctx *connCtx, opts optsRange) { + min, minErr := strconv.Atoi(opts.Min) + max, maxErr := strconv.Atoi(opts.Max) + if minErr != nil || maxErr != nil { + c.WriteError(msgInvalidInt) + return + } + + db := m.db(cctx.selectedDB) + + if !db.exists(opts.Key) { + c.WriteLen(0) + return + } + + if db.t(opts.Key) != "zset" { + c.WriteError(ErrWrongType.Error()) + return + } + + members := db.ssetMembers(opts.Key) + if opts.Reverse { + reverseSlice(members) + } + rs, re := redisRange(len(members), min, max, false) + if opts.WithScores { + c.WriteLen((re - rs) * 2) + } else { + c.WriteLen(re - rs) + } + for _, el := range members[rs:re] { + c.WriteBulk(el) + if opts.WithScores { + c.WriteFloat(db.ssetScore(opts.Key, el)) + } + } +} + +type optsRangeByScore struct { + Key string + Min string + Max string + Reverse bool + WithLimit bool + Offset string + Count string + WithScores bool +} + +func runRangeByScore(m *Miniredis, c *server.Peer, cctx *connCtx, opts optsRangeByScore) { + var limitOffset, limitCount int + var err error + if opts.WithLimit { + limitOffset, err = strconv.Atoi(opts.Offset) + if err != nil { + c.WriteError(msgInvalidInt) + return + } + limitCount, err = strconv.Atoi(opts.Count) + if err != nil { + c.WriteError(msgInvalidInt) + return + } + } + min, minIncl, minErr := parseFloatRange(opts.Min) + max, maxIncl, maxErr := parseFloatRange(opts.Max) + if minErr != nil || maxErr != nil { + c.WriteError(msgInvalidMinMax) + return + } + + db := m.db(cctx.selectedDB) + + if !db.exists(opts.Key) { + c.WriteLen(0) + return + } + + if db.t(opts.Key) != "zset" { + c.WriteError(ErrWrongType.Error()) + return + } + + members := db.ssetElements(opts.Key) + if opts.Reverse { + min, max = max, min + minIncl, maxIncl = maxIncl, minIncl + } + members = withSSRange(members, min, minIncl, max, maxIncl) + if opts.Reverse { + reverseElems(members) + } + + // Apply LIMIT ranges. That's . Unlike RANGE. + if opts.WithLimit { + if limitOffset < 0 { + members = ssElems{} + } else { + if limitOffset < len(members) { + members = members[limitOffset:] + } else { + // out of range + members = ssElems{} + } + if limitCount >= 0 { + if len(members) > limitCount { + members = members[:limitCount] + } + } + } + } + + if opts.WithScores { + c.WriteLen(len(members) * 2) + } else { + c.WriteLen(len(members)) + } + for _, el := range members { + c.WriteBulk(el.member) + if opts.WithScores { + c.WriteFloat(el.score) + } + } +} + +type optsRangeByLex struct { + Key string + Min string + Max string + Reverse bool + WithLimit bool + Offset string + Count string + WithScores bool +} + +func runRangeByLex(m *Miniredis, c *server.Peer, cctx *connCtx, opts optsRangeByLex) { + var limitOffset, limitCount int + var err error + if opts.WithLimit { + limitOffset, err = strconv.Atoi(opts.Offset) + if err != nil { + c.WriteError(msgInvalidInt) + return + } + limitCount, err = strconv.Atoi(opts.Count) + if err != nil { + c.WriteError(msgInvalidInt) + return + } + } + min, minIncl, minErr := parseLexrange(opts.Min) + max, maxIncl, maxErr := parseLexrange(opts.Max) + if minErr != nil || maxErr != nil { + c.WriteError(msgInvalidRangeItem) + return + } + + db := m.db(cctx.selectedDB) + + if !db.exists(opts.Key) { + c.WriteLen(0) + return + } + + if db.t(opts.Key) != "zset" { + c.WriteError(ErrWrongType.Error()) + return + } + + members := db.ssetMembers(opts.Key) + // Just key sort. If scores are not the same we don't care. + sort.Strings(members) + if opts.Reverse { + min, max = max, min + minIncl, maxIncl = maxIncl, minIncl + } + members = withLexRange(members, min, minIncl, max, maxIncl) + if opts.Reverse { + reverseSlice(members) + } + + // Apply LIMIT ranges. That's . Unlike RANGE. + if opts.WithLimit { + if limitOffset < 0 { + members = nil + } else { + if limitOffset < len(members) { + members = members[limitOffset:] + } else { + // out of range + members = nil + } + if limitCount >= 0 { + if len(members) > limitCount { + members = members[:limitCount] + } + } + } + } + + c.WriteLen(len(members)) + for _, el := range members { + c.WriteBulk(el) + } +} + +// optLexrange handles ZRANGE{,BYLEX} ranges. They start with '[', '(', or are +// '+' or '-'. +// Sets destValue and destInclusive. destValue can be '+' or '-'. +func parseLexrange(s string) (string, bool, error) { + if len(s) == 0 { + return "", false, errors.New(msgInvalidRangeItem) + } + + if s == "+" || s == "-" { + return s, false, nil + } + + switch s[0] { + case '(': + return s[1:], false, nil + case '[': + return s[1:], true, nil + default: + return "", false, errors.New(msgInvalidRangeItem) + } +} diff --git a/cmd_sorted_set_test.go b/cmd_sorted_set_test.go index f3ca482a..de0d4ba5 100644 --- a/cmd_sorted_set_test.go +++ b/cmd_sorted_set_test.go @@ -339,6 +339,28 @@ func TestSortedSetRange(t *testing.T) { ) }) + t.Run("reverse", func(t *testing.T) { + mustDo(t, c, + "ZRANGE", "z", "0", "-1", "REV", + proto.Strings("inf", "three", "drei", "zwei", "two", "one"), + ) + }) + + t.Run("limit", func(t *testing.T) { + mustDo(t, c, + "ZRANGE", "z", "0", "+inf", "BYSCORE", "LIMIT", "1", "2", + proto.Strings("two", "zwei"), + ) + mustDo(t, c, + "ZRANGE", "z", "0", "+inf", "BYSCORE", "LIMIT", "1", "-1", + proto.Strings("two", "zwei", "drei", "three", "inf"), + ) + mustDo(t, c, + "ZRANGE", "z", "0", "+inf", "BYSCORE", "LIMIT", "1", "9999", + proto.Strings("two", "zwei", "drei", "three", "inf"), + ) + }) + t.Run("errors", func(t *testing.T) { mustDo(t, c, "ZRANGE", @@ -364,6 +386,10 @@ func TestSortedSetRange(t *testing.T) { "ZRANGE", "set", "1", "2", "toomany", proto.Error(msgSyntaxError), ) + mustDo(t, c, + "ZRANGE", "set", "1", "2", "LIMIT", "1", "2", + proto.Error(msgLimitCombination), + ) // Wrong type of key s.Set("str", "value") mustDo(t, c, @@ -642,20 +668,20 @@ func TestSortedSetRangeByScore(t *testing.T) { proto.Error(msgSyntaxError), ) mustDo(t, c, - "ZRANGEBYSCORE", "set", "[1", "2", "toomany", + "ZRANGEBYSCORE", "set", "[1", "2", proto.Error("ERR min or max is not a float"), ) mustDo(t, c, - "ZRANGEBYSCORE", "set", "1", "[2", "toomany", + "ZRANGEBYSCORE", "set", "1", "[2", proto.Error("ERR min or max is not a float"), ) mustDo(t, c, "ZRANGEBYSCORE", "set", "[1", "2", "LIMIT", "noint", "1", - proto.Error("ERR min or max is not a float"), + proto.Error(msgInvalidInt), ) mustDo(t, c, "ZRANGEBYSCORE", "set", "[1", "2", "LIMIT", "1", "noint", - proto.Error("ERR min or max is not a float"), + proto.Error(msgInvalidInt), ) // Wrong type of key s.Set("str", "value") diff --git a/integration/sorted_set_test.go b/integration/sorted_set_test.go index 44ea3ba3..3cd11de0 100644 --- a/integration/sorted_set_test.go +++ b/integration/sorted_set_test.go @@ -170,23 +170,61 @@ func TestSortedSetRange(t *testing.T) { "+Inf", "more stars", "-Inf", "big bang", ) - c.Do("ZRANGE", "z", "0", "-1") - c.Do("ZRANGE", "z", "0", "-1", "WITHSCORES", "WITHSCORES") - c.Do("ZRANGE", "z", "0", "-1", "WiThScOrEs") - c.Do("ZRANGE", "z", "0", "-2") - c.Do("ZRANGE", "z", "0", "-1000") - c.Do("ZRANGE", "z", "2", "-2") - c.Do("ZRANGE", "z", "400", "-1") - c.Do("ZRANGE", "z", "300", "-110") - c.Do("ZREVRANGE", "z", "0", "-1") - c.Do("ZREVRANGE", "z", "0", "-1", "WITHSCORES") - c.Do("ZREVRANGE", "z", "0", "-1", "WITHSCORES", "WITHSCORES", "WITHSCORES") - c.Do("ZREVRANGE", "z", "0", "-1", "WiThScOrEs") - c.Do("ZREVRANGE", "z", "0", "-2") - c.Do("ZREVRANGE", "z", "0", "-1000") - c.Do("ZREVRANGE", "z", "2", "-2") - c.Do("ZREVRANGE", "z", "400", "-1") - c.Do("ZREVRANGE", "z", "300", "-110") + c.Do("ZADD", "zs", + "5", "berlin", + "5", "lisbon", + "5", "manila", + "5", "budapest", + "5", "london", + "5", "singapore", + "5", "amsterdam", + ) + + t.Run("plain", func(t *testing.T) { + c.Do("ZRANGE", "z", "0", "-1") + c.Do("ZRANGE", "z", "0", "10", "WITHSCORES", "WITHSCORES") + c.Do("ZRANGE", "z", "0", "-1", "WiThScOrEs") + c.Do("ZRANGE", "z", "0", "10") + c.Do("ZRANGE", "z", "0", "2") + c.Do("ZRANGE", "z", "2", "20") + c.Do("ZRANGE", "z", "0", "-4") + c.Do("ZRANGE", "z", "2", "-4") + c.Do("ZRANGE", "z", "400", "-1") + c.Do("ZRANGE", "z", "300", "-110") + c.Do("ZRANGE", "z", "0", "-1", "REV") + c.Error("not an integer", "ZRANGE", "z", "(0", "-1") + c.Error("not an integer", "ZRANGE", "z", "0", "(-1") + c.Error("combination", "ZRANGE", "z", "0", "-1", "LIMIT", "1", "2") + }) + + t.Run("byscore", func(t *testing.T) { + c.Do("ZRANGE", "z", "0", "-1", "BYSCORE") + c.Do("ZRANGE", "z", "0", "1000", "BYSCORE") + c.Do("ZRANGE", "z", "1", "2", "BYSCORE") + c.Do("ZRANGE", "z", "1", "(2", "BYSCORE") + c.Do("ZRANGE", "z", "-inf", "+inf", "BYSCORE") + c.Do("ZRANGE", "z", "-inf", "+inf", "BYSCORE", "REV") + c.Do("ZRANGE", "z", "-inf", "+inf", "BYSCORE", "LIMIT", "0", "1") + c.Do("ZRANGE", "z", "-inf", "+inf", "BYSCORE", "LIMIT", "1", "2") + c.Do("ZRANGE", "z", "-inf", "+inf", "BYSCORE", "LIMIT", "0", "-1") + c.Do("ZRANGE", "z", "-inf", "+inf", "BYSCORE", "REV", "LIMIT", "0", "1") + c.Error("not a float", "ZRANGE", "z", "[1", "2", "BYSCORE") + }) + + t.Run("bylex", func(t *testing.T) { + c.Do("ZRANGE", "zs", "[be", "(ma", "BYLEX") + c.Do("ZRANGE", "zs", "[be", "+", "BYLEX") + c.Do("ZRANGE", "zs", "-", "(ma", "BYLEX") + c.Do("ZRANGE", "zs", "-", "+", "BYLEX") + c.Do("ZRANGE", "zs", "[be", "(ma", "BYLEX", "REV") + c.Do("ZRANGE", "zs", "-", "+", "BYLEX", "LIMIT", "0", "1") + c.Do("ZRANGE", "zs", "-", "+", "BYLEX", "LIMIT", "1", "3") + c.Do("ZRANGE", "zs", "-", "+", "BYLEX", "LIMIT", "1", "-1") + c.Do("ZRANGE", "zs", "-", "+", "BYLEX", "LIMIT", "1", "-1", "REV") + c.Error("syntax error", "ZRANGE", "z", "[be", "[ma", "BYSCORE", "BYLEX") + c.Error("range item", "ZRANGE", "z", "be", "(ma", "BYLEX") + c.Error("range item", "ZRANGE", "z", "(be", "ma", "BYLEX") + }) c.Do("ZADD", "zz", "0", "aap", @@ -207,8 +245,34 @@ func TestSortedSetRange(t *testing.T) { c.Error("not an integer", "ZRANGE", "foo", "2", "noint") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "ZRANGE", "str", "300", "-110") + }) +} +func TestSortedSetRevRange(t *testing.T) { + testRaw(t, func(c *client) { + c.Do("ZADD", "z", + "1", "aap", + "2", "noot", + "3", "mies", + "2", "nootagain", + "3", "miesagain", + "+Inf", "the stars", + "+Inf", "more stars", + "-Inf", "big bang", + ) + c.Do("ZREVRANGE", "z", "0", "-1") + c.Do("ZREVRANGE", "z", "0", "-1", "WITHSCORES") + c.Do("ZREVRANGE", "z", "0", "-1", "WITHSCORES", "WITHSCORES", "WITHSCORES") + c.Do("ZREVRANGE", "z", "0", "-1", "WiThScOrEs") + c.Do("ZREVRANGE", "z", "0", "-2") + c.Do("ZREVRANGE", "z", "0", "-1000") + c.Do("ZREVRANGE", "z", "2", "-2") + c.Do("ZREVRANGE", "z", "400", "-1") + c.Do("ZREVRANGE", "z", "300", "-110") + c.Error("syntax", "ZREVRANGE", "z", "300", "-110", "REV") + // failure cases c.Error("wrong number", "ZREVRANGE") + c.Do("SET", "str", "I am a string") c.Error("wrong kind", "ZREVRANGE", "str", "300", "-110") }) } diff --git a/integration/test.go b/integration/test.go index 27159b95..1fece403 100644 --- a/integration/test.go +++ b/integration/test.go @@ -508,10 +508,9 @@ func (c *client) Error(msg string, cmd string, args ...string) { return } - // c.t.Logf("real:%q mini:%q", string(resReal), string(resMini)) - mini, err := proto.ReadError(resMini) if err != nil { + c.t.Logf("real:%q mini:%q", string(resReal), string(resMini)) c.t.Errorf("parse error miniredis: %s", err) return } diff --git a/opts.go b/opts.go index 86650818..016d2682 100644 --- a/opts.go +++ b/opts.go @@ -19,36 +19,3 @@ func optInt(c *server.Peer, src string, dest *int) bool { *dest = n return true } - -// optLexrange handles ZRANGE{,BYLEX} ranges. They start with '[', '(', or are -// '+' or '-'. -// Sets destValue and destInclusive. destValue can be '+' or '-'. -// Returns whether or not things were okay. -func optLexrange(c *server.Peer, s string, destValue *string, destInclusive *bool) bool { - if len(s) == 0 { - setDirty(c) - c.WriteError(msgInvalidRangeItem) - return false - } - - if s == "+" || s == "-" { - *destValue = s - *destInclusive = false - return true - } - - switch s[0] { - case '(': - *destValue = s[1:] - *destInclusive = false - return true - case '[': - *destValue = s[1:] - *destInclusive = true - return true - default: - setDirty(c) - c.WriteError(msgInvalidRangeItem) - return false - } -} diff --git a/redis.go b/redis.go index b1ccb30e..0f4c049c 100644 --- a/redis.go +++ b/redis.go @@ -47,6 +47,7 @@ const ( msgXtrimInvalidMaxLen = "ERR value is not an integer or out of range" msgXtrimInvalidLimit = "ERR syntax error, LIMIT cannot be used without the special ~ option" msgDBIndexOutOfRange = "ERR DB index is out of range" + msgLimitCombination = "ERR syntax error, LIMIT is only supported in combination with either BYSCORE or BYLEX" ) func errWrongNumber(cmd string) string {