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)
+       })
+}

Reply via email to