This is an automated email from the ASF dual-hosted git repository.

twice pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks.git


The following commit(s) were added to refs/heads/unstable by this push:
     new 6148f9bcc feat(string): Implement DELEX Command (#3317)
6148f9bcc is described below

commit 6148f9bccfd81a36213cd5427bac00b74427d875
Author: Valay Saitwadekar <[email protected]>
AuthorDate: Sat Jan 3 01:59:55 2026 -0800

    feat(string): Implement DELEX Command (#3317)
    
    Fixes #3310
    
    Implement DELEX Command
    
    Digest function commented out
    
    ---------
    
    Co-authored-by: hulk <[email protected]>
    Co-authored-by: Twice <[email protected]>
---
 src/commands/cmd_string.cc                     |  46 ++++++++
 src/types/redis_string.cc                      |  36 ++++++
 src/types/redis_string.h                       |  10 ++
 tests/cppunit/types/string_test.cc             | 147 +++++++++++++++++++++++++
 tests/gocase/unit/type/strings/strings_test.go |  73 ++++++++++++
 5 files changed, 312 insertions(+)

diff --git a/src/commands/cmd_string.cc b/src/commands/cmd_string.cc
index 9e3bf8aef..fd43fda72 100644
--- a/src/commands/cmd_string.cc
+++ b/src/commands/cmd_string.cc
@@ -108,6 +108,51 @@ 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 {
@@ -812,6 +857,7 @@ 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),
diff --git a/src/types/redis_string.cc b/src/types/redis_string.cc
index 273e51ec4..f7b267288 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<std::string> &old_value) {
   auto s =
diff --git a/src/types/redis_string.h b/src/types/redis_string.h
index 4e7bdfeac..b160d3d6d 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<uint64_t> 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<std::string> &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 1b8741767..f30ef1622 100644
--- a/tests/cppunit/types/string_test.cc
+++ b/tests/cppunit/types/string_test.cc
@@ -22,6 +22,7 @@
 
 #include <memory>
 
+#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 584030e65..c1a684847 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())

Reply via email to