Skip to content
14 changes: 6 additions & 8 deletions src/cli/main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,12 @@ struct NewOpt {
static void PrintUsage(const char *program, std::ostream &os = std::cout) {
os << program << " implements the Redis protocol based on RocksDB" << std::endl
<< "Usage:" << std::endl
<< std::left << new_opt << "-c, --config <filename>"
<< "set config file to <filename>, or `-` for stdin" << std::endl
<< new_opt << "-v, --version"
<< "print version information" << std::endl
<< new_opt << "-h, --help"
<< "print this help message" << std::endl
<< new_opt << "--<config-key> <config-value>"
<< "overwrite specific config option <config-key> to <config-value>" << std::endl;
<< std::left << new_opt << "-c, --config <filename>" << "set config file to <filename>, or `-` for stdin"
<< std::endl
<< new_opt << "-v, --version" << "print version information" << std::endl
<< new_opt << "-h, --help" << "print this help message" << std::endl
<< new_opt << "--<config-key> <config-value>" << "overwrite specific config option <config-key> to <config-value>"
<< std::endl;
}

static CLIOptions ParseCommandLineOptions(int argc, char **argv) {
Expand Down
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
49 changes: 48 additions & 1 deletion src/types/redis_string.cc
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@
}

rocksdb::Status String::Set(engine::Context &ctx, const std::string &user_key, const std::string &value,
StringSetArgs args, std::optional<std::string> &ret) {
const StringSetArgs &args, std::optional<std::string> &ret) {
uint64_t expire = 0;
std::string ns_key = AppendNamespacePrefix(user_key);

Expand All @@ -249,6 +249,21 @@
uint64_t old_expire = 0;
auto s = getValueAndExpire(ctx, ns_key, &old_value, &old_expire);
if (!s.ok() && !s.IsNotFound() && !s.IsInvalidArgument()) return s;
// If the existing key is not a string type, enforce expected behaviors:
if (s.IsInvalidArgument()) {
// For conditional comparisons (IFEQ/IFNE/IFDEQ/IFDNE), reading the old value is required,
// so return the underlying WRONGTYPE (InvalidArgument) error.
if (args.type == StringSetType::IFEQ || args.type == StringSetType::IFNE || args.type == StringSetType::IFDEQ ||
args.type == StringSetType::IFDNE) {
return s;
}
// For NX option, treat a wrong type as "key exists" so the condition is not met.
if (args.type == StringSetType::NX) {
if (!args.get) ret = std::nullopt;

Check failure on line 262 in src/types/redis_string.cc

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=apache_kvrocks&issues=AZ2PQyfL6OZC7Je6Ihbn&open=AZ2PQyfL6OZC7Je6Ihbn&pullRequest=3393
return rocksdb::Status::OK();
}
// For other options, continue (e.g., XX may still proceed since key exists).
}
// GET option
if (args.get) {
if (s.IsInvalidArgument()) {
Expand All @@ -271,6 +286,38 @@
// 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) {

Check warning on line 292 in src/types/redis_string.cc

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "matched" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=apache_kvrocks&issues=AZ2PQyfL6OZC7Je6Ihbj&open=AZ2PQyfL6OZC7Je6Ihbj&pullRequest=3393
if (!args.get) ret = std::nullopt;

Check failure on line 293 in src/types/redis_string.cc

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=apache_kvrocks&issues=AZ2PQyfL6OZC7Je6Ihbo&open=AZ2PQyfL6OZC7Je6Ihbo&pullRequest=3393
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) {

Check warning on line 300 in src/types/redis_string.cc

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "not_matched" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=apache_kvrocks&issues=AZ2PQyfL6OZC7Je6Ihbk&open=AZ2PQyfL6OZC7Je6Ihbk&pullRequest=3393
if (!args.get) ret = std::nullopt;

Check failure on line 301 in src/types/redis_string.cc

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=apache_kvrocks&issues=AZ2PQyfL6OZC7Je6Ihbp&open=AZ2PQyfL6OZC7Je6Ihbp&pullRequest=3393
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) {

Check warning on line 308 in src/types/redis_string.cc

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "matched" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=apache_kvrocks&issues=AZ2PQyfL6OZC7Je6Ihbl&open=AZ2PQyfL6OZC7Je6Ihbl&pullRequest=3393
if (!args.get) ret = std::nullopt;

Check failure on line 309 in src/types/redis_string.cc

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=apache_kvrocks&issues=AZ2PQyfL6OZC7Je6Ihbq&open=AZ2PQyfL6OZC7Je6Ihbq&pullRequest=3393
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) {

Check warning on line 316 in src/types/redis_string.cc

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "not_matched" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=apache_kvrocks&issues=AZ2PQyfL6OZC7Je6Ihbm&open=AZ2PQyfL6OZC7Je6Ihbm&pullRequest=3393
if (!args.get) ret = std::nullopt;

Check failure on line 317 in src/types/redis_string.cc

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=apache_kvrocks&issues=AZ2PQyfL6OZC7Je6Ihbr&open=AZ2PQyfL6OZC7Je6Ihbr&pullRequest=3393
return rocksdb::Status::OK();
}
if (!args.get) ret = "";
} else {
// if GET option not given, make ret not nil
if (!args.get) ret = "";
Expand Down
7 changes: 4 additions & 3 deletions 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 Expand Up @@ -103,8 +104,8 @@ class String : public Database {
std::optional<std::string> &old_value);
rocksdb::Status GetDel(engine::Context &ctx, const std::string &user_key, std::string *value);
rocksdb::Status Set(engine::Context &ctx, const std::string &user_key, const std::string &value);
rocksdb::Status Set(engine::Context &ctx, const std::string &user_key, const std::string &value, StringSetArgs args,
std::optional<std::string> &ret);
rocksdb::Status Set(engine::Context &ctx, const std::string &user_key, const std::string &value,
const StringSetArgs &args, std::optional<std::string> &ret);
rocksdb::Status SetEX(engine::Context &ctx, const std::string &user_key, const std::string &value,
uint64_t expire_ms);
rocksdb::Status SetNX(engine::Context &ctx, const std::string &user_key, const std::string &value, uint64_t expire_ms,
Expand Down
6 changes: 2 additions & 4 deletions tests/cppunit/config_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,8 @@ TEST(Config, LoadRocksDBDictionaryCompressionOptions) {
unlink(path);

std::ofstream output_file(path, std::ios::out);
output_file << "rocksdb.compression_max_dict_bytes 16384"
<< "\n";
output_file << "rocksdb.compression_zstd_max_train_bytes 262144"
<< "\n";
output_file << "rocksdb.compression_max_dict_bytes 16384" << "\n";
output_file << "rocksdb.compression_zstd_max_train_bytes 262144" << "\n";
output_file.close();

Config config;
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 @@ -613,3 +613,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
Loading