Skip to content
Closed
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
67 changes: 20 additions & 47 deletions src/commands/cmd_string.cc
Original file line number Diff line number Diff line change
Expand Up @@ -108,51 +108,6 @@ class CommandGetEx : public Commander {
std::optional<uint64_t> expire_;
};

class CommandDelEX : public Commander {
public:
Status Parse(const std::vector<std::string> &args) override {
if (args.size() > 4) {
return {Status::RedisParseErr, errWrongNumOfArguments};
}

CommandParser parser(args, 2);
while (parser.Good()) {
if (parser.EatEqICase("ifdeq")) {
option_ = {DelExOption::IFDEQ, GET_OR_RET(parser.TakeStr())};
} else if (parser.EatEqICase("ifdne")) {
option_ = {DelExOption::IFDNE, GET_OR_RET(parser.TakeStr())};
} else if (parser.EatEqICase("ifeq")) {
option_ = {DelExOption::IFEQ, GET_OR_RET(parser.TakeStr())};
} else if (parser.EatEqICase("ifne")) {
option_ = {DelExOption::IFNE, GET_OR_RET(parser.TakeStr())};
} else {
return {Status::RedisParseErr, errInvalidSyntax};
}
}
return Status::OK();
}

Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
redis::String string_db(srv->storage, conn->GetNamespace());
bool deleted = false;
auto s = string_db.DelEX(ctx, args_[1], option_, deleted);

if (!s.ok() && !s.IsNotFound()) {
return {Status::RedisExecErr, s.ToString()};
}

if (s.IsNotFound() || !deleted) {
*output = redis::Integer(0);
} else {
*output = redis::Integer(1);
}
return Status::OK();
}

private:
DelExOption option_;
};

class CommandStrlen : public Commander {
public:
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
Expand Down Expand Up @@ -344,6 +299,24 @@ 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());
if (cmp_value_.size() != 16) {
return {Status::RedisParseErr, "ERR digest must be exactly 16 hexadecimal characters"};
}
} else if (parser.EatEqICaseFlag("IFDNE", set_flag)) {
set_flag_ = StringSetType::IFDNE;
cmp_value_ = GET_OR_RET(parser.TakeStr());
if (cmp_value_.size() != 16) {
return {Status::RedisParseErr, "ERR digest must be exactly 16 hexadecimal characters"};
}
} else if (parser.EatEqICase("GET")) {
get_ = true;
} else {
Expand All @@ -358,7 +331,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 +358,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 Expand Up @@ -857,7 +831,6 @@ REDIS_REGISTER_COMMANDS(
MakeCmdAttr<CommandGetRange>("getrange", 4, "read-only", 1, 1, 1),
MakeCmdAttr<CommandSubStr>("substr", 4, "read-only", 1, 1, 1),
MakeCmdAttr<CommandGetDel>("getdel", 2, "write no-dbsize-check", 1, 1, 1),
MakeCmdAttr<CommandDelEX>("delex", -2, "write", 1, 1, 1),
MakeCmdAttr<CommandSetRange>("setrange", 4, "write", 1, 1, 1),
MakeCmdAttr<CommandMGet>("mget", -2, "read-only", 1, -1, 1),
MakeCmdAttr<CommandAppend>("append", 3, "write", 1, 1, 1), MakeCmdAttr<CommandSet>("set", -3, "write", 1, 1, 1),
Expand Down
87 changes: 51 additions & 36 deletions src/types/redis_string.cc
Original file line number Diff line number Diff line change
Expand Up @@ -183,41 +183,6 @@ rocksdb::Status String::GetEx(engine::Context &ctx, const std::string &user_key,
return rocksdb::Status::OK();
}

rocksdb::Status String::DelEX(engine::Context &ctx, const std::string &user_key, const DelExOption &option,
bool &deleted) {
deleted = false;
std::string val;
std::string ns_key = AppendNamespacePrefix(user_key);
rocksdb::Status s = getValue(ctx, ns_key, &val);
if (!s.ok()) return s;

bool matched = false;
switch (option.type) {
case DelExOption::NONE:
matched = true;
break;
case DelExOption::IFDEQ:
matched = option.value == util::StringDigest(val);
break;
case DelExOption::IFDNE:
matched = option.value != util::StringDigest(val);
break;
case DelExOption::IFEQ:
matched = option.value == val;
break;
case DelExOption::IFNE:
matched = option.value != val;
break;
default:
return rocksdb::Status::InvalidArgument();
}
if (matched) {
s = storage_->Delete(ctx, storage_->DefaultWriteOptions(), metadata_cf_handle_, ns_key);
deleted = s.ok();
}
return s;
}

rocksdb::Status String::GetSet(engine::Context &ctx, const std::string &user_key, const std::string &new_value,
std::optional<std::string> &old_value) {
auto s =
Expand All @@ -239,7 +204,7 @@ rocksdb::Status String::Set(engine::Context &ctx, const std::string &user_key, c
}

rocksdb::Status String::Set(engine::Context &ctx, const std::string &user_key, const std::string &value,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is getting a bit complex. You could try refactoring it in this PR, or open a new PR to refactor it after this one is merged.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have pushed a new commit that addresses all of them:
Uppercase & Malformed Digests: Added the tests and updated the C++ logic (util::EqualICase and length validation) to handle them correctly.
NX + GET on Wrong Type: I found that the original C++ logic was short-circuiting and returning nil when NX failed, bypassing the GET type check. I have fixed this in redis_string.cc so it now properly returns a WRONGTYPE error, exactly matching Redis standard behavior. The tests pass beautifully now.
Regarding the String::Set refactoring: to keep this PR focused and safe, I will open a separate PR to refactor that function after this one is merged.

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 +214,24 @@ rocksdb::Status String::Set(engine::Context &ctx, const std::string &user_key, c
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;
return rocksdb::Status::OK();
}
// If GET option is set, continue to let GET handling return WRONGTYPE error.
}
// 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 +254,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::EqualICase(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::EqualICase(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
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
Loading