diff --git a/src/commands/cmd_string.cc b/src/commands/cmd_string.cc index 9e3bf8aef11..fd43fda7263 100644 --- a/src/commands/cmd_string.cc +++ b/src/commands/cmd_string.cc @@ -108,6 +108,51 @@ class CommandGetEx : public Commander { std::optional expire_; }; +class CommandDelEX : public Commander { + public: + Status Parse(const std::vector &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 { @@ -812,6 +857,7 @@ REDIS_REGISTER_COMMANDS( MakeCmdAttr("getrange", 4, "read-only", 1, 1, 1), MakeCmdAttr("substr", 4, "read-only", 1, 1, 1), MakeCmdAttr("getdel", 2, "write no-dbsize-check", 1, 1, 1), + MakeCmdAttr("delex", -2, "write", 1, 1, 1), MakeCmdAttr("setrange", 4, "write", 1, 1, 1), MakeCmdAttr("mget", -2, "read-only", 1, -1, 1), MakeCmdAttr("append", 3, "write", 1, 1, 1), MakeCmdAttr("set", -3, "write", 1, 1, 1), diff --git a/src/types/redis_string.cc b/src/types/redis_string.cc index 273e51ec432..f7b26728803 100644 --- a/src/types/redis_string.cc +++ b/src/types/redis_string.cc @@ -29,6 +29,7 @@ #include "common/string_util.h" #include "parse_util.h" #include "storage/redis_metadata.h" +#include "string_util.h" #include "time_util.h" namespace redis { @@ -182,6 +183,41 @@ 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 &old_value) { auto s = diff --git a/src/types/redis_string.h b/src/types/redis_string.h index 4e7bdfeac59..b160d3d6da3 100644 --- a/src/types/redis_string.h +++ b/src/types/redis_string.h @@ -34,6 +34,15 @@ struct StringPair { Slice value; }; +struct DelExOption { + enum Type { NONE, IFDEQ, IFDNE, IFEQ, IFNE }; + Type type; + std::string value; + + DelExOption() : type(NONE) {} + DelExOption(Type type, std::string value) : type(type), value(std::move(value)) {} +}; + enum class StringSetType { NONE, NX, XX }; struct StringSetArgs { @@ -89,6 +98,7 @@ class String : public Database { rocksdb::Status Get(engine::Context &ctx, const std::string &user_key, std::string *value); rocksdb::Status GetEx(engine::Context &ctx, const std::string &user_key, std::string *value, std::optional expire); + rocksdb::Status DelEX(engine::Context &ctx, const std::string &user_key, const DelExOption &option, bool &deleted); rocksdb::Status GetSet(engine::Context &ctx, const std::string &user_key, const std::string &new_value, std::optional &old_value); rocksdb::Status GetDel(engine::Context &ctx, const std::string &user_key, std::string *value); diff --git a/tests/cppunit/types/string_test.cc b/tests/cppunit/types/string_test.cc index 1b874176720..f30ef162222 100644 --- a/tests/cppunit/types/string_test.cc +++ b/tests/cppunit/types/string_test.cc @@ -22,6 +22,7 @@ #include +#include "string_util.h" #include "test_base.h" #include "time_util.h" #include "types/redis_string.h" @@ -156,6 +157,152 @@ TEST_F(RedisStringTest, GetSet) { } auto s = string_->Del(*ctx_, key_); } + +TEST_F(RedisStringTest, DelEX) { + DelExOption option = {DelExOption::NONE, ""}; + bool deleted = false; + + std::string key = "test-string-key69"; + std::string value = "test-strings-value69"; + auto status = string_->Set(*ctx_, key, value); + ASSERT_TRUE(status.ok()); + status = string_->Get(*ctx_, key, &value); + ASSERT_TRUE(status.ok() && !status.IsNotFound()); + EXPECT_EQ("test-strings-value69", value); + + // Check no args delete works + auto s = string_->DelEX(*ctx_, key, option, deleted); + EXPECT_TRUE(s.ok()); + EXPECT_FALSE(s.IsNotFound()); + EXPECT_TRUE(deleted); + EXPECT_EQ(option.type, DelExOption::NONE); + status = string_->Get(*ctx_, key, &value); + EXPECT_TRUE(!status.ok() && status.IsNotFound()); + EXPECT_NE("test-strings-value69", value); + + // Check no args delete on same key + s = string_->DelEX(*ctx_, key, option, deleted); + EXPECT_TRUE(s.IsNotFound()); + EXPECT_FALSE(deleted); + + // Check no args delete on invalid/notfound key + key = "random"; + s = string_->DelEX(*ctx_, key, option, deleted); + EXPECT_TRUE(s.IsNotFound()); + EXPECT_FALSE(deleted); + status = string_->Get(*ctx_, key, &value); + EXPECT_TRUE(!status.ok() && status.IsNotFound()); + + // Checking true false cases for all args + key = "test-string-key69"; + value = "test-strings-value69"; + status = string_->Set(*ctx_, key, value); + EXPECT_TRUE(status.ok()); + option.type = DelExOption::IFDEQ; + option.value = "xxxxxxxxxxxxxxxx"; + deleted = false; + s = string_->DelEX(*ctx_, key, option, deleted); + EXPECT_TRUE(s.ok()); + EXPECT_FALSE(s.IsNotFound()); + EXPECT_FALSE(deleted); + status = string_->Get(*ctx_, key, &value); + EXPECT_TRUE(status.ok() && !status.IsNotFound()); + EXPECT_EQ("test-strings-value69", value); + + option.type = DelExOption::IFDEQ; + option.value = util::StringDigest(value); + deleted = false; + s = string_->DelEX(*ctx_, key, option, deleted); + EXPECT_TRUE(s.ok()); + EXPECT_FALSE(s.IsNotFound()); + EXPECT_TRUE(deleted); + status = string_->Get(*ctx_, key, &value); + EXPECT_TRUE(!status.ok()); + EXPECT_TRUE(status.IsNotFound()); + EXPECT_NE("test-strings-value69", value); + + key = "test-string-key69"; + value = "test-strings-value69"; + status = string_->Set(*ctx_, key, value); + EXPECT_TRUE(status.ok()); + option.type = DelExOption::IFDNE; + option.value = util::StringDigest(value); + deleted = false; + s = string_->DelEX(*ctx_, key, option, deleted); + EXPECT_TRUE(s.ok()); + EXPECT_FALSE(s.IsNotFound()); + EXPECT_FALSE(deleted); + status = string_->Get(*ctx_, key, &value); + EXPECT_TRUE(status.ok() && !status.IsNotFound()); + EXPECT_EQ("test-strings-value69", value); + + option.type = DelExOption::IFDNE; + option.value = "xxxxxxxxxxxxxxxx"; + deleted = false; + s = string_->DelEX(*ctx_, key, option, deleted); + EXPECT_TRUE(s.ok()); + EXPECT_FALSE(s.IsNotFound()); + EXPECT_TRUE(deleted); + status = string_->Get(*ctx_, key, &value); + EXPECT_TRUE(!status.ok()); + EXPECT_TRUE(status.IsNotFound()); + EXPECT_NE("test-strings-value69", value); + + key = "test-string-key69"; + value = "test-strings-value69"; + status = string_->Set(*ctx_, key, value); + EXPECT_TRUE(status.ok()); + option.type = DelExOption::IFEQ; + option.value = "random"; + deleted = false; + s = string_->DelEX(*ctx_, key, option, deleted); + EXPECT_TRUE(s.ok()); + EXPECT_FALSE(s.IsNotFound()); + EXPECT_FALSE(deleted); + status = string_->Get(*ctx_, key, &value); + EXPECT_TRUE(status.ok() && !status.IsNotFound()); + EXPECT_EQ("test-strings-value69", value); + + option.type = DelExOption::IFEQ; + option.value = "test-strings-value69"; + deleted = false; + s = string_->DelEX(*ctx_, key, option, deleted); + EXPECT_TRUE(s.ok()); + EXPECT_FALSE(s.IsNotFound()); + EXPECT_TRUE(deleted); + status = string_->Get(*ctx_, key, &value); + EXPECT_TRUE(!status.ok()); + EXPECT_TRUE(status.IsNotFound()); + EXPECT_NE("test-strings-value69", value); + + key = "test-string-key69"; + value = "test-strings-value69"; + status = string_->Set(*ctx_, key, value); + EXPECT_TRUE(status.ok()); + option.type = DelExOption::IFNE; + option.value = "test-strings-value69"; + deleted = false; + s = string_->DelEX(*ctx_, key, option, deleted); + EXPECT_TRUE(s.ok()); + EXPECT_FALSE(s.IsNotFound()); + EXPECT_FALSE(deleted); + status = string_->Get(*ctx_, key, &value); + EXPECT_TRUE(status.ok() && !status.IsNotFound()); + EXPECT_EQ("test-strings-value69", value); + + option.type = DelExOption::IFNE; + option.value = "random"; + deleted = false; + s = string_->DelEX(*ctx_, key, option, deleted); + EXPECT_TRUE(s.ok()); + EXPECT_FALSE(s.IsNotFound()); + EXPECT_TRUE(deleted); + status = string_->Get(*ctx_, key, &value); + EXPECT_TRUE(!status.ok()); + EXPECT_TRUE(status.IsNotFound()); + EXPECT_NE("test-strings-value69", value); +} + TEST_F(RedisStringTest, GetDel) { for (auto &pair : pairs_) { string_->Set(*ctx_, pair.key.ToString(), pair.value.ToString()); diff --git a/tests/gocase/unit/type/strings/strings_test.go b/tests/gocase/unit/type/strings/strings_test.go index 584030e65da..c1a68484754 100644 --- a/tests/gocase/unit/type/strings/strings_test.go +++ b/tests/gocase/unit/type/strings/strings_test.go @@ -277,6 +277,79 @@ func testString(t *testing.T, configs util.KvrocksServerConfigs) { require.Equal(t, "", rdb.GetDel(ctx, "foo").Val()) }) + t.Run("DelEX command no args", func(t *testing.T) { + key := "test-string-key69" + value := "test-strings-value69" + require.NoError(t, rdb.Set(ctx, key, value, 0).Err()) + require.Equal(t, value, rdb.Get(ctx, key).Val()) + + require.Equal(t, int64(1), rdb.Do(ctx, "DELEX", key).Val()) + require.Equal(t, "", rdb.Get(ctx, key).Val()) + + require.NoError(t, rdb.Do(ctx, "DelEX", key).Err()) + require.Equal(t, int64(0), rdb.Do(ctx, "DelEX", key).Val()) + + require.Equal(t, "", rdb.Get(ctx, "random").Val()) + require.NoError(t, rdb.Do(ctx, "DelEX", "random").Err()) + require.Equal(t, int64(0), rdb.Do(ctx, "DELEX", "random").Val()) + }) + + t.Run("DelEX command with args", func(t *testing.T) { + key := "test-string-key69" + value := "Hello world" + require.NoError(t, rdb.Set(ctx, key, value, 0).Err()) + require.Equal(t, value, rdb.Get(ctx, key).Val()) + + r := rdb.Do(ctx, "DelEX", key, "random", "random", "random").Err() + require.ErrorContains(t, r, "wrong number") + + r = rdb.Do(ctx, "DelEX", key, "random", "random").Err() + require.ErrorContains(t, r, "syntax error") + + digest := "b6acb9d84a38ff74" + require.NoError(t, rdb.Do(ctx, "DelEX", key, "ifdeq", "xxxxxxxxxxxxxxxx").Err()) + require.Equal(t, int64(0), rdb.Do(ctx, "DelEX", key, "ifdeq", "xxxxxxxxxxxxxxxx").Val()) + require.Equal(t, value, rdb.Get(ctx, key).Val()) + require.NoError(t, rdb.Do(ctx, "DelEX", key, "ifdeq", digest).Err()) + require.Equal(t, "", rdb.Get(ctx, value).Val()) + require.NoError(t, rdb.Set(ctx, key, value, 0).Err()) + require.Equal(t, int64(1), rdb.Do(ctx, "DELEX", key, "ifdeq", digest).Val()) + require.Equal(t, "", rdb.Get(ctx, value).Val()) + + require.NoError(t, rdb.Set(ctx, key, value, 0).Err()) + require.Equal(t, value, rdb.Get(ctx, key).Val()) + require.NoError(t, rdb.Do(ctx, "DelEX", key, "ifdne", digest).Err()) + require.Equal(t, int64(0), rdb.Do(ctx, "DelEX", key, "ifdne", digest).Val()) + require.Equal(t, value, rdb.Get(ctx, key).Val()) + require.NoError(t, rdb.Do(ctx, "DelEX", key, "ifdne", "xxxxxxxxxxxxxxxx").Err()) + require.Equal(t, "", rdb.Get(ctx, value).Val()) + require.NoError(t, rdb.Set(ctx, key, value, 0).Err()) + require.Equal(t, int64(1), rdb.Do(ctx, "DelEX", key, "ifdne", "xxxxxxxxxxxxxxxx").Val()) + require.Equal(t, "", rdb.Get(ctx, value).Val()) + + require.NoError(t, rdb.Set(ctx, key, value, 0).Err()) + require.Equal(t, value, rdb.Get(ctx, key).Val()) + require.NoError(t, rdb.Do(ctx, "DelEX", key, "ifeq", "random").Err()) + require.Equal(t, int64(0), rdb.Do(ctx, "DelEX", key, "ifeq", "random").Val()) + require.Equal(t, value, rdb.Get(ctx, key).Val()) + require.NoError(t, rdb.Do(ctx, "DelEX", key, "ifeq", value).Err()) + require.Equal(t, "", rdb.Get(ctx, value).Val()) + require.NoError(t, rdb.Set(ctx, key, value, 0).Err()) + require.Equal(t, int64(1), rdb.Do(ctx, "DelEX", key, "ifeq", value).Val()) + require.Equal(t, "", rdb.Get(ctx, value).Val()) + + require.NoError(t, rdb.Set(ctx, key, value, 0).Err()) + require.Equal(t, value, rdb.Get(ctx, key).Val()) + require.NoError(t, rdb.Do(ctx, "DelEX", key, "ifne", value).Err()) + require.Equal(t, int64(0), rdb.Do(ctx, "DelEX", key, "ifne", value).Val()) + require.Equal(t, value, rdb.Get(ctx, key).Val()) + require.NoError(t, rdb.Do(ctx, "DelEX", key, "ifne", "random").Err()) + require.Equal(t, "", rdb.Get(ctx, value).Val()) + require.NoError(t, rdb.Set(ctx, key, value, 0).Err()) + require.Equal(t, int64(1), rdb.Do(ctx, "DelEX", key, "ifne", "random").Val()) + require.Equal(t, "", rdb.Get(ctx, value).Val()) + }) + t.Run("MGET command", func(t *testing.T) { require.NoError(t, rdb.FlushDB(ctx).Err()) require.NoError(t, rdb.Set(ctx, "foo", "BAR", 0).Err())