Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/commands/cmd_string.cc
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,18 @@ class CommandSet : public Commander {
set_flag_ = StringSetType::NX;
} else if (parser.EatEqICaseFlag("XX", set_flag)) {
set_flag_ = StringSetType::XX;
} else if (parser.EatEqICaseFlag("IFEQ", set_flag)) {
set_flag_ = StringSetType::IFEQ;
cmp_value_ = GET_OR_RET(parser.TakeStr());
} else if (parser.EatEqICaseFlag("IFNE", set_flag)) {
set_flag_ = StringSetType::IFNE;
cmp_value_ = GET_OR_RET(parser.TakeStr());
} else if (parser.EatEqICaseFlag("IFDEQ", set_flag)) {
set_flag_ = StringSetType::IFDEQ;
cmp_value_ = GET_OR_RET(parser.TakeStr());
} else if (parser.EatEqICaseFlag("IFDNE", set_flag)) {
set_flag_ = StringSetType::IFDNE;
cmp_value_ = GET_OR_RET(parser.TakeStr());
} else if (parser.EatEqICase("GET")) {
get_ = true;
} else {
Expand All @@ -358,7 +370,7 @@ class CommandSet : public Commander {
std::optional<std::string> ret;
redis::String string_db(srv->storage, conn->GetNamespace());

rocksdb::Status s = string_db.Set(ctx, args_[1], args_[2], {expire_, set_flag_, get_, keep_ttl_}, ret);
rocksdb::Status s = string_db.Set(ctx, args_[1], args_[2], {expire_, set_flag_, get_, keep_ttl_, cmp_value_}, ret);

if (!s.ok()) {
return {Status::RedisExecErr, s.ToString()};
Expand All @@ -385,6 +397,7 @@ class CommandSet : public Commander {
bool get_ = false;
bool keep_ttl_ = false;
StringSetType set_flag_ = StringSetType::NONE;
std::string cmp_value_;
};

class CommandSetEX : public Commander {
Expand Down
32 changes: 32 additions & 0 deletions src/types/redis_string.cc
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,38 @@ rocksdb::Status String::Set(engine::Context &ctx, const std::string &user_key, c
// if XX option given, the key didn't exist before: return nil
if (!args.get) ret = std::nullopt;
return rocksdb::Status::OK();
} else if (args.type == StringSetType::IFEQ) {
// condition met only when key exists AND value matches
bool matched = s.ok() && (old_value == args.cmp_value);
if (!matched) {
if (!args.get) ret = std::nullopt;
return rocksdb::Status::OK();
}
if (!args.get) ret = "";
} else if (args.type == StringSetType::IFNE) {
// condition not met when key exists AND value matches; key-not-found counts as met
bool not_matched = s.ok() && (old_value == args.cmp_value);
if (not_matched) {
if (!args.get) ret = std::nullopt;
return rocksdb::Status::OK();
}
if (!args.get) ret = "";
} else if (args.type == StringSetType::IFDEQ) {
// condition met only when key exists AND digest matches
bool matched = s.ok() && (util::StringDigest(old_value) == args.cmp_value);
if (!matched) {
if (!args.get) ret = std::nullopt;
return rocksdb::Status::OK();
}
if (!args.get) ret = "";
} else if (args.type == StringSetType::IFDNE) {
// condition not met when key exists AND digest matches; key-not-found counts as met
bool not_matched = s.ok() && (util::StringDigest(old_value) == args.cmp_value);
if (not_matched) {
if (!args.get) ret = std::nullopt;
return rocksdb::Status::OK();
}
if (!args.get) ret = "";
} else {
// if GET option not given, make ret not nil
if (!args.get) ret = "";
Expand Down
3 changes: 2 additions & 1 deletion src/types/redis_string.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,15 @@ struct DelExOption {
DelExOption(Type type, std::string value) : type(type), value(std::move(value)) {}
};

enum class StringSetType { NONE, NX, XX };
enum class StringSetType { NONE, NX, XX, IFEQ, IFNE, IFDEQ, IFDNE };

struct StringSetArgs {
// Expire time in mill seconds.
uint64_t expire;
StringSetType type;
bool get;
bool keep_ttl;
std::string cmp_value; // valid only when type is IFEQ/IFNE/IFDEQ/IFDNE
};

struct StringMSetArgs {
Expand Down
219 changes: 219 additions & 0 deletions tests/cppunit/types/string_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -580,3 +580,222 @@ TEST_F(RedisStringTest, LCS) {
4},
std::get<StringLCSIdxResult>(rst));
}

TEST_F(RedisStringTest, SetIFEQ) {
std::string key = "ifeq-key";
std::string value = "hello";
std::optional<std::string> ret;

// key not found: condition not met, no write
auto s = string_->Set(*ctx_, key, "new", {0, StringSetType::IFEQ, false, false, value}, ret);
EXPECT_TRUE(s.ok());
EXPECT_FALSE(ret.has_value());
std::string got;
EXPECT_TRUE(string_->Get(*ctx_, key, &got).IsNotFound());

// set up the key
string_->Set(*ctx_, key, value);

// value matches: write succeeds
ret = std::nullopt;
s = string_->Set(*ctx_, key, "new", {0, StringSetType::IFEQ, false, false, value}, ret);
EXPECT_TRUE(s.ok());
EXPECT_TRUE(ret.has_value());
string_->Get(*ctx_, key, &got);
EXPECT_EQ("new", got);

// value mismatches: no write
ret = std::nullopt;
s = string_->Set(*ctx_, key, "newer", {0, StringSetType::IFEQ, false, false, "wrong"}, ret);
EXPECT_TRUE(s.ok());
EXPECT_FALSE(ret.has_value());
string_->Get(*ctx_, key, &got);
EXPECT_EQ("new", got);

EXPECT_TRUE(string_->Del(*ctx_, key).ok());
}

TEST_F(RedisStringTest, SetIFNE) {
std::string key = "ifne-key";
std::string value = "hello";
std::optional<std::string> ret;

// key not found: condition met (creates key)
auto s = string_->Set(*ctx_, key, "created", {0, StringSetType::IFNE, false, false, "anything"}, ret);
EXPECT_TRUE(s.ok());
EXPECT_TRUE(ret.has_value());
std::string got;
string_->Get(*ctx_, key, &got);
EXPECT_EQ("created", got);

// value matches: condition not met, no write
ret = std::nullopt;
s = string_->Set(*ctx_, key, "new", {0, StringSetType::IFNE, false, false, "created"}, ret);
EXPECT_TRUE(s.ok());
EXPECT_FALSE(ret.has_value());
string_->Get(*ctx_, key, &got);
EXPECT_EQ("created", got);

// value mismatches: write succeeds
ret = std::nullopt;
s = string_->Set(*ctx_, key, "updated", {0, StringSetType::IFNE, false, false, "wrong"}, ret);
EXPECT_TRUE(s.ok());
EXPECT_TRUE(ret.has_value());
string_->Get(*ctx_, key, &got);
EXPECT_EQ("updated", got);

EXPECT_TRUE(string_->Del(*ctx_, key).ok());
}

TEST_F(RedisStringTest, SetIFDEQ) {
std::string key = "ifdeq-key";
std::string value = "hello";
std::optional<std::string> ret;

// key not found: condition not met, no write
auto s = string_->Set(*ctx_, key, "new", {0, StringSetType::IFDEQ, false, false, util::StringDigest(value)}, ret);
EXPECT_TRUE(s.ok());
EXPECT_FALSE(ret.has_value());
std::string got;
EXPECT_TRUE(string_->Get(*ctx_, key, &got).IsNotFound());

// set up the key
string_->Set(*ctx_, key, value);

// digest matches: write succeeds
ret = std::nullopt;
s = string_->Set(*ctx_, key, "new", {0, StringSetType::IFDEQ, false, false, util::StringDigest(value)}, ret);
EXPECT_TRUE(s.ok());
EXPECT_TRUE(ret.has_value());
string_->Get(*ctx_, key, &got);
EXPECT_EQ("new", got);

// digest mismatches: no write
ret = std::nullopt;
s = string_->Set(*ctx_, key, "newer", {0, StringSetType::IFDEQ, false, false, "xxxxxxxxxxxxxxxx"}, ret);
EXPECT_TRUE(s.ok());
EXPECT_FALSE(ret.has_value());
string_->Get(*ctx_, key, &got);
EXPECT_EQ("new", got);

// empty string edge case: digest of "" is well-defined
string_->Set(*ctx_, key, "");
ret = std::nullopt;
s = string_->Set(*ctx_, key, "nonempty", {0, StringSetType::IFDEQ, false, false, util::StringDigest("")}, ret);
EXPECT_TRUE(s.ok());
EXPECT_TRUE(ret.has_value());
string_->Get(*ctx_, key, &got);
EXPECT_EQ("nonempty", got);

EXPECT_TRUE(string_->Del(*ctx_, key).ok());
}

TEST_F(RedisStringTest, SetIFDNE) {
std::string key = "ifdne-key";
std::string value = "hello";
std::optional<std::string> ret;

// key not found: condition met (creates key)
auto s = string_->Set(*ctx_, key, "created", {0, StringSetType::IFDNE, false, false, "xxxxxxxxxxxxxxxx"}, ret);
EXPECT_TRUE(s.ok());
EXPECT_TRUE(ret.has_value());
std::string got;
string_->Get(*ctx_, key, &got);
EXPECT_EQ("created", got);

// digest matches: condition not met, no write
ret = std::nullopt;
s = string_->Set(*ctx_, key, "new", {0, StringSetType::IFDNE, false, false, util::StringDigest("created")}, ret);
EXPECT_TRUE(s.ok());
EXPECT_FALSE(ret.has_value());
string_->Get(*ctx_, key, &got);
EXPECT_EQ("created", got);

// digest mismatches: write succeeds
ret = std::nullopt;
s = string_->Set(*ctx_, key, "updated", {0, StringSetType::IFDNE, false, false, "xxxxxxxxxxxxxxxx"}, ret);
EXPECT_TRUE(s.ok());
EXPECT_TRUE(ret.has_value());
string_->Get(*ctx_, key, &got);
EXPECT_EQ("updated", got);

EXPECT_TRUE(string_->Del(*ctx_, key).ok());
}

TEST_F(RedisStringTest, SetConditionalWithGET) {
std::string key = "cond-get-key";
std::string value = "original";
std::optional<std::string> ret;

string_->Set(*ctx_, key, value);

// IFEQ + GET, condition met: returns old value
ret = std::nullopt;
auto s = string_->Set(*ctx_, key, "new", {0, StringSetType::IFEQ, true, false, value}, ret);
EXPECT_TRUE(s.ok());
EXPECT_TRUE(ret.has_value());
EXPECT_EQ(value, ret.value());

// IFEQ + GET, condition not met: returns old value (GET always returns prev value)
ret = std::nullopt;
s = string_->Set(*ctx_, key, "newer", {0, StringSetType::IFEQ, true, false, "wrong"}, ret);
EXPECT_TRUE(s.ok());
EXPECT_TRUE(ret.has_value());
EXPECT_EQ("new", ret.value()); // old value returned even though condition not met

// IFNE + GET, condition met (value mismatches): returns old value
ret = std::nullopt;
s = string_->Set(*ctx_, key, "ifne-new", {0, StringSetType::IFNE, true, false, "wrong"}, ret);
EXPECT_TRUE(s.ok());
EXPECT_TRUE(ret.has_value());
EXPECT_EQ("new", ret.value()); // key was "new" before this SET

// key not found + IFEQ + GET: returns nullopt
EXPECT_TRUE(string_->Del(*ctx_, key).ok());
ret = std::nullopt;
s = string_->Set(*ctx_, key, "val", {0, StringSetType::IFEQ, true, false, "anything"}, ret);
EXPECT_TRUE(s.ok());
EXPECT_FALSE(ret.has_value());
}

TEST_F(RedisStringTest, SetConditionalWithTTL) {
std::string key = "cond-ttl-key";
std::string value = "hello";
std::optional<std::string> ret;

string_->Set(*ctx_, key, value);

// condition met + EX: TTL is set
uint64_t future_ms = util::GetTimeStampMS() + 5000;
ret = std::nullopt;
auto s = string_->Set(*ctx_, key, "new", {future_ms, StringSetType::IFEQ, false, false, value}, ret);
EXPECT_TRUE(s.ok());
EXPECT_TRUE(ret.has_value());
int64_t ttl = 0;
EXPECT_TRUE(string_->TTL(*ctx_, key, &ttl).ok());
EXPECT_GT(ttl, 3000);
EXPECT_LE(ttl, 6000);

// condition met + KEEPTTL: original TTL preserved
ret = std::nullopt;
s = string_->Set(*ctx_, key, "newer", {0, StringSetType::IFEQ, false, true, "new"}, ret);
EXPECT_TRUE(s.ok());
EXPECT_TRUE(ret.has_value());
int64_t ttl2 = 0;
EXPECT_TRUE(string_->TTL(*ctx_, key, &ttl2).ok());
EXPECT_GT(ttl2, 0);

// condition not met: TTL unchanged
int64_t ttl_before = 0;
EXPECT_TRUE(string_->TTL(*ctx_, key, &ttl_before).ok());
ret = std::nullopt;
s = string_->Set(*ctx_, key, "fail", {0, StringSetType::IFEQ, false, false, "wrong"}, ret);
EXPECT_TRUE(s.ok());
EXPECT_FALSE(ret.has_value());
int64_t ttl_after = 0;
EXPECT_TRUE(string_->TTL(*ctx_, key, &ttl_after).ok());
// TTL should still be positive and roughly the same
EXPECT_GT(ttl_after, 0);

EXPECT_TRUE(string_->Del(*ctx_, key).ok());
}
Loading