This is an automated email from the ASF dual-hosted git repository.
maplefu pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks.git
The following commit(s) were added to refs/heads/unstable by this push:
new 3b8c69fa Add support of the SORT command (#2262)
3b8c69fa is described below
commit 3b8c69fa33967bd3e3f623ffcb8f2591dd7ea340
Author: Zhou SiLe <[email protected]>
AuthorDate: Tue May 7 15:02:38 2024 +0800
Add support of the SORT command (#2262)
Co-authored-by: 纪华裕 <[email protected]>
Co-authored-by: hulk <[email protected]>
Co-authored-by: mwish <[email protected]>
---
src/commands/cmd_key.cc | 118 ++++-
src/storage/redis_db.cc | 212 +++++++++
src/storage/redis_db.h | 63 +++
tests/gocase/unit/sort/sort_test.go | 881 ++++++++++++++++++++++++++++++++++++
4 files changed, 1273 insertions(+), 1 deletion(-)
diff --git a/src/commands/cmd_key.cc b/src/commands/cmd_key.cc
index 589fa1ed..24d8fe29 100644
--- a/src/commands/cmd_key.cc
+++ b/src/commands/cmd_key.cc
@@ -424,6 +424,120 @@ class CommandCopy : public Commander {
bool replace_ = false;
};
+template <bool ReadOnly>
+class CommandSort : public Commander {
+ public:
+ Status Parse(const std::vector<std::string> &args) override {
+ CommandParser parser(args, 2);
+ while (parser.Good()) {
+ if (parser.EatEqICase("BY")) {
+ if (!sort_argument_.sortby.empty()) return {Status::InvalidArgument,
"don't use multiple BY parameters"};
+ sort_argument_.sortby = GET_OR_RET(parser.TakeStr());
+
+ if (sort_argument_.sortby.find('*') == std::string::npos) {
+ sort_argument_.dontsort = true;
+ } else {
+ /* TODO:
+ * If BY is specified with a real pattern, we can't accept it in
cluster mode,
+ * unless we can make sure the keys formed by the pattern are in the
same slot
+ * as the key to sort.
+ * If BY is specified with a real pattern, we can't accept
+ * it if no full ACL key access is applied for this command. */
+ }
+ } else if (parser.EatEqICase("LIMIT")) {
+ sort_argument_.offset = GET_OR_RET(parser.template TakeInt<int>());
+ sort_argument_.count = GET_OR_RET(parser.template TakeInt<int>());
+ } else if (parser.EatEqICase("GET")) {
+ /* TODO:
+ * If GET is specified with a real pattern, we can't accept it in
cluster mode,
+ * unless we can make sure the keys formed by the pattern are in the
same slot
+ * as the key to sort. */
+ sort_argument_.getpatterns.push_back(GET_OR_RET(parser.TakeStr()));
+ } else if (parser.EatEqICase("ASC")) {
+ sort_argument_.desc = false;
+ } else if (parser.EatEqICase("DESC")) {
+ sort_argument_.desc = true;
+ } else if (parser.EatEqICase("ALPHA")) {
+ sort_argument_.alpha = true;
+ } else if (parser.EatEqICase("STORE")) {
+ if constexpr (ReadOnly) {
+ return {Status::RedisParseErr, "SORT_RO is read-only and does not
support the STORE parameter"};
+ }
+ sort_argument_.storekey = GET_OR_RET(parser.TakeStr());
+ } else {
+ return parser.InvalidSyntax();
+ }
+ }
+
+ return Status::OK();
+ }
+
+ Status Execute(Server *srv, Connection *conn, std::string *output) override {
+ redis::Database redis(srv->storage, conn->GetNamespace());
+ RedisType type = kRedisNone;
+ if (auto s = redis.Type(args_[1], &type); !s.ok()) {
+ return {Status::RedisExecErr, s.ToString()};
+ }
+
+ if (type != RedisType::kRedisList && type != RedisType::kRedisSet && type
!= RedisType::kRedisZSet) {
+ *output = Error("WRONGTYPE Operation against a key holding the wrong
kind of value");
+ return Status::OK();
+ }
+
+ /* When sorting a set with no sort specified, we must sort the output
+ * so the result is consistent across scripting and replication.
+ *
+ * The other types (list, sorted set) will retain their native order
+ * even if no sort order is requested, so they remain stable across
+ * scripting and replication.
+ *
+ * TODO: support CLIENT_SCRIPT flag, (!storekey_.empty() || c->flags &
CLIENT_SCRIPT)) */
+ if (sort_argument_.dontsort && type == RedisType::kRedisSet &&
(!sort_argument_.storekey.empty())) {
+ /* Force ALPHA sorting */
+ sort_argument_.dontsort = false;
+ sort_argument_.alpha = true;
+ sort_argument_.sortby = "";
+ }
+
+ std::vector<std::optional<std::string>> sorted_elems;
+ Database::SortResult res = Database::SortResult::DONE;
+
+ if (auto s = redis.Sort(type, args_[1], sort_argument_, &sorted_elems,
&res); !s.ok()) {
+ return {Status::RedisExecErr, s.ToString()};
+ }
+
+ switch (res) {
+ case Database::SortResult::UNKNOWN_TYPE:
+ *output = redis::Error("Unknown Type");
+ break;
+ case Database::SortResult::DOUBLE_CONVERT_ERROR:
+ *output = redis::Error("One or more scores can't be converted into
double");
+ break;
+ case Database::SortResult::LIMIT_EXCEEDED:
+ *output = redis::Error("The number of elements to be sorted exceeds
SORT_LENGTH_LIMIT = " +
+ std::to_string(SORT_LENGTH_LIMIT));
+ break;
+ case Database::SortResult::DONE:
+ if (sort_argument_.storekey.empty()) {
+ std::vector<std::string> output_vec;
+ output_vec.reserve(sorted_elems.size());
+ for (const auto &elem : sorted_elems) {
+ output_vec.emplace_back(elem.has_value() ?
redis::BulkString(elem.value()) : conn->NilString());
+ }
+ *output = redis::Array(output_vec);
+ } else {
+ *output = Integer(sorted_elems.size());
+ }
+ break;
+ }
+
+ return Status::OK();
+ }
+
+ private:
+ SortArgument sort_argument_;
+};
+
REDIS_REGISTER_COMMANDS(MakeCmdAttr<CommandTTL>("ttl", 2, "read-only", 1, 1,
1),
MakeCmdAttr<CommandPTTL>("pttl", 2, "read-only", 1, 1,
1),
MakeCmdAttr<CommandType>("type", 2, "read-only", 1, 1,
1),
@@ -442,6 +556,8 @@ REDIS_REGISTER_COMMANDS(MakeCmdAttr<CommandTTL>("ttl", 2,
"read-only", 1, 1, 1),
MakeCmdAttr<CommandDel>("unlink", -2, "write
no-dbsize-check", 1, -1, 1),
MakeCmdAttr<CommandRename>("rename", 3, "write", 1, 2,
1),
MakeCmdAttr<CommandRenameNX>("renamenx", 3, "write",
1, 2, 1),
- MakeCmdAttr<CommandCopy>("copy", -3, "write", 1, 2,
1), )
+ MakeCmdAttr<CommandCopy>("copy", -3, "write", 1, 2, 1),
+ MakeCmdAttr<CommandSort<false>>("sort", -2, "write",
1, 1, 1),
+ MakeCmdAttr<CommandSort<true>>("sort_ro", -2,
"read-only", 1, 1, 1))
} // namespace redis
diff --git a/src/storage/redis_db.cc b/src/storage/redis_db.cc
index 3ff1fa0a..13f9bd2f 100644
--- a/src/storage/redis_db.cc
+++ b/src/storage/redis_db.cc
@@ -35,6 +35,11 @@
#include "storage/redis_metadata.h"
#include "storage/storage.h"
#include "time_util.h"
+#include "types/redis_hash.h"
+#include "types/redis_list.h"
+#include "types/redis_set.h"
+#include "types/redis_string.h"
+#include "types/redis_zset.h"
namespace redis {
@@ -768,4 +773,211 @@ rocksdb::Status Database::Copy(const std::string &key,
const std::string &new_ke
return storage_->Write(storage_->DefaultWriteOptions(),
batch->GetWriteBatch());
}
+std::optional<std::string> Database::lookupKeyByPattern(const std::string
&pattern, const std::string &subst) {
+ if (pattern == "#") {
+ return subst;
+ }
+
+ auto match_pos = pattern.find('*');
+ if (match_pos == std::string::npos) {
+ return std::nullopt;
+ }
+
+ // hash field
+ std::string field;
+ auto arrow_pos = pattern.find("->", match_pos + 1);
+ if (arrow_pos != std::string::npos && arrow_pos + 2 < pattern.size()) {
+ field = pattern.substr(arrow_pos + 2);
+ }
+
+ std::string key = pattern.substr(0, match_pos + 1);
+ key.replace(match_pos, 1, subst);
+
+ std::string value;
+ RedisType type = RedisType::kRedisNone;
+ if (!field.empty()) {
+ auto hash_db = redis::Hash(storage_, namespace_);
+ if (auto s = hash_db.Type(key, &type); !s.ok() || type !=
RedisType::kRedisHash) {
+ return std::nullopt;
+ }
+
+ if (auto s = hash_db.Get(key, field, &value); !s.ok()) {
+ return std::nullopt;
+ }
+ } else {
+ auto string_db = redis::String(storage_, namespace_);
+ if (auto s = string_db.Type(key, &type); !s.ok() || type !=
RedisType::kRedisString) {
+ return std::nullopt;
+ }
+ if (auto s = string_db.Get(key, &value); !s.ok()) {
+ return std::nullopt;
+ }
+ }
+ return value;
+}
+
+rocksdb::Status Database::Sort(RedisType type, const std::string &key, const
SortArgument &args,
+ std::vector<std::optional<std::string>> *elems,
SortResult *res) {
+ // Obtain the length of the object to sort.
+ const std::string ns_key = AppendNamespacePrefix(key);
+ Metadata metadata(type, false);
+ auto s = GetMetadata(GetOptions{}, {type}, ns_key, &metadata);
+ if (!s.ok()) return s;
+
+ if (metadata.size > SORT_LENGTH_LIMIT) {
+ *res = SortResult::LIMIT_EXCEEDED;
+ return rocksdb::Status::OK();
+ }
+ auto vectorlen = static_cast<int>(metadata.size);
+
+ // Adjust the offset and count of the limit
+ int offset = args.offset >= vectorlen ? 0 : std::clamp(args.offset, 0,
vectorlen - 1);
+ int count = args.offset >= vectorlen ? 0 : std::clamp(args.count, -1,
vectorlen - offset);
+ if (count == -1) count = vectorlen - offset;
+
+ // Get the elements that need to be sorted
+ std::vector<std::string> str_vec;
+ if (count != 0) {
+ if (type == RedisType::kRedisList) {
+ auto list_db = redis::List(storage_, namespace_);
+
+ if (args.dontsort) {
+ if (args.desc) {
+ s = list_db.Range(key, -count - offset, -1 - offset, &str_vec);
+ if (!s.ok()) return s;
+ std::reverse(str_vec.begin(), str_vec.end());
+ } else {
+ s = list_db.Range(key, offset, offset + count - 1, &str_vec);
+ if (!s.ok()) return s;
+ }
+ } else {
+ s = list_db.Range(key, 0, -1, &str_vec);
+ if (!s.ok()) return s;
+ }
+ } else if (type == RedisType::kRedisSet) {
+ auto set_db = redis::Set(storage_, namespace_);
+ s = set_db.Members(key, &str_vec);
+ if (!s.ok()) return s;
+
+ if (args.dontsort) {
+ str_vec = std::vector(std::make_move_iterator(str_vec.begin() +
offset),
+ std::make_move_iterator(str_vec.begin() + offset
+ count));
+ }
+ } else if (type == RedisType::kRedisZSet) {
+ auto zset_db = redis::ZSet(storage_, namespace_);
+ std::vector<MemberScore> member_scores;
+
+ if (args.dontsort) {
+ RangeRankSpec spec;
+ spec.start = offset;
+ spec.stop = offset + count - 1;
+ spec.reversed = args.desc;
+ s = zset_db.RangeByRank(key, spec, &member_scores, nullptr);
+ if (!s.ok()) return s;
+
+ for (auto &member_score : member_scores) {
+ str_vec.emplace_back(std::move(member_score.member));
+ }
+ } else {
+ s = zset_db.GetAllMemberScores(key, &member_scores);
+ if (!s.ok()) return s;
+
+ for (auto &member_score : member_scores) {
+ str_vec.emplace_back(std::move(member_score.member));
+ }
+ }
+ } else {
+ *res = SortResult::UNKNOWN_TYPE;
+ return s;
+ }
+ }
+
+ std::vector<RedisSortObject> sort_vec(str_vec.size());
+ for (size_t i = 0; i < str_vec.size(); ++i) {
+ sort_vec[i].obj = str_vec[i];
+ }
+
+ // Sort by BY, ALPHA, ASC/DESC
+ if (!args.dontsort) {
+ for (size_t i = 0; i < sort_vec.size(); ++i) {
+ std::string byval;
+ if (!args.sortby.empty()) {
+ auto lookup = lookupKeyByPattern(args.sortby, str_vec[i]);
+ if (!lookup.has_value()) continue;
+ byval = std::move(lookup.value());
+ } else {
+ byval = str_vec[i];
+ }
+
+ if (args.alpha && !args.sortby.empty()) {
+ sort_vec[i].v = byval;
+ } else if (!args.alpha && !byval.empty()) {
+ auto double_byval = ParseFloat<double>(byval);
+ if (!double_byval) {
+ *res = SortResult::DOUBLE_CONVERT_ERROR;
+ return rocksdb::Status::OK();
+ }
+ sort_vec[i].v = *double_byval;
+ }
+ }
+
+ std::sort(sort_vec.begin(), sort_vec.end(), [&args](const RedisSortObject
&a, const RedisSortObject &b) {
+ return RedisSortObject::SortCompare(a, b, args);
+ });
+
+ // Gets the element specified by Limit
+ if (offset != 0 || count != vectorlen) {
+ sort_vec = std::vector(std::make_move_iterator(sort_vec.begin() +
offset),
+ std::make_move_iterator(sort_vec.begin() + offset
+ count));
+ }
+ }
+
+ // Perform storage
+ for (auto &elem : sort_vec) {
+ if (args.getpatterns.empty()) {
+ elems->emplace_back(elem.obj);
+ }
+ for (const std::string &pattern : args.getpatterns) {
+ std::optional<std::string> val = lookupKeyByPattern(pattern, elem.obj);
+ if (val.has_value()) {
+ elems->emplace_back(val.value());
+ } else {
+ elems->emplace_back(std::nullopt);
+ }
+ }
+ }
+
+ if (!args.storekey.empty()) {
+ std::vector<std::string> store_elems;
+ store_elems.reserve(elems->size());
+ for (const auto &e : *elems) {
+ store_elems.emplace_back(e.value_or(""));
+ }
+ redis::List list_db(storage_, namespace_);
+ s = list_db.Trim(args.storekey, -1, 0);
+ if (!s.ok()) return s;
+ uint64_t new_size = 0;
+ s = list_db.Push(args.storekey, std::vector<Slice>(store_elems.cbegin(),
store_elems.cend()), false, &new_size);
+ if (!s.ok()) return s;
+ }
+
+ return rocksdb::Status::OK();
+}
+
+bool RedisSortObject::SortCompare(const RedisSortObject &a, const
RedisSortObject &b, const SortArgument &args) {
+ if (!args.alpha) {
+ double score_a = std::get<double>(a.v);
+ double score_b = std::get<double>(b.v);
+ return !args.desc ? score_a < score_b : score_a > score_b;
+ } else {
+ if (!args.sortby.empty()) {
+ std::string cmp_a = std::get<std::string>(a.v);
+ std::string cmp_b = std::get<std::string>(b.v);
+ return !args.desc ? cmp_a < cmp_b : cmp_a > cmp_b;
+ } else {
+ return !args.desc ? a.obj < b.obj : a.obj > b.obj;
+ }
+ }
+}
+
} // namespace redis
diff --git a/src/storage/redis_db.h b/src/storage/redis_db.h
index 73a5a654..84579b10 100644
--- a/src/storage/redis_db.h
+++ b/src/storage/redis_db.h
@@ -21,15 +21,52 @@
#pragma once
#include <map>
+#include <optional>
#include <string>
#include <utility>
+#include <variant>
#include <vector>
#include "redis_metadata.h"
+#include "server/redis_reply.h"
#include "storage.h"
namespace redis {
+/// SORT_LENGTH_LIMIT limits the number of elements to be sorted
+/// to avoid using too much memory and causing system crashes.
+/// TODO: Expect to expand or eliminate SORT_LENGTH_LIMIT
+/// through better mechanisms such as memory restriction logic.
+constexpr uint64_t SORT_LENGTH_LIMIT = 512;
+
+struct SortArgument {
+ std::string sortby; // BY
+ bool dontsort = false; // DONT SORT
+ int offset = 0; // LIMIT OFFSET
+ int count = -1; // LIMIT COUNT
+ std::vector<std::string> getpatterns; // GET
+ bool desc = false; // ASC/DESC
+ bool alpha = false; // ALPHA
+ std::string storekey; // STORE
+};
+
+struct RedisSortObject {
+ std::string obj;
+ std::variant<double, std::string> v;
+
+ /// SortCompare is a helper function that enables `RedisSortObject` to be
sorted based on `SortArgument`.
+ ///
+ /// It can assist in implementing the third parameter `Compare comp`
required by `std::sort`
+ ///
+ /// \param args The basis used to compare two RedisSortObjects.
+ /// If `args.alpha` is false, `RedisSortObject.v` will be taken as double
for comparison
+ /// If `args.alpha` is true and `args.sortby` is not empty,
`RedisSortObject.v` will be taken as string for comparison
+ /// If `args.alpha` is true and `args.sortby` is empty, the comparison is by
`RedisSortObject.obj`.
+ ///
+ /// \return If `desc` is false, returns true when `a < b`, otherwise returns
true when `a > b`
+ static bool SortCompare(const RedisSortObject &a, const RedisSortObject &b,
const SortArgument &args);
+};
+
/// Database is a wrapper of underlying storage engine, it provides
/// some common operations for redis commands.
class Database {
@@ -107,6 +144,17 @@ class Database {
enum class CopyResult { KEY_NOT_EXIST, KEY_ALREADY_EXIST, DONE };
[[nodiscard]] rocksdb::Status Copy(const std::string &key, const std::string
&new_key, bool nx, bool delete_old,
CopyResult *res);
+ enum class SortResult { UNKNOWN_TYPE, DOUBLE_CONVERT_ERROR, LIMIT_EXCEEDED,
DONE };
+ /// Sort sorts keys of the specified type according to SortArgument
+ ///
+ /// \param type is the type of sort key, which must be LIST, SET or ZSET
+ /// \param key is to be sorted
+ /// \param args provide the parameters to sort by
+ /// \param elems contain the sorted results
+ /// \param res represents the sorted result type.
+ /// When status is not ok, `res` should not been checked, otherwise it
should be checked whether `res` is `DONE`
+ [[nodiscard]] rocksdb::Status Sort(RedisType type, const std::string &key,
const SortArgument &args,
+ std::vector<std::optional<std::string>>
*elems, SortResult *res);
protected:
engine::Storage *storage_;
@@ -119,6 +167,21 @@ class Database {
// Already internal keys
[[nodiscard]] rocksdb::Status existsInternal(const std::vector<std::string>
&keys, int *ret);
[[nodiscard]] rocksdb::Status typeInternal(const Slice &key, RedisType
*type);
+
+ /// lookupKeyByPattern is a helper function of `Sort` to support `GET` and
`BY` fields.
+ ///
+ /// \param pattern can be the value of a `BY` or `GET` field
+ /// \param subst is used to replace the "*" or "#" matched in the pattern
string.
+ /// \return Returns the value associated to the key with a name obtained
using the following rules:
+ /// 1) The first occurrence of '*' in 'pattern' is substituted with
'subst'.
+ /// 2) If 'pattern' matches the "->" string, everything on the left of
+ /// the arrow is treated as the name of a hash field, and the part on
the
+ /// left as the key name containing a hash. The value of the specified
+ /// field is returned.
+ /// 3) If 'pattern' equals "#", the function simply returns 'subst' itself
so
+ /// that the SORT command can be used like: SORT key GET # to retrieve
+ /// the Set/List elements directly.
+ std::optional<std::string> lookupKeyByPattern(const std::string &pattern,
const std::string &subst);
};
class LatestSnapShot {
public:
diff --git a/tests/gocase/unit/sort/sort_test.go
b/tests/gocase/unit/sort/sort_test.go
new file mode 100644
index 00000000..6715ed78
--- /dev/null
+++ b/tests/gocase/unit/sort/sort_test.go
@@ -0,0 +1,881 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package sort
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/redis/go-redis/v9"
+
+ "github.com/apache/kvrocks/tests/gocase/util"
+ "github.com/stretchr/testify/require"
+)
+
+func TestSortParser(t *testing.T) {
+ srv := util.StartServer(t, map[string]string{})
+ defer srv.Close()
+
+ ctx := context.Background()
+ rdb := srv.NewClient()
+ defer func() { require.NoError(t, rdb.Close()) }()
+
+ t.Run("SORT Parser", func(t *testing.T) {
+ rdb.RPush(ctx, "bad-case-key", 5, 4, 3, 2, 1)
+
+ _, err := rdb.Do(ctx, "Sort").Result()
+ require.EqualError(t, err, "ERR wrong number of arguments")
+
+ _, err = rdb.Do(ctx, "Sort", "bad-case-key", "BadArg").Result()
+ require.EqualError(t, err, "ERR syntax error")
+
+ _, err = rdb.Do(ctx, "Sort", "bad-case-key", "LIMIT").Result()
+ require.EqualError(t, err, "ERR no more item to parse")
+
+ _, err = rdb.Do(ctx, "Sort", "bad-case-key", "LIMIT",
1).Result()
+ require.EqualError(t, err, "ERR no more item to parse")
+
+ _, err = rdb.Do(ctx, "Sort", "bad-case-key", "LIMIT", 1,
"not-number").Result()
+ require.EqualError(t, err, "ERR not started as an integer")
+
+ _, err = rdb.Do(ctx, "Sort", "bad-case-key", "STORE").Result()
+ require.EqualError(t, err, "ERR no more item to parse")
+
+ rdb.MSet(ctx, "rank_1", 1, "rank_2", "rank_3", 3, "rank_4", 4,
"rank_5", 5)
+ _, err = rdb.Do(ctx, "Sort", "bad-case-key", "BY", "dontsort",
"BY", "rank_*").Result()
+ require.EqualError(t, err, "ERR don't use multiple BY
parameters")
+
+ _, err = rdb.Do(ctx, "Sort_RO", "bad-case-key", "STORE",
"store_ro_key").Result()
+ require.EqualError(t, err, "ERR SORT_RO is read-only and does
not support the STORE parameter")
+ })
+}
+
+func TestSortLengthLimit(t *testing.T) {
+ srv := util.StartServer(t, map[string]string{})
+ defer srv.Close()
+
+ ctx := context.Background()
+ rdb := srv.NewClient()
+ defer func() { require.NoError(t, rdb.Close()) }()
+
+ t.Run("SORT Length Limit", func(t *testing.T) {
+ for i := 0; i <= 512; i++ {
+ rdb.LPush(ctx, "many-list-elems-key", i)
+ }
+ _, err := rdb.Sort(ctx, "many-list-elems-key",
&redis.Sort{}).Result()
+ require.EqualError(t, err, "The number of elements to be sorted
exceeds SORT_LENGTH_LIMIT = 512")
+
+ for i := 0; i <= 512; i++ {
+ rdb.SAdd(ctx, "many-set-elems-key", i)
+ }
+ _, err = rdb.Sort(ctx, "many-set-elems-key",
&redis.Sort{}).Result()
+ require.EqualError(t, err, "The number of elements to be sorted
exceeds SORT_LENGTH_LIMIT = 512")
+
+ for i := 0; i <= 512; i++ {
+ rdb.ZAdd(ctx, "many-zset-elems-key", redis.Z{Score:
float64(i), Member: fmt.Sprintf("%d", i)})
+ }
+ _, err = rdb.Sort(ctx, "many-zset-elems-key",
&redis.Sort{}).Result()
+ require.EqualError(t, err, "The number of elements to be sorted
exceeds SORT_LENGTH_LIMIT = 512")
+ })
+}
+
+func TestListSort(t *testing.T) {
+ srv := util.StartServer(t, map[string]string{})
+ defer srv.Close()
+
+ ctx := context.Background()
+ rdb := srv.NewClient()
+ defer func() { require.NoError(t, rdb.Close()) }()
+
+ t.Run("SORT Basic", func(t *testing.T) {
+ rdb.LPush(ctx, "today_cost", 30, 1.5, 10, 8)
+
+ sortResult, err := rdb.Sort(ctx, "today_cost",
&redis.Sort{}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1.5", "8", "10", "30"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "today_cost",
&redis.Sort{Order: "ASC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1.5", "8", "10", "30"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "today_cost",
&redis.Sort{Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"30", "10", "8", "1.5"}, sortResult)
+
+ sortResult, err = rdb.SortRO(ctx, "today_cost",
&redis.Sort{Order: "ASC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1.5", "8", "10", "30"}, sortResult)
+
+ sortResult, err = rdb.SortRO(ctx, "today_cost",
&redis.Sort{Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"30", "10", "8", "1.5"}, sortResult)
+ })
+
+ t.Run("SORT ALPHA", func(t *testing.T) {
+ rdb.LPush(ctx, "website", "www.reddit.com", "www.slashdot.com",
"www.infoq.com")
+
+ sortResult, err := rdb.Sort(ctx, "website", &redis.Sort{Alpha:
true}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"www.infoq.com", "www.reddit.com",
"www.slashdot.com"}, sortResult)
+
+ _, err = rdb.Sort(ctx, "website", &redis.Sort{Alpha:
false}).Result()
+ require.EqualError(t, err, "One or more scores can't be
converted into double")
+ })
+
+ t.Run("SORT LIMIT", func(t *testing.T) {
+ rdb.RPush(ctx, "rank", 1, 3, 5, 7, 9, 2, 4, 6, 8, 10)
+
+ sortResult, err := rdb.Sort(ctx, "rank", &redis.Sort{Offset: 0,
Count: 5}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4", "5"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 0,
Count: 5, Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"10", "9", "8", "7", "6"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -1,
Count: 0}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 10,
Count: 0}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 10,
Count: 1}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 11,
Count: 1}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -1,
Count: 1}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -2,
Count: 2}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -1,
Count: 11}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7",
"8", "9", "10"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -2,
Count: -1}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7",
"8", "9", "10"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -2,
Count: -2}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7",
"8", "9", "10"}, sortResult)
+ })
+
+ t.Run("SORT BY + GET", func(t *testing.T) {
+ rdb.LPush(ctx, "uid", 1, 2, 3, 4)
+ rdb.MSet(ctx, "user_name_1", "admin", "user_name_2", "jack",
"user_name_3", "peter", "user_name_4", "mary")
+ rdb.MSet(ctx, "user_level_1", 9999, "user_level_2", 10,
"user_level_3", 25, "user_level_4", 70)
+
+ sortResult, err := rdb.Sort(ctx, "uid", &redis.Sort{By:
"user_level_*"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"2", "3", "4", "1"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{Get:
[]string{"user_name_*"}}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"admin", "jack", "peter", "mary"},
sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"user_level_*", Get: []string{"user_name_*"}}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"jack", "peter", "mary", "admin"},
sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{Get:
[]string{"user_level_*", "user_name_*"}}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"9999", "admin", "10", "jack", "25",
"peter", "70", "mary"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{Get:
[]string{"#", "user_level_*", "user_name_*"}}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "9999", "admin", "2", "10",
"jack", "3", "25", "peter", "4", "70", "mary"}, sortResult)
+
+ // not sorted
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"4", "3", "2", "1"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 0, Count: 1}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"4"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 1, Count: 2}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"3", "2"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 4, Count: 1}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 4, Count: 0}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 1, Count: -1}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"3", "2", "1"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 0, Count: 1, Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 1, Count: 2, Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"2", "3"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 4, Count: 1, Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 4, Count: 0, Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 1, Count: -1, Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"2", "3", "4"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Get: []string{"#", "user_level_*", "user_name_*"}}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"4", "70", "mary", "3", "25",
"peter", "2", "10", "jack", "1", "9999", "admin"}, sortResult)
+
+ // pattern with hash tag
+ rdb.HMSet(ctx, "user_info_1", "name", "admin", "level", 9999)
+ rdb.HMSet(ctx, "user_info_2", "name", "jack", "level", 10)
+ rdb.HMSet(ctx, "user_info_3", "name", "peter", "level", 25)
+ rdb.HMSet(ctx, "user_info_4", "name", "mary", "level", 70)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"user_info_*->level"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"2", "3", "4", "1"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"user_info_*->level", Get: []string{"user_info_*->name"}}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"jack", "peter", "mary", "admin"},
sortResult)
+
+ // get/by empty and nil
+ rdb.LPush(ctx, "uid_empty_nil", 4, 5, 6)
+ rdb.MSet(ctx, "user_name_5", "tom", "user_level_5", -1)
+
+ getResult, err := rdb.Do(ctx, "Sort", "uid_empty_nil", "Get",
"user_name_*").Slice()
+ require.NoError(t, err)
+ require.Equal(t, []interface{}{"mary", "tom", nil}, getResult)
+ byResult, err := rdb.Do(ctx, "Sort", "uid_empty_nil", "By",
"user_level_*").Slice()
+ require.NoError(t, err)
+ require.Equal(t, []interface{}{"5", "6", "4"}, byResult)
+
+ rdb.MSet(ctx, "user_name_6", "", "user_level_6", "")
+
+ getResult, err = rdb.Do(ctx, "Sort", "uid_empty_nil", "Get",
"user_name_*").Slice()
+ require.NoError(t, err)
+ require.Equal(t, []interface{}{"mary", "tom", ""}, getResult)
+
+ byResult, err = rdb.Do(ctx, "Sort", "uid_empty_nil", "By",
"user_level_*").Slice()
+ require.NoError(t, err)
+ require.Equal(t, []interface{}{"5", "6", "4"}, byResult)
+ })
+
+ t.Run("SORT STORE", func(t *testing.T) {
+ rdb.RPush(ctx, "numbers", 1, 3, 5, 7, 9, 2, 4, 6, 8, 10)
+
+ storedLen, err := rdb.Do(ctx, "Sort", "numbers", "STORE",
"sorted-numbers").Result()
+ require.NoError(t, err)
+ require.Equal(t, int64(10), storedLen)
+
+ sortResult, err := rdb.LRange(ctx, "sorted-numbers", 0,
-1).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7",
"8", "9", "10"}, sortResult)
+
+ rdb.LPush(ctx, "no-force-alpha-sort-key", 123, 3, 21)
+ storedLen, err = rdb.Do(ctx, "Sort", "no-force-alpha-sort-key",
"BY", "not-exists-key", "STORE", "no-alpha-sorted").Result()
+ require.NoError(t, err)
+ require.Equal(t, int64(3), storedLen)
+
+ sortResult, err = rdb.LRange(ctx, "no-alpha-sorted", 0,
-1).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"21", "3", "123"}, sortResult)
+
+ // get empty and nil
+ rdb.LPush(ctx, "uid_get_empty_nil", 4, 5, 6)
+ rdb.MSet(ctx, "user_name_4", "mary", "user_level_4", 70,
"user_name_5", "tom", "user_level_5", -1)
+
+ storedLen, err = rdb.Do(ctx, "Sort", "uid_get_empty_nil",
"Get", "user_name_*", "Store", "get_empty_nil_store").Result()
+ require.NoError(t, err)
+ require.Equal(t, int64(3), storedLen)
+
+ sortResult, err = rdb.LRange(ctx, "get_empty_nil_store", 0,
-1).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"mary", "tom", ""}, sortResult)
+
+ rdb.MSet(ctx, "user_name_6", "", "user_level_6", "")
+ storedLen, err = rdb.Do(ctx, "Sort", "uid_get_empty_nil",
"Get", "user_name_*", "Store", "get_empty_nil_store").Result()
+ require.NoError(t, err)
+ require.Equal(t, int64(3), storedLen)
+
+ sortResult, err = rdb.LRange(ctx, "get_empty_nil_store", 0,
-1).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"mary", "tom", ""}, sortResult)
+ })
+}
+
+func TestSetSort(t *testing.T) {
+ srv := util.StartServer(t, map[string]string{})
+ defer srv.Close()
+
+ ctx := context.Background()
+ rdb := srv.NewClient()
+ defer func() { require.NoError(t, rdb.Close()) }()
+
+ t.Run("SORT Basic", func(t *testing.T) {
+ rdb.SAdd(ctx, "today_cost", 30, 1.5, 10, 8)
+
+ sortResult, err := rdb.Sort(ctx, "today_cost",
&redis.Sort{}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1.5", "8", "10", "30"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "today_cost",
&redis.Sort{Order: "ASC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1.5", "8", "10", "30"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "today_cost",
&redis.Sort{Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"30", "10", "8", "1.5"}, sortResult)
+
+ sortResult, err = rdb.SortRO(ctx, "today_cost",
&redis.Sort{Order: "ASC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1.5", "8", "10", "30"}, sortResult)
+
+ sortResult, err = rdb.SortRO(ctx, "today_cost",
&redis.Sort{Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"30", "10", "8", "1.5"}, sortResult)
+ })
+
+ t.Run("SORT ALPHA", func(t *testing.T) {
+ rdb.SAdd(ctx, "website", "www.reddit.com", "www.slashdot.com",
"www.infoq.com")
+
+ sortResult, err := rdb.Sort(ctx, "website", &redis.Sort{Alpha:
true}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"www.infoq.com", "www.reddit.com",
"www.slashdot.com"}, sortResult)
+
+ _, err = rdb.Sort(ctx, "website", &redis.Sort{Alpha:
false}).Result()
+ require.EqualError(t, err, "One or more scores can't be
converted into double")
+ })
+
+ t.Run("SORT LIMIT", func(t *testing.T) {
+ rdb.SAdd(ctx, "rank", 1, 3, 5, 7, 9, 2, 4, 6, 8, 10)
+
+ sortResult, err := rdb.Sort(ctx, "rank", &redis.Sort{Offset: 0,
Count: 5}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4", "5"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 0,
Count: 5, Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"10", "9", "8", "7", "6"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -1,
Count: 0}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 10,
Count: 0}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 10,
Count: 1}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 11,
Count: 1}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -1,
Count: 1}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -2,
Count: 2}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -1,
Count: 11}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7",
"8", "9", "10"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -2,
Count: -1}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7",
"8", "9", "10"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -2,
Count: -2}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7",
"8", "9", "10"}, sortResult)
+ })
+
+ t.Run("SORT BY + GET", func(t *testing.T) {
+ rdb.SAdd(ctx, "uid", 4, 3, 2, 1)
+ rdb.MSet(ctx, "user_name_1", "admin", "user_name_2", "jack",
"user_name_3", "peter", "user_name_4", "mary")
+ rdb.MSet(ctx, "user_level_1", 9999, "user_level_2", 10,
"user_level_3", 25, "user_level_4", 70)
+
+ sortResult, err := rdb.Sort(ctx, "uid", &redis.Sort{By:
"user_level_*"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"2", "3", "4", "1"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{Get:
[]string{"user_name_*"}}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"admin", "jack", "peter", "mary"},
sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"user_level_*", Get: []string{"user_name_*"}}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"jack", "peter", "mary", "admin"},
sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{Get:
[]string{"user_level_*", "user_name_*"}}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"9999", "admin", "10", "jack", "25",
"peter", "70", "mary"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{Get:
[]string{"#", "user_level_*", "user_name_*"}}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "9999", "admin", "2", "10",
"jack", "3", "25", "peter", "4", "70", "mary"}, sortResult)
+
+ // not sorted
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 0, Count: 1}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 1, Count: 2}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"2", "3"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 4, Count: 1}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 4, Count: 0}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 1, Count: -1}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"2", "3", "4"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 0, Count: 1, Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 1, Count: 2, Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"2", "3"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 4, Count: 1, Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 4, Count: 0, Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 1, Count: -1, Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"2", "3", "4"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Get: []string{"#", "user_level_*", "user_name_*"}}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "9999", "admin", "2", "10",
"jack", "3", "25", "peter", "4", "70", "mary"}, sortResult)
+
+ // pattern with hash tag
+ rdb.HMSet(ctx, "user_info_1", "name", "admin", "level", 9999)
+ rdb.HMSet(ctx, "user_info_2", "name", "jack", "level", 10)
+ rdb.HMSet(ctx, "user_info_3", "name", "peter", "level", 25)
+ rdb.HMSet(ctx, "user_info_4", "name", "mary", "level", 70)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"user_info_*->level"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"2", "3", "4", "1"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"user_info_*->level", Get: []string{"user_info_*->name"}}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"jack", "peter", "mary", "admin"},
sortResult)
+
+ // get/by empty and nil
+ rdb.SAdd(ctx, "uid_empty_nil", 4, 5, 6)
+ rdb.MSet(ctx, "user_name_5", "tom", "user_level_5", -1)
+
+ getResult, err := rdb.Do(ctx, "Sort", "uid_empty_nil", "Get",
"user_name_*").Slice()
+ require.NoError(t, err)
+ require.Equal(t, []interface{}{"mary", "tom", nil}, getResult)
+ byResult, err := rdb.Do(ctx, "Sort", "uid_empty_nil", "By",
"user_level_*").Slice()
+ require.NoError(t, err)
+ require.Equal(t, []interface{}{"5", "6", "4"}, byResult)
+
+ rdb.MSet(ctx, "user_name_6", "", "user_level_6", "")
+
+ getResult, err = rdb.Do(ctx, "Sort", "uid_empty_nil", "Get",
"user_name_*").Slice()
+ require.NoError(t, err)
+ require.Equal(t, []interface{}{"mary", "tom", ""}, getResult)
+
+ byResult, err = rdb.Do(ctx, "Sort", "uid_empty_nil", "By",
"user_level_*").Slice()
+ require.NoError(t, err)
+ require.Equal(t, []interface{}{"5", "6", "4"}, byResult)
+
+ })
+
+ t.Run("SORT STORE", func(t *testing.T) {
+ rdb.SAdd(ctx, "numbers", 1, 3, 5, 7, 9, 2, 4, 6, 8, 10)
+
+ storedLen, err := rdb.Do(ctx, "Sort", "numbers", "STORE",
"sorted-numbers").Result()
+ require.NoError(t, err)
+ require.Equal(t, int64(10), storedLen)
+
+ sortResult, err := rdb.LRange(ctx, "sorted-numbers", 0,
-1).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7",
"8", "9", "10"}, sortResult)
+
+ rdb.SAdd(ctx, "force-alpha-sort-key", 123, 3, 21)
+ storedLen, err = rdb.Do(ctx, "Sort", "force-alpha-sort-key",
"BY", "not-exists-key", "STORE", "alpha-sorted").Result()
+ require.NoError(t, err)
+ require.Equal(t, int64(3), storedLen)
+
+ sortResult, err = rdb.LRange(ctx, "alpha-sorted", 0,
-1).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"123", "21", "3"}, sortResult)
+
+ // get empty and nil
+ rdb.SAdd(ctx, "uid_get_empty_nil", 4, 5, 6)
+ rdb.MSet(ctx, "user_name_4", "mary", "user_level_4", 70,
"user_name_5", "tom", "user_level_5", -1)
+
+ storedLen, err = rdb.Do(ctx, "Sort", "uid_get_empty_nil",
"Get", "user_name_*", "Store", "get_empty_nil_store").Result()
+ require.NoError(t, err)
+ require.Equal(t, int64(3), storedLen)
+
+ sortResult, err = rdb.LRange(ctx, "get_empty_nil_store", 0,
-1).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"mary", "tom", ""}, sortResult)
+
+ rdb.MSet(ctx, "user_name_6", "", "user_level_6", "")
+ storedLen, err = rdb.Do(ctx, "Sort", "uid_get_empty_nil",
"Get", "user_name_*", "Store", "get_empty_nil_store").Result()
+ require.NoError(t, err)
+ require.Equal(t, int64(3), storedLen)
+
+ sortResult, err = rdb.LRange(ctx, "get_empty_nil_store", 0,
-1).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"mary", "tom", ""}, sortResult)
+ })
+}
+
+func TestZSetSort(t *testing.T) {
+ srv := util.StartServer(t, map[string]string{})
+ defer srv.Close()
+
+ ctx := context.Background()
+ rdb := srv.NewClient()
+ defer func() { require.NoError(t, rdb.Close()) }()
+
+ t.Run("SORT Basic", func(t *testing.T) {
+ rdb.ZAdd(ctx, "today_cost", redis.Z{Score: 30, Member: "1"},
redis.Z{Score: 1.5, Member: "2"}, redis.Z{Score: 10, Member: "3"},
redis.Z{Score: 8, Member: "4"})
+
+ sortResult, err := rdb.Sort(ctx, "today_cost",
&redis.Sort{}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "today_cost",
&redis.Sort{Order: "ASC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "today_cost",
&redis.Sort{Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"4", "3", "2", "1"}, sortResult)
+
+ sortResult, err = rdb.SortRO(ctx, "today_cost",
&redis.Sort{Order: "ASC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4"}, sortResult)
+
+ sortResult, err = rdb.SortRO(ctx, "today_cost",
&redis.Sort{Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"4", "3", "2", "1"}, sortResult)
+ })
+
+ t.Run("SORT ALPHA", func(t *testing.T) {
+ rdb.ZAdd(ctx, "website", redis.Z{Score: 1, Member:
"www.reddit.com"}, redis.Z{Score: 2, Member: "www.slashdot.com"},
redis.Z{Score: 3, Member: "www.infoq.com"})
+
+ sortResult, err := rdb.Sort(ctx, "website", &redis.Sort{Alpha:
true}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"www.infoq.com", "www.reddit.com",
"www.slashdot.com"}, sortResult)
+
+ _, err = rdb.Sort(ctx, "website", &redis.Sort{Alpha:
false}).Result()
+ require.EqualError(t, err, "One or more scores can't be
converted into double")
+ })
+
+ t.Run("SORT LIMIT", func(t *testing.T) {
+ rdb.ZAdd(ctx, "rank",
+ redis.Z{Score: 1, Member: "1"},
+ redis.Z{Score: 2, Member: "3"},
+ redis.Z{Score: 3, Member: "5"},
+ redis.Z{Score: 4, Member: "7"},
+ redis.Z{Score: 5, Member: "9"},
+ redis.Z{Score: 6, Member: "2"},
+ redis.Z{Score: 7, Member: "4"},
+ redis.Z{Score: 8, Member: "6"},
+ redis.Z{Score: 9, Member: "8"},
+ redis.Z{Score: 10, Member: "10"},
+ )
+
+ sortResult, err := rdb.Sort(ctx, "rank", &redis.Sort{Offset: 0,
Count: 5}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4", "5"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 0,
Count: 5, Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"10", "9", "8", "7", "6"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -1,
Count: 0}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 10,
Count: 0}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 10,
Count: 1}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 11,
Count: 1}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -1,
Count: 1}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -2,
Count: 2}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -1,
Count: 11}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7",
"8", "9", "10"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -2,
Count: -1}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7",
"8", "9", "10"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -2,
Count: -2}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7",
"8", "9", "10"}, sortResult)
+ })
+
+ t.Run("SORT BY + GET", func(t *testing.T) {
+ rdb.ZAdd(ctx, "uid",
+ redis.Z{Score: 1, Member: "4"},
+ redis.Z{Score: 2, Member: "3"},
+ redis.Z{Score: 3, Member: "2"},
+ redis.Z{Score: 4, Member: "1"})
+
+ rdb.MSet(ctx, "user_name_1", "admin", "user_name_2", "jack",
"user_name_3", "peter", "user_name_4", "mary")
+ rdb.MSet(ctx, "user_level_1", 9999, "user_level_2", 10,
"user_level_3", 25, "user_level_4", 70)
+
+ sortResult, err := rdb.Sort(ctx, "uid", &redis.Sort{By:
"user_level_*"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"2", "3", "4", "1"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{Get:
[]string{"user_name_*"}}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"admin", "jack", "peter", "mary"},
sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"user_level_*", Get: []string{"user_name_*"}}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"jack", "peter", "mary", "admin"},
sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{Get:
[]string{"user_level_*", "user_name_*"}}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"9999", "admin", "10", "jack", "25",
"peter", "70", "mary"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{Get:
[]string{"#", "user_level_*", "user_name_*"}}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "9999", "admin", "2", "10",
"jack", "3", "25", "peter", "4", "70", "mary"}, sortResult)
+
+ // not sorted
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"4", "3", "2", "1"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 0, Count: 1}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"4"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 1, Count: 2}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"3", "2"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 4, Count: 1}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 4, Count: 0}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 1, Count: -1}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"3", "2", "1"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 0, Count: 1, Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 1, Count: 2, Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"2", "3"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 4, Count: 1, Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 4, Count: 0, Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Offset: 1, Count: -1, Order: "DESC"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"2", "3", "4"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"not-exists-key", Get: []string{"#", "user_level_*", "user_name_*"}}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"4", "70", "mary", "3", "25",
"peter", "2", "10", "jack", "1", "9999", "admin"}, sortResult)
+
+ // pattern with hash tag
+ rdb.HMSet(ctx, "user_info_1", "name", "admin", "level", 9999)
+ rdb.HMSet(ctx, "user_info_2", "name", "jack", "level", 10)
+ rdb.HMSet(ctx, "user_info_3", "name", "peter", "level", 25)
+ rdb.HMSet(ctx, "user_info_4", "name", "mary", "level", 70)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"user_info_*->level"}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"2", "3", "4", "1"}, sortResult)
+
+ sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By:
"user_info_*->level", Get: []string{"user_info_*->name"}}).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"jack", "peter", "mary", "admin"},
sortResult)
+
+ // get/by empty and nil
+ rdb.ZAdd(ctx, "uid_empty_nil",
+ redis.Z{Score: 4, Member: "6"},
+ redis.Z{Score: 5, Member: "5"},
+ redis.Z{Score: 6, Member: "4"})
+ rdb.MSet(ctx, "user_name_5", "tom", "user_level_5", -1)
+
+ getResult, err := rdb.Do(ctx, "Sort", "uid_empty_nil", "Get",
"user_name_*").Slice()
+ require.NoError(t, err)
+ require.Equal(t, []interface{}{"mary", "tom", nil}, getResult)
+ byResult, err := rdb.Do(ctx, "Sort", "uid_empty_nil", "By",
"user_level_*").Slice()
+ require.NoError(t, err)
+ require.Equal(t, []interface{}{"5", "6", "4"}, byResult)
+
+ rdb.MSet(ctx, "user_name_6", "", "user_level_6", "")
+
+ getResult, err = rdb.Do(ctx, "Sort", "uid_empty_nil", "Get",
"user_name_*").Slice()
+ require.NoError(t, err)
+ require.Equal(t, []interface{}{"mary", "tom", ""}, getResult)
+
+ byResult, err = rdb.Do(ctx, "Sort", "uid_empty_nil", "By",
"user_level_*").Slice()
+ require.NoError(t, err)
+ require.Equal(t, []interface{}{"5", "6", "4"}, byResult)
+ })
+
+ t.Run("SORT STORE", func(t *testing.T) {
+ rdb.ZAdd(ctx, "numbers",
+ redis.Z{Score: 1, Member: "1"},
+ redis.Z{Score: 2, Member: "3"},
+ redis.Z{Score: 3, Member: "5"},
+ redis.Z{Score: 4, Member: "7"},
+ redis.Z{Score: 5, Member: "9"},
+ redis.Z{Score: 6, Member: "2"},
+ redis.Z{Score: 7, Member: "4"},
+ redis.Z{Score: 8, Member: "6"},
+ redis.Z{Score: 9, Member: "8"},
+ redis.Z{Score: 10, Member: "10"},
+ )
+
+ storedLen, err := rdb.Do(ctx, "Sort", "numbers", "STORE",
"sorted-numbers").Result()
+ require.NoError(t, err)
+ require.Equal(t, int64(10), storedLen)
+
+ sortResult, err := rdb.LRange(ctx, "sorted-numbers", 0,
-1).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7",
"8", "9", "10"}, sortResult)
+
+ rdb.ZAdd(ctx, "no-force-alpha-sort-key",
+ redis.Z{Score: 1, Member: "123"},
+ redis.Z{Score: 2, Member: "3"},
+ redis.Z{Score: 3, Member: "21"},
+ )
+
+ storedLen, err = rdb.Do(ctx, "Sort", "no-force-alpha-sort-key",
"BY", "not-exists-key", "STORE", "no-alpha-sorted").Result()
+ require.NoError(t, err)
+ require.Equal(t, int64(3), storedLen)
+
+ sortResult, err = rdb.LRange(ctx, "no-alpha-sorted", 0,
-1).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"123", "3", "21"}, sortResult)
+
+ // get empty and nil
+ rdb.ZAdd(ctx, "uid_get_empty_nil",
+ redis.Z{Score: 4, Member: "6"},
+ redis.Z{Score: 5, Member: "5"},
+ redis.Z{Score: 6, Member: "4"})
+ rdb.MSet(ctx, "user_name_4", "mary", "user_level_4", 70,
"user_name_5", "tom", "user_level_5", -1)
+
+ storedLen, err = rdb.Do(ctx, "Sort", "uid_get_empty_nil",
"Get", "user_name_*", "Store", "get_empty_nil_store").Result()
+ require.NoError(t, err)
+ require.Equal(t, int64(3), storedLen)
+
+ sortResult, err = rdb.LRange(ctx, "get_empty_nil_store", 0,
-1).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"mary", "tom", ""}, sortResult)
+
+ rdb.MSet(ctx, "user_name_6", "", "user_level_6", "")
+ storedLen, err = rdb.Do(ctx, "Sort", "uid_get_empty_nil",
"Get", "user_name_*", "Store", "get_empty_nil_store").Result()
+ require.NoError(t, err)
+ require.Equal(t, int64(3), storedLen)
+
+ sortResult, err = rdb.LRange(ctx, "get_empty_nil_store", 0,
-1).Result()
+ require.NoError(t, err)
+ require.Equal(t, []string{"mary", "tom", ""}, sortResult)
+ })
+}