diff --git a/src/commands/redis_cmd.cc b/src/commands/redis_cmd.cc index a66b13c93df..dd1ee74bf4d 100644 --- a/src/commands/redis_cmd.cc +++ b/src/commands/redis_cmd.cc @@ -1614,34 +1614,39 @@ class CommandHGetAll : public Commander { } }; -class CommandHRange : public Commander { +class CommandHRangeByLex : public Commander { public: Status Parse(const std::vector &args) override { - if (args.size() != 6 && args.size() != 4) { - return {Status::RedisParseErr, errWrongNumOfArguments}; + CommandParser parser(args, 4); + while (parser.Good()) { + if (parser.EatEqICase("REV")) { + spec_.reversed = true; + } else if (parser.EatEqICase("LIMIT")) { + spec_.offset = GET_OR_RET(parser.TakeInt()); + spec_.count = GET_OR_RET(parser.TakeInt()); + } else { + return parser.InvalidSyntax(); + } } - - if (args.size() == 6 && Util::ToLower(args[4]) != "limit") { - return {Status::RedisInvalidCmd, errInvalidSyntax}; + Status s; + if (spec_.reversed) { + s = ParseRangeLexSpec(args[3], args[2], &spec_); + } else { + s = ParseRangeLexSpec(args[2], args[3], &spec_); } - - if (args.size() == 6) { - auto parse_result = ParseInt(args_[5], 10); - if (!parse_result) return {Status::RedisParseErr, errValueNotInteger}; - - limit_ = *parse_result; + if (!s.IsOK()) { + return Status(Status::RedisParseErr, s.Msg()); } - return Commander::Parse(args); + return Status::OK(); } Status Execute(Server *svr, Connection *conn, std::string *output) override { Redis::Hash hash_db(svr->storage_, conn->GetNamespace()); std::vector field_values; - auto s = hash_db.Range(args_[1], args_[2], args_[3], limit_, &field_values); + rocksdb::Status s = hash_db.RangeByLex(args_[1], spec_, &field_values); if (!s.ok()) { return {Status::RedisExecErr, s.ToString()}; } - std::vector kv_pairs; for (const auto &p : field_values) { kv_pairs.emplace_back(p.field); @@ -1653,7 +1658,7 @@ class CommandHRange : public Commander { } private: - int64_t limit_ = LONG_MAX; + CommonRangeLexSpec spec_; }; class CommandPush : public Commander { @@ -2674,7 +2679,7 @@ class CommandZIncrBy : public Commander { class CommandZLexCount : public Commander { public: Status Parse(const std::vector &args) override { - Status s = Redis::ZSet::ParseRangeLexSpec(args[2], args[3], &spec_); + Status s = ParseRangeLexSpec(args[2], args[3], &spec_); if (!s.IsOK()) { return {Status::RedisParseErr, s.Msg()}; } @@ -2695,7 +2700,7 @@ class CommandZLexCount : public Commander { } private: - ZRangeLexSpec spec_; + CommonRangeLexSpec spec_; }; class CommandZPop : public Commander { @@ -2808,9 +2813,9 @@ class CommandZRangeByLex : public Commander { Status Parse(const std::vector &args) override { Status s; if (spec_.reversed) { - s = Redis::ZSet::ParseRangeLexSpec(args[3], args[2], &spec_); + s = ParseRangeLexSpec(args[3], args[2], &spec_); } else { - s = Redis::ZSet::ParseRangeLexSpec(args[2], args[3], &spec_); + s = ParseRangeLexSpec(args[2], args[3], &spec_); } if (!s.IsOK()) { @@ -2844,7 +2849,7 @@ class CommandZRangeByLex : public Commander { } private: - ZRangeLexSpec spec_; + CommonRangeLexSpec spec_; }; class CommandZRangeByScore : public Commander { @@ -3031,7 +3036,7 @@ class CommandZRemRangeByScore : public Commander { class CommandZRemRangeByLex : public Commander { public: Status Parse(const std::vector &args) override { - Status s = Redis::ZSet::ParseRangeLexSpec(args[2], args[3], &spec_); + Status s = ParseRangeLexSpec(args[2], args[3], &spec_); if (!s.IsOK()) { return {Status::RedisParseErr, s.Msg()}; } @@ -3051,7 +3056,7 @@ class CommandZRemRangeByLex : public Commander { } private: - ZRangeLexSpec spec_; + CommonRangeLexSpec spec_; }; class CommandZScore : public Commander { @@ -6420,7 +6425,7 @@ REDIS_REGISTER_COMMANDS( MakeCmdAttr("hvals", 2, "read-only", 1, 1, 1), MakeCmdAttr("hgetall", 2, "read-only", 1, 1, 1), MakeCmdAttr("hscan", -3, "read-only", 1, 1, 1), - MakeCmdAttr("hrange", -4, "read-only", 1, 1, 1), + MakeCmdAttr("hrangebylex", -4, "read-only", 1, 1, 1), MakeCmdAttr("lpush", -3, "write", 1, 1, 1), MakeCmdAttr("rpush", -3, "write", 1, 1, 1), MakeCmdAttr("lpushx", -3, "write", 1, 1, 1), diff --git a/src/common/range_spec.cc b/src/common/range_spec.cc new file mode 100644 index 00000000000..82b695c6bdb --- /dev/null +++ b/src/common/range_spec.cc @@ -0,0 +1,53 @@ +/* + * 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. + * + */ +#include "range_spec.h" + +Status ParseRangeLexSpec(const std::string &min, const std::string &max, CommonRangeLexSpec *spec) { + if (min == "+" || max == "-") { + return Status(Status::NotOK, "min > max"); + } + + if (min == "-") { + spec->min = ""; + } else { + if (min[0] == '(') { + spec->minex = true; + } else if (min[0] == '[') { + spec->minex = false; + } else { + return Status(Status::NotOK, "the min is illegal"); + } + spec->min = min.substr(1); + } + + if (max == "+") { + spec->max_infinite = true; + } else { + if (max[0] == '(') { + spec->maxex = true; + } else if (max[0] == '[') { + spec->maxex = false; + } else { + return Status(Status::NotOK, "the max is illegal"); + } + spec->max = max.substr(1); + } + return Status::OK(); +} diff --git a/src/common/range_spec.h b/src/common/range_spec.h new file mode 100644 index 00000000000..cc1a2868c96 --- /dev/null +++ b/src/common/range_spec.h @@ -0,0 +1,36 @@ +/* + * 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. + * + */ + +#pragma once + +#include + +#include "status.h" +struct CommonRangeLexSpec { + std::string min, max; + bool minex, maxex; /* are min or max exclusive */ + bool max_infinite; /* are max infinite */ + int64_t offset, count; + bool removed, reversed; + CommonRangeLexSpec() + : minex(false), maxex(false), max_infinite(false), offset(-1), count(-1), removed(false), reversed(false) {} +}; + +Status ParseRangeLexSpec(const std::string &min, const std::string &max, CommonRangeLexSpec *spec); diff --git a/src/types/redis_hash.cc b/src/types/redis_hash.cc index d7001b67cb5..b2bf589be6b 100644 --- a/src/types/redis_hash.cc +++ b/src/types/redis_hash.cc @@ -276,10 +276,10 @@ rocksdb::Status Hash::MSet(const Slice &user_key, const std::vector return storage_->Write(storage_->DefaultWriteOptions(), &batch); } -rocksdb::Status Hash::Range(const Slice &user_key, const Slice &start, const Slice &stop, int64_t limit, - std::vector *field_values) { +rocksdb::Status Hash::RangeByLex(const Slice &user_key, const CommonRangeLexSpec &spec, + std::vector *field_values) { field_values->clear(); - if (start.compare(stop) >= 0 || limit <= 0) { + if (spec.count == 0) { return rocksdb::Status::OK(); } std::string ns_key; @@ -287,26 +287,54 @@ rocksdb::Status Hash::Range(const Slice &user_key, const Slice &start, const Sli HashMetadata metadata(false); rocksdb::Status s = GetMetadata(ns_key, &metadata); if (!s.ok()) return s.IsNotFound() ? rocksdb::Status::OK() : s; - limit = std::min(static_cast(metadata.size), limit); - std::string start_key, stop_key; - InternalKey(ns_key, start, metadata.version, storage_->IsSlotIdEncoded()).Encode(&start_key); - InternalKey(ns_key, stop, metadata.version, storage_->IsSlotIdEncoded()).Encode(&stop_key); + + std::string start_member = spec.reversed ? spec.max : spec.min; + std::string start_key, prefix_key, next_version_prefix_key; + InternalKey(ns_key, start_member, metadata.version, storage_->IsSlotIdEncoded()).Encode(&start_key); + InternalKey(ns_key, "", metadata.version, storage_->IsSlotIdEncoded()).Encode(&prefix_key); + InternalKey(ns_key, "", metadata.version + 1, storage_->IsSlotIdEncoded()).Encode(&next_version_prefix_key); rocksdb::ReadOptions read_options; LatestSnapShot ss(db_); read_options.snapshot = ss.GetSnapShot(); - rocksdb::Slice upper_bound(stop_key); + rocksdb::Slice upper_bound(next_version_prefix_key); read_options.iterate_upper_bound = &upper_bound; + rocksdb::Slice lower_bound(prefix_key); + read_options.iterate_lower_bound = &lower_bound; read_options.fill_cache = false; auto iter = DBUtil::UniqueIterator(db_, read_options); - iter->Seek(start_key); - for (int64_t i = 0; iter->Valid() && i <= limit - 1; ++i) { + if (!spec.reversed) { + iter->Seek(start_key); + } else { + if (spec.max_infinite) { + iter->SeekToLast(); + } else { + iter->SeekForPrev(start_key); + } + } + int64_t pos = 0; + for (; iter->Valid() && iter->key().starts_with(prefix_key); (!spec.reversed ? iter->Next() : iter->Prev())) { FieldValue tmp_field_value; InternalKey ikey(iter->key(), storage_->IsSlotIdEncoded()); + if (spec.reversed) { + if (ikey.GetSubKey().ToString() < spec.min || (spec.minex && ikey.GetSubKey().ToString() == spec.min)) { + break; + } + if ((spec.maxex && ikey.GetSubKey().ToString() == spec.max) || + (!spec.max_infinite && ikey.GetSubKey().ToString() > spec.max)) { + continue; + } + } else { + if (spec.minex && ikey.GetSubKey().ToString() == spec.min) continue; // the min member was exclusive + if ((spec.maxex && ikey.GetSubKey().ToString() == spec.max) || + (!spec.max_infinite && ikey.GetSubKey().ToString() > spec.max)) + break; + } + if (spec.offset >= 0 && pos++ < spec.offset) continue; tmp_field_value.field = ikey.GetSubKey().ToString(); tmp_field_value.value = iter->value().ToString(); field_values->emplace_back(tmp_field_value); - iter->Next(); + if (spec.count > 0 && field_values && field_values->size() >= static_cast(spec.count)) break; } return rocksdb::Status::OK(); } diff --git a/src/types/redis_hash.h b/src/types/redis_hash.h index 0ed4c9726e9..a9add33be4d 100644 --- a/src/types/redis_hash.h +++ b/src/types/redis_hash.h @@ -25,6 +25,7 @@ #include #include +#include "common/range_spec.h" #include "encoding.h" #include "storage/redis_db.h" #include "storage/redis_metadata.h" @@ -48,8 +49,8 @@ class Hash : public SubKeyScanner { rocksdb::Status IncrBy(const Slice &user_key, const Slice &field, int64_t increment, int64_t *ret); rocksdb::Status IncrByFloat(const Slice &user_key, const Slice &field, double increment, double *ret); rocksdb::Status MSet(const Slice &user_key, const std::vector &field_values, bool nx, int *ret); - rocksdb::Status Range(const Slice &user_key, const Slice &start, const Slice &stop, int64_t limit, - std::vector *field_values); + rocksdb::Status RangeByLex(const Slice &user_key, const CommonRangeLexSpec &spec, + std::vector *field_values); rocksdb::Status MGet(const Slice &user_key, const std::vector &fields, std::vector *values, std::vector *statuses); rocksdb::Status GetAll(const Slice &user_key, std::vector *field_values, diff --git a/src/types/redis_zset.cc b/src/types/redis_zset.cc index 57aecd71ac0..bc2b7ef50bb 100644 --- a/src/types/redis_zset.cc +++ b/src/types/redis_zset.cc @@ -414,8 +414,8 @@ rocksdb::Status ZSet::RangeByScore(const Slice &user_key, ZRangeSpec spec, std:: return rocksdb::Status::OK(); } -rocksdb::Status ZSet::RangeByLex(const Slice &user_key, const ZRangeLexSpec &spec, std::vector *members, - int *size) { +rocksdb::Status ZSet::RangeByLex(const Slice &user_key, const CommonRangeLexSpec &spec, + std::vector *members, int *size) { if (size) *size = 0; if (members) members->clear(); if (spec.offset > -1 && spec.count == 0) { @@ -564,7 +564,7 @@ rocksdb::Status ZSet::RemoveRangeByScore(const Slice &user_key, ZRangeSpec spec, return RangeByScore(user_key, spec, nullptr, ret); } -rocksdb::Status ZSet::RemoveRangeByLex(const Slice &user_key, ZRangeLexSpec spec, int *ret) { +rocksdb::Status ZSet::RemoveRangeByLex(const Slice &user_key, CommonRangeLexSpec spec, int *ret) { spec.removed = true; return RangeByLex(user_key, spec, nullptr, ret); } @@ -799,39 +799,6 @@ Status ZSet::ParseRangeSpec(const std::string &min, const std::string &max, ZRan return Status::OK(); } -Status ZSet::ParseRangeLexSpec(const std::string &min, const std::string &max, ZRangeLexSpec *spec) { - if (min == "+" || max == "-") { - return Status(Status::NotOK, "min > max"); - } - - if (min == "-") { - spec->min = ""; - } else { - if (min[0] == '(') { - spec->minex = true; - } else if (min[0] == '[') { - spec->minex = false; - } else { - return Status(Status::NotOK, "the min is illegal"); - } - spec->min = min.substr(1); - } - - if (max == "+") { - spec->max_infinite = true; - } else { - if (max[0] == '(') { - spec->maxex = true; - } else if (max[0] == '[') { - spec->maxex = false; - } else { - return Status(Status::NotOK, "the max is illegal"); - } - spec->max = max.substr(1); - } - return Status::OK(); -} - rocksdb::Status ZSet::Scan(const Slice &user_key, const std::string &cursor, uint64_t limit, const std::string &member_prefix, std::vector *members, std::vector *scores) { diff --git a/src/types/redis_zset.h b/src/types/redis_zset.h index ac2625dee42..fc8a4ecb3d5 100644 --- a/src/types/redis_zset.h +++ b/src/types/redis_zset.h @@ -25,6 +25,7 @@ #include #include +#include "common/range_spec.h" #include "storage/redis_db.h" #include "storage/redis_metadata.h" @@ -43,15 +44,6 @@ struct ZRangeSpec { ZRangeSpec() = default; }; -struct ZRangeLexSpec { - std::string min, max; - bool minex = false, maxex = false; /* are min or max exclusive */ - bool max_infinite = false; /* are max infinite */ - int offset = -1, count = -1; - bool removed = false, reversed = false; - ZRangeLexSpec() = default; -}; - struct KeyWeight { std::string key; double weight; @@ -107,17 +99,16 @@ class ZSet : public SubKeyScanner { rocksdb::Status IncrBy(const Slice &user_key, const Slice &member, double increment, double *score); rocksdb::Status Range(const Slice &user_key, int start, int stop, uint8_t flags, std::vector *mscores); rocksdb::Status RangeByScore(const Slice &user_key, ZRangeSpec spec, std::vector *mscores, int *size); - rocksdb::Status RangeByLex(const Slice &user_key, const ZRangeLexSpec &spec, std::vector *members, + rocksdb::Status RangeByLex(const Slice &user_key, const CommonRangeLexSpec &spec, std::vector *members, int *size); rocksdb::Status Rank(const Slice &user_key, const Slice &member, bool reversed, int *ret); rocksdb::Status Remove(const Slice &user_key, const std::vector &members, int *ret); rocksdb::Status RemoveRangeByScore(const Slice &user_key, ZRangeSpec spec, int *ret); - rocksdb::Status RemoveRangeByLex(const Slice &user_key, ZRangeLexSpec spec, int *ret); + rocksdb::Status RemoveRangeByLex(const Slice &user_key, CommonRangeLexSpec spec, int *ret); rocksdb::Status RemoveRangeByRank(const Slice &user_key, int start, int stop, int *ret); rocksdb::Status Pop(const Slice &user_key, int count, bool min, std::vector *mscores); rocksdb::Status Score(const Slice &user_key, const Slice &member, double *score); static Status ParseRangeSpec(const std::string &min, const std::string &max, ZRangeSpec *spec); - static Status ParseRangeLexSpec(const std::string &min, const std::string &max, ZRangeLexSpec *spec); rocksdb::Status Scan(const Slice &user_key, const std::string &cursor, uint64_t limit, const std::string &member_prefix, std::vector *members, std::vector *scores = nullptr); diff --git a/tests/cppunit/t_hash_test.cc b/tests/cppunit/t_hash_test.cc index 9f0eabd6f3b..7caa367d3bd 100644 --- a/tests/cppunit/t_hash_test.cc +++ b/tests/cppunit/t_hash_test.cc @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -160,8 +161,8 @@ TEST_F(RedisHashTest, HIncrByFloat) { hash->Del(key_); } -TEST_F(RedisHashTest, HRange) { - int ret; +TEST_F(RedisHashTest, HRangeByLex) { + int ret = 0; std::vector fvs; for (size_t i = 0; i < 4; i++) { fvs.emplace_back(FieldValue{"key" + std::to_string(i), "value" + std::to_string(i)}); @@ -180,19 +181,113 @@ TEST_F(RedisHashTest, HRange) { s = hash->MSet(key_, fvs, false, &ret); EXPECT_EQ(ret, 0); std::vector result; - s = hash->Range(key_, "key0", "key4", INT_MAX, &result); + CommonRangeLexSpec spec; + spec.offset = 0; + spec.count = INT_MAX; + spec.min = "key0"; + spec.max = "key3"; + s = hash->RangeByLex(key_, spec, &result); EXPECT_TRUE(s.ok()); EXPECT_EQ(4, result.size()); EXPECT_EQ("key0", result[0].field); EXPECT_EQ("key1", result[1].field); EXPECT_EQ("key2", result[2].field); + EXPECT_EQ("key3", result[3].field); hash->Del(key_); } + + rocksdb::Status s = hash->MSet(key_, tmp, false, &ret); + EXPECT_TRUE(s.ok() && static_cast(tmp.size()) == ret); + // use offset and count + std::vector result; + CommonRangeLexSpec spec; + spec.offset = 0; + spec.count = INT_MAX; + spec.min = "key0"; + spec.max = "key3"; + spec.offset = 1; + s = hash->RangeByLex(key_, spec, &result); + EXPECT_TRUE(s.ok()); + EXPECT_EQ(3, result.size()); + EXPECT_EQ("key1", result[0].field); + EXPECT_EQ("key2", result[1].field); + EXPECT_EQ("key3", result[2].field); + + spec.offset = 1; + spec.count = 1; + s = hash->RangeByLex(key_, spec, &result); + EXPECT_TRUE(s.ok()); + EXPECT_EQ(1, result.size()); + EXPECT_EQ("key1", result[0].field); + + spec.offset = 0; + spec.count = 0; + s = hash->RangeByLex(key_, spec, &result); + EXPECT_TRUE(s.ok()); + EXPECT_EQ(0, result.size()); + + spec.offset = 1000; + spec.count = 1000; + s = hash->RangeByLex(key_, spec, &result); + EXPECT_TRUE(s.ok()); + EXPECT_EQ(0, result.size()); + // exclusive range + spec.offset = 0; + spec.count = -1; + spec.minex = true; + s = hash->RangeByLex(key_, spec, &result); + EXPECT_TRUE(s.ok()); + EXPECT_EQ(3, result.size()); + EXPECT_EQ("key1", result[0].field); + EXPECT_EQ("key2", result[1].field); + EXPECT_EQ("key3", result[2].field); + + spec.offset = 0; + spec.count = -1; + spec.maxex = true; + spec.minex = false; + s = hash->RangeByLex(key_, spec, &result); + EXPECT_TRUE(s.ok()); + EXPECT_EQ(3, result.size()); + EXPECT_EQ("key0", result[0].field); + EXPECT_EQ("key1", result[1].field); + EXPECT_EQ("key2", result[2].field); + + spec.offset = 0; + spec.count = -1; + spec.maxex = true; + spec.minex = true; + s = hash->RangeByLex(key_, spec, &result); + EXPECT_TRUE(s.ok()); + EXPECT_EQ(2, result.size()); + EXPECT_EQ("key1", result[0].field); + EXPECT_EQ("key2", result[1].field); + + // inf and revered + spec.minex = false; + spec.maxex = false; + spec.min = "-"; + spec.max = "+"; + spec.max_infinite = true; + spec.reversed = true; + s = hash->RangeByLex(key_, spec, &result); + EXPECT_TRUE(s.ok()); + EXPECT_EQ(4 + 26, result.size()); + EXPECT_EQ("key3", result[0].field); + EXPECT_EQ("key2", result[1].field); + EXPECT_EQ("key1", result[2].field); + EXPECT_EQ("key0", result[3].field); + hash->Del(key_); } -TEST_F(RedisHashTest, HRangeNonExistingKey) { +TEST_F(RedisHashTest, HRangeByLexNonExistingKey) { std::vector result; - auto s = hash->Range("non-existing-key", "any-start-key", "any-end-key", 10, &result); + CommonRangeLexSpec spec; + spec.offset = 0; + spec.count = INT_MAX; + spec.min = "any-start-key"; + spec.max = "any-end-key"; + auto s = hash->RangeByLex("non-existing-key", spec, &result); EXPECT_TRUE(s.ok()); EXPECT_EQ(result.size(), 0); } diff --git a/tests/cppunit/t_zset_test.cc b/tests/cppunit/t_zset_test.cc index 288fbe1142c..8dc3917568e 100644 --- a/tests/cppunit/t_zset_test.cc +++ b/tests/cppunit/t_zset_test.cc @@ -174,7 +174,7 @@ TEST_F(RedisZSetTest, RangeByLex) { zset->Add(key_, ZAddFlags::Default(), &mscores, &ret); EXPECT_EQ(fields_.size(), ret); - ZRangeLexSpec spec; + CommonRangeLexSpec spec; spec.min = fields_[0].ToString(); spec.max = fields_[fields_.size() - 1].ToString(); std::vector members; diff --git a/tests/gocase/unit/type/hash/hash_test.go b/tests/gocase/unit/type/hash/hash_test.go index 8ffb6a8937b..f8a23bc1a6a 100644 --- a/tests/gocase/unit/type/hash/hash_test.go +++ b/tests/gocase/unit/type/hash/hash_test.go @@ -614,54 +614,71 @@ func TestHash(t *testing.T) { }) kvArray := []string{"a", "a", "b", "b", "c", "c", "d", "d", "e", "e", "key1", "value1", "key2", "value2", "key3", "value3", "key10", "value10", "z", "z", "x", "x"} - t.Run("HRange normal situation ", func(t *testing.T) { + t.Run("HrangeByLex BYLEX normal situation ", func(t *testing.T) { require.NoError(t, rdb.Del(ctx, "hashkey").Err()) require.NoError(t, rdb.HMSet(ctx, "hashkey", kvArray).Err()) - require.EqualValues(t, []interface{}{"key1", "value1", "key10", "value10"}, rdb.Do(ctx, "HRange", "hashkey", "key1", "key2", "limit", 100).Val()) - require.EqualValues(t, []interface{}{"key1", "value1", "key10", "value10", "key2", "value2"}, rdb.Do(ctx, "HRange", "hashkey", "key1", "key3", "limit", 100).Val()) + require.EqualValues(t, []interface{}{"key1", "value1", "key10", "value10"}, rdb.Do(ctx, "HrangeByLex", "hashkey", "[key1", "[key10").Val()) + require.EqualValues(t, []interface{}{"key1", "value1", "key10", "value10", "key2", "value2"}, rdb.Do(ctx, "HrangeByLex", "hashkey", "[key1", "[key2").Val()) + require.EqualValues(t, []interface{}{"key1", "value1", "key10", "value10", "key2", "value2"}, rdb.Do(ctx, "HrangeByLex", "hashkey", "[key1", "(key3").Val()) + require.EqualValues(t, []interface{}{"key10", "value10", "key2", "value2", "key3", "value3"}, rdb.Do(ctx, "HrangeByLex", "hashkey", "(key1", "[key3", "limit", 0, -1).Val()) + require.EqualValues(t, []interface{}{"key10", "value10", "key2", "value2"}, rdb.Do(ctx, "HrangeByLex", "hashkey", "(key1", "(key3").Val()) + require.EqualValues(t, []interface{}{"a", "a", "b", "b"}, rdb.Do(ctx, "HrangeByLex", "hashkey", "-", "[b").Val()) + require.EqualValues(t, []interface{}{"x", "x", "z", "z"}, rdb.Do(ctx, "HrangeByLex", "hashkey", "[x", "+").Val()) + require.EqualValues(t, []interface{}{"z", "z", "x", "x"}, rdb.Do(ctx, "HrangeByLex", "hashkey", "+", "[x", "REV").Val()) + require.EqualValues(t, []interface{}{"a", "a", "b", "b", "c", "c", "d", "d", "e", "e", "key1", "value1", "key10", "value10", "key2", "value2", "key3", "value3", "x", "x", "z", "z"}, rdb.Do(ctx, "HrangeByLex", "hashkey", "-", "+").Val()) + require.EqualValues(t, []interface{}{"z", "z", "x", "x", "key3", "value3", "key2", "value2", "key10", "value10", "key1", "value1", "e", "e", "d", "d", "c", "c", "b", "b", "a", "a"}, rdb.Do(ctx, "HrangeByLex", "hashkey", "+", "-", "REV").Val()) }) - t.Run("HRange stop <= start", func(t *testing.T) { + t.Run("HrangeByLex BYLEX stop < start", func(t *testing.T) { require.NoError(t, rdb.Del(ctx, "hashkey").Err()) require.NoError(t, rdb.HMSet(ctx, "hashkey", kvArray).Err()) - require.EqualValues(t, []interface{}{}, rdb.Do(ctx, "HRange", "hashkey", "key2", "key1", "limit", 100).Val()) - require.EqualValues(t, []interface{}{}, rdb.Do(ctx, "HRange", "hashkey", "key1", "key1", "limit", 100).Val()) + require.EqualValues(t, []interface{}{}, rdb.Do(ctx, "HrangeByLex", "hashkey", "[key2", "[key1", "limit", 0, 100).Val()) + require.EqualValues(t, []interface{}{}, rdb.Do(ctx, "HrangeByLex", "hashkey", "(key1", "(key1", "limit", 0, 100).Val()) }) - t.Run("HRange limit", func(t *testing.T) { + t.Run("HrangeByLex BYLEX limit", func(t *testing.T) { require.NoError(t, rdb.Del(ctx, "hashkey").Err()) require.NoError(t, rdb.HMSet(ctx, "hashkey", kvArray).Err()) - require.EqualValues(t, []interface{}{"a", "a", "b", "b"}, rdb.Do(ctx, "HRange", "hashkey", "a", "z", "limit", 2).Val()) - require.EqualValues(t, []interface{}{"a", "a", "b", "b", "c", "c", "d", "d", "e", "e", "key1", "value1", "key10", "value10", "key2", "value2", "key3", "value3", "x", "x", "z", "z"}, rdb.Do(ctx, "HRange", "hashkey", "a", "zzz", "limit", 10000).Val()) + require.EqualValues(t, []interface{}{"a", "a", "b", "b"}, rdb.Do(ctx, "HrangeByLex", "hashkey", "[a", "[z", "limit", 0, 2).Val()) + require.EqualValues(t, []interface{}{"z", "z", "x", "x"}, rdb.Do(ctx, "HrangeByLex", "hashkey", "[z", "[a", "limit", 0, 2, "REV").Val()) + require.EqualValues(t, []interface{}{"b", "b", "c", "c"}, rdb.Do(ctx, "HrangeByLex", "hashkey", "[a", "[z", "limit", 1, 2).Val()) + require.EqualValues(t, []interface{}{"x", "x", "key3", "value3"}, rdb.Do(ctx, "HrangeByLex", "hashkey", "[z", "[a", "limit", 1, 2, "REV").Val()) + require.EqualValues(t, []interface{}{}, rdb.Do(ctx, "HrangeByLex", "hashkey", "[a", "[z", "limit", 1000, -1).Val()) + require.EqualValues(t, []interface{}{}, rdb.Do(ctx, "HrangeByLex", "hashkey", "[a", "[z", "limit", 0, 0).Val()) + require.EqualValues(t, []interface{}{"a", "a", "b", "b", "c", "c", "d", "d", "e", "e", "key1", "value1", "key10", "value10", "key2", "value2", "key3", "value3", "x", "x", "z", "z"}, rdb.Do(ctx, "HrangeByLex", "hashkey", "[a", "[zzz", "limit", 0, 10000).Val()) }) - t.Run("HRange limit is negative", func(t *testing.T) { + t.Run("HrangeByLex BYLEX limit is negative", func(t *testing.T) { require.NoError(t, rdb.Del(ctx, "hashkey").Err()) require.NoError(t, rdb.HMSet(ctx, "hashkey", kvArray).Err()) - require.EqualValues(t, []interface{}{}, rdb.Do(ctx, "HRange", "hashkey", "a", "z", "limit", -100).Val()) - require.EqualValues(t, []interface{}{}, rdb.Do(ctx, "HRange", "hashkey", "a", "z", "limit", 0).Val()) + require.EqualValues(t, []interface{}{"x", "x", "z", "z"}, rdb.Do(ctx, "HrangeByLex", "hashkey", "[x", "[z", "limit", 0, -100).Val()) + require.EqualValues(t, []interface{}{"z", "z", "x", "x"}, rdb.Do(ctx, "HrangeByLex", "hashkey", "[z", "[x", "limit", 0, -100, "REV").Val()) + require.EqualValues(t, []interface{}{"x", "x", "z", "z"}, rdb.Do(ctx, "HrangeByLex", "hashkey", "[x", "[z", "limit", 0, -10).Val()) }) - t.Run("HRange nonexistent key", func(t *testing.T) { + t.Run("HrangeByLex BYLEX nonexistent key", func(t *testing.T) { require.NoError(t, rdb.Del(ctx, "hashkey").Err()) - require.EqualValues(t, []interface{}{}, rdb.Do(ctx, "HRange", "hashkey", "a", "z", "limit", 10000).Val()) - require.EqualValues(t, []interface{}{}, rdb.Do(ctx, "HRange", "hashkey", "a", "z", "limit", 10000).Val()) + require.EqualValues(t, []interface{}{}, rdb.Do(ctx, "HrangeByLex", "hashkey", "[a", "[z").Val()) + require.EqualValues(t, []interface{}{}, rdb.Do(ctx, "HrangeByLex", "hashkey", "[a", "[z").Val()) }) - t.Run("HRange limit typo", func(t *testing.T) { + t.Run("HrangeByLex typo", func(t *testing.T) { require.NoError(t, rdb.Del(ctx, "hashkey").Err()) require.NoError(t, rdb.HMSet(ctx, "hashkey", kvArray).Err()) - require.ErrorContains(t, rdb.Do(ctx, "HRange", "hashkey", "a", "z", "limitzz", 10000).Err(), "ERR syntax") + require.ErrorContains(t, rdb.Do(ctx, "HrangeByLex", "hashkey", "[a", "[z", "limitzz", 0, 10000).Err(), "ERR syntax") + require.ErrorContains(t, rdb.Do(ctx, "HrangeByLex", "hashkey", "[a", "[z", "limit", 0, 10000, "BYLE").Err(), "ERR syntax") + require.ErrorContains(t, rdb.Do(ctx, "HrangeByLex", "hashkey", "[a", "[z", "limit", 0, 10000, "RE").Err(), "ERR syntax") + require.ErrorContains(t, rdb.Do(ctx, "HrangeByLex", "hashkey", "a", "z").Err(), "illegal") }) - t.Run("HRange wrong number of arguments", func(t *testing.T) { + t.Run("HrangeByLex wrong number of arguments", func(t *testing.T) { require.NoError(t, rdb.Del(ctx, "hashkey").Err()) require.NoError(t, rdb.HMSet(ctx, "hashkey", kvArray).Err()) - require.ErrorContains(t, rdb.Do(ctx, "HRange", "hashkey", "a", "z", "limit", 10000, "a").Err(), "wrong number of arguments") - require.ErrorContains(t, rdb.Do(ctx, "HRange", "hashkey", "a", "z", "limit").Err(), "wrong number of arguments") - require.ErrorContains(t, rdb.Do(ctx, "HRange", "hashkey", "a").Err(), "wrong number of arguments") - require.ErrorContains(t, rdb.Do(ctx, "HRange", "hashkey").Err(), "wrong number of arguments") - require.ErrorContains(t, rdb.Do(ctx, "HRange").Err(), "wrong number of arguments") + require.ErrorContains(t, rdb.Do(ctx, "HrangeByLex", "hashkey", "[a", "[z", "limit", 10000, 1, 1, 1, 1).Err(), "syntax error") + require.ErrorContains(t, rdb.Do(ctx, "HrangeByLex", "hashkey", "[a", "[z", "limit").Err(), "no more item to parse") + require.ErrorContains(t, rdb.Do(ctx, "HrangeByLex", "hashkey", "[a").Err(), "wrong number of arguments") + require.ErrorContains(t, rdb.Do(ctx, "HrangeByLex", "hashkey").Err(), "wrong number of arguments") + require.ErrorContains(t, rdb.Do(ctx, "HrangeByLex").Err(), "wrong number of arguments") }) } }