diff --git a/src/commands/redis_cmd.cc b/src/commands/redis_cmd.cc index 903ca8fa168..1f98e64f84e 100644 --- a/src/commands/redis_cmd.cc +++ b/src/commands/redis_cmd.cc @@ -74,6 +74,7 @@ const char *errUnbalancedStreamList = "Unbalanced XREAD list of streams: for each stream key an ID or '$' must be specified."; const char *errTimeoutIsNegative = "timeout is negative"; const char *errLimitOptionNotAllowed = "syntax error, LIMIT cannot be used without the special ~ option"; +const char *errZSetLTGTNX = "GT, LT, and/or NX options at the same time are not compatible"; enum class AuthResult { OK, @@ -2377,12 +2378,20 @@ class CommandSInterStore : public Commander { class CommandZAdd : public Commander { public: Status Parse(const std::vector &args) override { - if (args.size() % 2 != 0) { - return Status(Status::RedisParseErr, errInvalidSyntax); + unsigned index = 2; + parseFlags(args, index); + if (auto s = validateFlags(); !s.IsOK()) { + return s; + } + if (auto left = (args.size() - index); left >= 0) { + if (flags_.HasIncr() && left != 2) { + return Status(Status::RedisParseErr, "INCR option supports a single increment-element pair"); + } else if (left % 2 != 0 || left == 0) { + return Status(Status::RedisParseErr, errInvalidSyntax); + } } - try { - for (unsigned i = 2; i < args.size(); i += 2) { + for (unsigned i = index; i < args.size(); i += 2) { double score = std::stod(args[i]); if (std::isnan(score)) { return Status(Status::RedisParseErr, "ERR score is not a valid float"); @@ -2397,19 +2406,62 @@ class CommandZAdd : public Commander { Status Execute(Server *svr, Connection *conn, std::string *output) override { int ret; + double old_score = member_scores_[0].score; Redis::ZSet zset_db(svr->storage_, conn->GetNamespace()); - rocksdb::Status s = zset_db.Add(args_[1], 0, &member_scores_, &ret); + rocksdb::Status s = zset_db.Add(args_[1], flags_, &member_scores_, &ret); if (!s.ok()) { return Status(Status::RedisExecErr, s.ToString()); } - *output = Redis::Integer(ret); + if (flags_.HasIncr()) { + auto new_score = member_scores_[0].score; + if ((flags_.HasNX() || flags_.HasXX() || flags_.HasLT() || flags_.HasGT()) && old_score == new_score && + ret == 0) { // not the first time using incr && score not changed + *output = Redis::NilString(); + return Status::OK(); + } + *output = Redis::BulkString(Util::Float2String(new_score)); + } else { + *output = Redis::Integer(ret); + } return Status::OK(); } private: std::vector member_scores_; + ZAddFlags flags_{0}; + + void parseFlags(const std::vector &args, unsigned &index); + Status validateFlags() const; }; +void CommandZAdd::parseFlags(const std::vector &args, unsigned &index) { + std::unordered_map options = {{"xx", kZSetXX}, {"nx", kZSetNX}, {"ch", kZSetCH}, + {"lt", kZSetLT}, {"gt", kZSetGT}, {"incr", kZSetIncr}}; + for (unsigned i = 2; i < args.size(); i++) { + auto option = Util::ToLower(args[i]); + auto it = options.find(option); + if (it != options.end()) { + flags_.SetFlag(it->second); + index++; + } else { + break; + } + } +} + +Status CommandZAdd::validateFlags() const { + if (!flags_.HasAnyFlags()) { + return Status::OK(); + } + if (flags_.HasNX() && flags_.HasXX()) { + return Status(Status::RedisParseErr, "XX and NX options at the same time are not compatible"); + } + if ((flags_.HasLT() && flags_.HasGT()) || (flags_.HasLT() && flags_.HasNX()) || (flags_.HasGT() && flags_.HasNX())) { + return Status(Status::RedisParseErr, errZSetLTGTNX); + } + return Status::OK(); +} + class CommandZCount : public Commander { public: Status Parse(const std::vector &args) override { diff --git a/src/types/redis_geo.cc b/src/types/redis_geo.cc index 293ca3776ba..0400eddceaf 100644 --- a/src/types/redis_geo.cc +++ b/src/types/redis_geo.cc @@ -35,7 +35,7 @@ rocksdb::Status Geo::Add(const Slice &user_key, std::vector *geo_point GeoHashFix52Bits bits = GeoHashHelper::Align52Bits(hash); member_scores.emplace_back(MemberScore{geo_point.member, static_cast(bits)}); } - return ZSet::Add(user_key, 0, &member_scores, ret); + return ZSet::Add(user_key, ZAddFlags::Default(), &member_scores, ret); } rocksdb::Status Geo::Dist(const Slice &user_key, const Slice &member_1, const Slice &member_2, double *dist) { @@ -120,7 +120,7 @@ rocksdb::Status Geo::Radius(const Slice &user_key, double longitude, double lati member_scores.emplace_back(MemberScore{geo_point.member, score}); } int ret; - ZSet::Add(store_key, 0, &member_scores, &ret); + ZSet::Add(store_key, ZAddFlags::Default(), &member_scores, &ret); } } diff --git a/src/types/redis_zset.cc b/src/types/redis_zset.cc index 63d2a9a8723..558f25ab27b 100644 --- a/src/types/redis_zset.cc +++ b/src/types/redis_zset.cc @@ -37,7 +37,7 @@ rocksdb::Status ZSet::GetMetadata(const Slice &ns_key, ZSetMetadata *metadata) { return Database::GetMetadata(kRedisZSet, ns_key, metadata); } -rocksdb::Status ZSet::Add(const Slice &user_key, uint8_t flags, std::vector *mscores, int *ret) { +rocksdb::Status ZSet::Add(const Slice &user_key, ZAddFlags flags, std::vector *mscores, int *ret) { *ret = 0; std::string ns_key; @@ -49,6 +49,7 @@ rocksdb::Status ZSet::Add(const Slice &user_key, uint8_t flags, std::vectorGet(rocksdb::ReadOptions(), member_key, &old_score_bytes); if (!s.ok() && !s.IsNotFound()) return s; if (s.ok()) { + if (!s.IsNotFound() && flags.HasNX()) { + continue; + } double old_score = DecodeDouble(old_score_bytes.data()); - if (flags == kZSetIncr) { + if (flags.HasIncr()) { + if ((flags.HasLT() && (*mscores)[i].score >= 0) || (flags.HasGT() && (*mscores)[i].score <= 0)) { + continue; + } (*mscores)[i].score += old_score; if (std::isnan((*mscores)[i].score)) { return rocksdb::Status::InvalidArgument("resulting score is not a number (NaN)"); } } if ((*mscores)[i].score != old_score) { + if ((flags.HasLT() && (*mscores)[i].score >= old_score) || + (flags.HasGT() && (*mscores)[i].score <= old_score)) { + continue; + } old_score_bytes.append((*mscores)[i].member); std::string old_score_key; InternalKey(ns_key, old_score_bytes, metadata.version, storage_->IsSlotIdEncoded()).Encode(&old_score_key); @@ -94,10 +105,14 @@ rocksdb::Status ZSet::Add(const Slice &user_key, uint8_t flags, std::vectorIsSlotIdEncoded()).Encode(&new_score_key); batch.Put(score_cf_handle_, new_score_key, Slice()); + changed++; } continue; } } + if (flags.HasXX()) { + continue; + } std::string score_bytes, score_key; PutDouble(&score_bytes, (*mscores)[i].score); batch.Put(member_key, score_bytes); @@ -113,6 +128,9 @@ rocksdb::Status ZSet::Add(const Slice &user_key, uint8_t flags, std::vectorWrite(storage_->DefaultWriteOptions(), &batch); } @@ -138,7 +156,7 @@ rocksdb::Status ZSet::IncrBy(const Slice &user_key, const Slice &member, double int ret; std::vector mscores; mscores.emplace_back(MemberScore{member.ToString(), increment}); - rocksdb::Status s = Add(user_key, kZSetIncr, &mscores, &ret); + rocksdb::Status s = Add(user_key, ZAddFlags::Incr(), &mscores, &ret); if (!s.ok()) return s; *score = mscores[0].score; return rocksdb::Status::OK(); diff --git a/src/types/redis_zset.h b/src/types/redis_zset.h index 42b3fd5b50c..b97e98be24f 100644 --- a/src/types/redis_zset.h +++ b/src/types/redis_zset.h @@ -82,6 +82,31 @@ enum ZSetFlags { kZSetXX = 1 << 2, kZSetReversed = 1 << 3, kZSetRemoved = 1 << 4, + kZSetGT = 1 << 5, + kZSetLT = 1 << 6, + kZSetCH = 1 << 7, +}; + +class ZAddFlags { + public: + explicit ZAddFlags(uint8_t flags = 0) : flags(flags) {} + + bool HasNX() const { return (flags & kZSetNX) != 0; } + bool HasXX() const { return (flags & kZSetXX) != 0; } + bool HasLT() const { return (flags & kZSetLT) != 0; } + bool HasGT() const { return (flags & kZSetGT) != 0; } + bool HasCH() const { return (flags & kZSetCH) != 0; } + bool HasIncr() const { return (flags & kZSetIncr) != 0; } + bool HasAnyFlags() const { return flags != 0; } + + void SetFlag(ZSetFlags setFlags) { flags |= setFlags; } + + static const ZAddFlags Incr() { return ZAddFlags{kZSetIncr}; } + + static const ZAddFlags Default() { return ZAddFlags{0}; } + + private: + uint8_t flags = 0; }; namespace Redis { @@ -90,7 +115,7 @@ class ZSet : public SubKeyScanner { public: explicit ZSet(Engine::Storage *storage, const std::string &ns) : SubKeyScanner(storage, ns), score_cf_handle_(storage->GetCFHandle("zset_score")) {} - rocksdb::Status Add(const Slice &user_key, uint8_t flags, std::vector *mscores, int *ret); + rocksdb::Status Add(const Slice &user_key, ZAddFlags flags, std::vector *mscores, int *ret); rocksdb::Status Card(const Slice &user_key, int *ret); rocksdb::Status Count(const Slice &user_key, const ZRangeSpec &spec, int *ret); rocksdb::Status IncrBy(const Slice &user_key, const Slice &member, double increment, double *score); diff --git a/tests/cppunit/compact_test.cc b/tests/cppunit/compact_test.cc index 4042e885ed9..1c75e04f822 100644 --- a/tests/cppunit/compact_test.cc +++ b/tests/cppunit/compact_test.cc @@ -75,7 +75,7 @@ TEST(Compact, Filter) { auto zset = std::make_unique(storage_.get(), ns); std::string expired_zset_key = "expire_zset_key"; std::vector member_scores = {MemberScore{"z1", 1.1}, MemberScore{"z2", 0.4}}; - zset->Add(expired_zset_key, 0, &member_scores, &ret); + zset->Add(expired_zset_key, ZAddFlags::Default(), &member_scores, &ret); zset->Expire(expired_zset_key, 1); // expired usleep(10000); diff --git a/tests/cppunit/disk_test.cc b/tests/cppunit/disk_test.cc index ca252df43fa..5472c5b9d5e 100644 --- a/tests/cppunit/disk_test.cc +++ b/tests/cppunit/disk_test.cc @@ -148,7 +148,7 @@ TEST_F(RedisDiskTest, ZsetDisk) { mscores[i].score = 1.0 * value_size[int(values_.size()) - i - 1]; approximate_size += (key_.size() + 8 + mscores[i].member.size() + 8) * 2; } - rocksdb::Status s = zset->Add(key_, 0, &mscores, &ret); + rocksdb::Status s = zset->Add(key_, ZAddFlags::Default(), &mscores, &ret); EXPECT_TRUE(s.ok() && ret == 5); uint64_t key_size = 0; EXPECT_TRUE(disk->GetKeySize(key_, kRedisZSet, &key_size).ok()); diff --git a/tests/cppunit/t_zset_test.cc b/tests/cppunit/t_zset_test.cc index d750ab0a29c..288fbe1142c 100644 --- a/tests/cppunit/t_zset_test.cc +++ b/tests/cppunit/t_zset_test.cc @@ -47,14 +47,14 @@ TEST_F(RedisZSetTest, Add) { for (size_t i = 0; i < fields_.size(); i++) { mscores.emplace_back(MemberScore{fields_[i].ToString(), scores_[i]}); } - zset->Add(key_, 0, &mscores, &ret); + zset->Add(key_, ZAddFlags::Default(), &mscores, &ret); EXPECT_EQ(static_cast(fields_.size()), ret); for (size_t i = 0; i < fields_.size(); i++) { double got; rocksdb::Status s = zset->Score(key_, fields_[i], &got); EXPECT_EQ(scores_[i], got); } - zset->Add(key_, 0, &mscores, &ret); + zset->Add(key_, ZAddFlags::Default(), &mscores, &ret); EXPECT_EQ(ret, 0); zset->Del(key_); } @@ -65,7 +65,7 @@ TEST_F(RedisZSetTest, IncrBy) { for (size_t i = 0; i < fields_.size(); i++) { mscores.emplace_back(MemberScore{fields_[i].ToString(), scores_[i]}); } - zset->Add(key_, 0, &mscores, &ret); + zset->Add(key_, ZAddFlags::Default(), &mscores, &ret); EXPECT_EQ(fields_.size(), ret); for (size_t i = 0; i < fields_.size(); i++) { double increment = 12.3, score; @@ -81,7 +81,7 @@ TEST_F(RedisZSetTest, Remove) { for (size_t i = 0; i < fields_.size(); i++) { mscores.emplace_back(MemberScore{fields_[i].ToString(), scores_[i]}); } - zset->Add(key_, 0, &mscores, &ret); + zset->Add(key_, ZAddFlags::Default(), &mscores, &ret); EXPECT_EQ(fields_.size(), ret); zset->Remove(key_, fields_, &ret); EXPECT_EQ(fields_.size(), ret); @@ -100,7 +100,7 @@ TEST_F(RedisZSetTest, Range) { mscores.emplace_back(MemberScore{fields_[i].ToString(), scores_[i]}); } int count = mscores.size() - 1; - zset->Add(key_, 0, &mscores, &ret); + zset->Add(key_, ZAddFlags::Default(), &mscores, &ret); EXPECT_EQ(fields_.size(), ret); zset->Range(key_, 0, -2, 0, &mscores); EXPECT_EQ(mscores.size(), count); @@ -118,7 +118,7 @@ TEST_F(RedisZSetTest, RevRange) { mscores.emplace_back(MemberScore{fields_[i].ToString(), scores_[i]}); } int count = mscores.size() - 1; - zset->Add(key_, 0, &mscores, &ret); + zset->Add(key_, ZAddFlags::Default(), &mscores, &ret); EXPECT_EQ(static_cast(fields_.size()), ret); zset->Range(key_, 0, -2, kZSetReversed, &mscores); EXPECT_EQ(mscores.size(), count); @@ -135,7 +135,7 @@ TEST_F(RedisZSetTest, PopMin) { for (size_t i = 0; i < fields_.size(); i++) { mscores.emplace_back(MemberScore{fields_[i].ToString(), scores_[i]}); } - zset->Add(key_, 0, &mscores, &ret); + zset->Add(key_, ZAddFlags::Default(), &mscores, &ret); EXPECT_EQ(static_cast(fields_.size()), ret); zset->Pop(key_, mscores.size() - 1, true, &mscores); for (size_t i = 0; i < mscores.size(); i++) { @@ -154,7 +154,7 @@ TEST_F(RedisZSetTest, PopMax) { for (size_t i = 0; i < fields_.size(); i++) { mscores.emplace_back(MemberScore{fields_[i].ToString(), scores_[i]}); } - zset->Add(key_, 0, &mscores, &ret); + zset->Add(key_, ZAddFlags::Default(), &mscores, &ret); EXPECT_EQ(static_cast(fields_.size()), ret); zset->Pop(key_, mscores.size() - 1, false, &mscores); for (size_t i = 0; i < mscores.size(); i++) { @@ -171,7 +171,7 @@ TEST_F(RedisZSetTest, RangeByLex) { for (size_t i = 0; i < fields_.size(); i++) { mscores.emplace_back(MemberScore{fields_[i].ToString(), scores_[i]}); } - zset->Add(key_, 0, &mscores, &ret); + zset->Add(key_, ZAddFlags::Default(), &mscores, &ret); EXPECT_EQ(fields_.size(), ret); ZRangeLexSpec spec; @@ -227,7 +227,7 @@ TEST_F(RedisZSetTest, RangeByScore) { for (size_t i = 0; i < fields_.size(); i++) { mscores.emplace_back(MemberScore{fields_[i].ToString(), scores_[i]}); } - zset->Add(key_, 0, &mscores, &ret); + zset->Add(key_, ZAddFlags::Default(), &mscores, &ret); EXPECT_EQ(fields_.size(), ret); // test case: inclusive the min and max score @@ -275,7 +275,7 @@ TEST_F(RedisZSetTest, RangeByScoreWithLimit) { for (size_t i = 0; i < fields_.size(); i++) { mscores.emplace_back(MemberScore{fields_[i].ToString(), scores_[i]}); } - zset->Add(key_, 0, &mscores, &ret); + zset->Add(key_, ZAddFlags::Default(), &mscores, &ret); EXPECT_EQ(fields_.size(), ret); ZRangeSpec spec; @@ -296,7 +296,7 @@ TEST_F(RedisZSetTest, RemRangeByScore) { for (size_t i = 0; i < fields_.size(); i++) { mscores.emplace_back(MemberScore{fields_[i].ToString(), scores_[i]}); } - zset->Add(key_, 0, &mscores, &ret); + zset->Add(key_, ZAddFlags::Default(), &mscores, &ret); EXPECT_EQ(fields_.size(), ret); ZRangeSpec spec; spec.min = scores_[0]; @@ -315,7 +315,7 @@ TEST_F(RedisZSetTest, RemoveRangeByRank) { for (size_t i = 0; i < fields_.size(); i++) { mscores.emplace_back(MemberScore{fields_[i].ToString(), scores_[i]}); } - zset->Add(key_, 0, &mscores, &ret); + zset->Add(key_, ZAddFlags::Default(), &mscores, &ret); EXPECT_EQ(fields_.size(), ret); zset->RemoveRangeByRank(key_, 0, fields_.size() - 2, &ret); EXPECT_EQ(fields_.size() - 1, ret); @@ -329,7 +329,7 @@ TEST_F(RedisZSetTest, RemoveRevRangeByRank) { for (size_t i = 0; i < fields_.size(); i++) { mscores.emplace_back(MemberScore{fields_[i].ToString(), scores_[i]}); } - zset->Add(key_, 0, &mscores, &ret); + zset->Add(key_, ZAddFlags::Default(), &mscores, &ret); EXPECT_EQ(fields_.size(), ret); zset->RemoveRangeByRank(key_, 0, fields_.size() - 2, &ret); EXPECT_EQ(static_cast(fields_.size() - 1), ret); @@ -343,7 +343,7 @@ TEST_F(RedisZSetTest, Rank) { for (size_t i = 0; i < fields_.size(); i++) { mscores.emplace_back(MemberScore{fields_[i].ToString(), scores_[i]}); } - zset->Add(key_, 0, &mscores, &ret); + zset->Add(key_, ZAddFlags::Default(), &mscores, &ret); EXPECT_EQ(static_cast(fields_.size()), ret); for (size_t i = 0; i < fields_.size(); i++) { diff --git a/tests/gocase/unit/type/zset/zset_test.go b/tests/gocase/unit/type/zset/zset_test.go index 616387e1037..7dae9e625b2 100644 --- a/tests/gocase/unit/type/zset/zset_test.go +++ b/tests/gocase/unit/type/zset/zset_test.go @@ -93,6 +93,82 @@ func basicTests(t *testing.T, rdb *redis.Client, ctx context.Context, encoding s require.Equal(t, float64(30), rdb.ZScore(ctx, "ztmp", "x").Val()) }) + t.Run(fmt.Sprintf("ZSET ZADD INCR option supports a single pair - %s", encoding), func(t *testing.T) { + rdb.Del(ctx, "ztmp") + require.Equal(t, 1.5, rdb.ZAddArgsIncr(ctx, "ztmp", redis.ZAddArgs{Members: []redis.Z{{Member: "abc", Score: 1.5}}}).Val()) + require.Contains(t, rdb.ZAddArgsIncr(ctx, "ztmp", redis.ZAddArgs{Members: []redis.Z{{Member: "abc", Score: 1.5}, {Member: "adc"}}}).Err(), + "INCR option supports a single increment-element pair") + }) + + t.Run(fmt.Sprintf("ZSET ZADD IncrMixedOtherOptions - %s", encoding), func(t *testing.T) { + rdb.Del(ctx, "ztmp") + require.Equal(t, "1.5", rdb.Do(ctx, "zadd", "ztmp", "nx", "nx", "nx", "nx", "incr", "1.5", "abc").Val()) + require.Equal(t, redis.Nil, rdb.Do(ctx, "zadd", "ztmp", "nx", "nx", "nx", "nx", "incr", "1.5", "abc").Err()) + require.Equal(t, "3", rdb.Do(ctx, "zadd", "ztmp", "xx", "xx", "xx", "xx", "incr", "1.5", "abc").Val()) + + rdb.Del(ctx, "ztmp") + require.Equal(t, 1.5, rdb.ZAddArgsIncr(ctx, "ztmp", redis.ZAddArgs{NX: true, Members: []redis.Z{{Member: "abc", Score: 1.5}}}).Val()) + require.Equal(t, redis.Nil, rdb.ZAddArgsIncr(ctx, "ztmp", redis.ZAddArgs{NX: true, Members: []redis.Z{{Member: "abc", Score: 1.5}}}).Err()) + + rdb.Del(ctx, "ztmp") + require.Equal(t, redis.Nil, rdb.ZAddArgsIncr(ctx, "ztmp", redis.ZAddArgs{XX: true, Members: []redis.Z{{Member: "abc", Score: 1.5}}}).Err()) + require.Equal(t, 1.5, rdb.ZAddArgsIncr(ctx, "ztmp", redis.ZAddArgs{NX: true, Members: []redis.Z{{Member: "abc", Score: 1.5}}}).Val()) + + rdb.Del(ctx, "ztmp") + require.Equal(t, 1.5, rdb.ZAddArgsIncr(ctx, "ztmp", redis.ZAddArgs{NX: true, Members: []redis.Z{{Member: "abc", Score: 1.5}}}).Val()) + require.Equal(t, 3.0, rdb.ZAddArgsIncr(ctx, "ztmp", redis.ZAddArgs{GT: true, Members: []redis.Z{{Member: "abc", Score: 1.5}}}).Val()) + require.Equal(t, 0.0, rdb.ZAddArgsIncr(ctx, "ztmp", redis.ZAddArgs{GT: true, Members: []redis.Z{{Member: "abc", Score: -1.5}}}).Val()) + require.Equal(t, redis.Nil, rdb.ZAddArgsIncr(ctx, "ztmp", redis.ZAddArgs{GT: true, Members: []redis.Z{{Member: "abc", Score: -1.5}}}).Err()) + + rdb.Del(ctx, "ztmp") + require.Equal(t, 1.5, rdb.ZAddArgsIncr(ctx, "ztmp", redis.ZAddArgs{NX: true, Members: []redis.Z{{Member: "abc", Score: 1.5}}}).Val()) + require.Equal(t, 0.0, rdb.ZAddArgsIncr(ctx, "ztmp", redis.ZAddArgs{LT: true, Members: []redis.Z{{Member: "abc", Score: -1.5}}}).Val()) + require.Equal(t, 0.0, rdb.ZAddArgsIncr(ctx, "ztmp", redis.ZAddArgs{LT: true, Members: []redis.Z{{Member: "abc", Score: 1.5}}}).Val()) + require.Equal(t, redis.Nil, rdb.ZAddArgsIncr(ctx, "ztmp", redis.ZAddArgs{LT: true, Members: []redis.Z{{Member: "abc", Score: 1.5}}}).Err()) + }) + + t.Run(fmt.Sprintf("ZSET ZADD LT/GT with other options - %s", encoding), func(t *testing.T) { + rdb.Del(ctx, "ztmp") + require.EqualValues(t, 1, rdb.ZAddArgs(ctx, "ztmp", redis.ZAddArgs{Members: []redis.Z{{Member: "abc", Score: 1.5}}}).Val()) + require.EqualValues(t, 1, rdb.ZAddArgs(ctx, "ztmp", redis.ZAddArgs{GT: true, Ch: true, Members: []redis.Z{{Member: "abc", Score: 2.5}}}).Val()) + require.EqualValues(t, 0, rdb.ZAddArgs(ctx, "ztmp", redis.ZAddArgs{GT: true, Ch: true, Members: []redis.Z{{Member: "abc", Score: 2.5}}}).Val()) + require.EqualValues(t, 0, rdb.ZAddArgs(ctx, "ztmp", redis.ZAddArgs{GT: true, Ch: false, Members: []redis.Z{{Member: "abc", Score: 2.5}}}).Val()) + require.EqualValues(t, 0, rdb.ZAddArgs(ctx, "ztmp", redis.ZAddArgs{GT: true, Ch: false, Members: []redis.Z{{Member: "abc", Score: 100}}}).Val()) + require.Contains(t, rdb.Do(ctx, "zadd", "ztmp", "lt", "gt", "1", "m1", "2", "m2").Err(), + "GT, LT, and/or NX options at the same time are not compatible") + + rdb.Del(ctx, "ztmp") + require.EqualValues(t, 1, rdb.ZAddArgs(ctx, "ztmp", redis.ZAddArgs{LT: true, Ch: true, Members: []redis.Z{{Member: "abc", Score: 1.5}}}).Val()) + require.EqualValues(t, 1, rdb.ZAddArgs(ctx, "ztmp", redis.ZAddArgs{LT: true, Ch: true, Members: []redis.Z{{Member: "abc", Score: 1.2}}}).Val()) + require.EqualValues(t, 0, rdb.ZAddArgs(ctx, "ztmp", redis.ZAddArgs{LT: true, Ch: false, Members: []redis.Z{{Member: "abc", Score: 0.5}}}).Val()) + + rdb.Del(ctx, "newAbc1", "newAbc2") + require.EqualValues(t, 2, rdb.ZAddArgs(ctx, "ztmp", redis.ZAddArgs{Ch: true, Members: []redis.Z{{Member: "abc", Score: 0.5}, {Member: "newAbc1", Score: 10}, {Member: "newAbc2"}}}).Val()) + }) + + t.Run(fmt.Sprintf("ZSET ZADD NX/XX option supports a single pair - %s", encoding), func(t *testing.T) { + rdb.Del(ctx, "ztmp") + require.EqualValues(t, 2, rdb.ZAddArgs(ctx, "ztmp", redis.ZAddArgs{NX: true, Members: []redis.Z{{Member: "a", Score: 1}, {Member: "b", Score: 2}}}).Val()) + require.EqualValues(t, 1, rdb.ZAddArgs(ctx, "ztmp", redis.ZAddArgs{NX: true, Members: []redis.Z{{Member: "c", Score: 3}}}).Val()) + + rdb.Del(ctx, "ztmp") + require.EqualValues(t, 1, rdb.ZAddArgs(ctx, "ztmp", redis.ZAddArgs{NX: true, Members: []redis.Z{{Member: "abc", Score: 1.5}}}).Val()) + require.EqualValues(t, 0, rdb.ZAddArgs(ctx, "ztmp", redis.ZAddArgs{NX: true, Members: []redis.Z{{Member: "abc", Score: 1.5}}}).Val()) + require.EqualValues(t, 1, rdb.ZAddArgs(ctx, "ztmp", redis.ZAddArgs{XX: true, Ch: true, Members: []redis.Z{{Member: "abc", Score: 2.5}}}).Val()) + require.EqualValues(t, 0, rdb.ZAddArgs(ctx, "ztmp", redis.ZAddArgs{XX: true, Ch: true, Members: []redis.Z{{Member: "abc", Score: 2.5}}}).Val()) + require.Contains(t, rdb.Do(ctx, "zadd", "ztmp", "nx", "xx", "1", "m1", "2", "m2").Err(), + "XX and NX options at the same time are not compatible") + + require.Contains(t, rdb.Do(ctx, "zadd", "ztmp", "lt", "nx", "1", "m1", "2", "m2").Err(), + "GT, LT, and/or NX options at the same time are not compatible") + require.Contains(t, rdb.Do(ctx, "zadd", "ztmp", "gt", "nx", "1", "m1", "2", "m2").Err(), + "GT, LT, and/or NX options at the same time are not compatible") + + rdb.Del(ctx, "ztmp") + require.EqualValues(t, 1, rdb.ZAddArgs(ctx, "ztmp", redis.ZAddArgs{NX: true, Ch: true, Members: []redis.Z{{Member: "abc", Score: 1.5}}}).Val()) + require.EqualValues(t, 0, rdb.ZAddArgs(ctx, "ztmp", redis.ZAddArgs{NX: true, Members: []redis.Z{{Member: "abc", Score: 1.5}}}).Val()) + }) + t.Run(fmt.Sprintf("ZSET element can't be set to NaN with ZADD - %s", encoding), func(t *testing.T) { require.Contains(t, rdb.ZAdd(ctx, "myzset", redis.Z{Score: math.NaN(), Member: "abc"}).Err(), "float") })