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

hulk 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 bf694106 Add support for JSON.STRAPPEND and JSON.STRLEN command (#1841)
bf694106 is described below

commit bf694106c4ca21722fc4a4d941ca31aaf6ee6996
Author: junxiangMu <[email protected]>
AuthorDate: Tue Dec 5 20:23:02 2023 +0800

    Add support for JSON.STRAPPEND and JSON.STRLEN command (#1841)
---
 src/commands/cmd_json.cc                 | 59 +++++++++++++++++++++++++++++++-
 src/types/json.h                         | 45 ++++++++++++++++++++++++
 src/types/redis_json.cc                  | 39 +++++++++++++++++++++
 src/types/redis_json.h                   |  3 ++
 tests/cppunit/types/json_test.cc         | 34 ++++++++++++++++++
 tests/gocase/unit/type/json/json_test.go | 24 +++++++++++++
 6 files changed, 203 insertions(+), 1 deletion(-)

diff --git a/src/commands/cmd_json.cc b/src/commands/cmd_json.cc
index a6c47ef1..6602cc9f 100644
--- a/src/commands/cmd_json.cc
+++ b/src/commands/cmd_json.cc
@@ -567,6 +567,61 @@ class CommandJsonNumMultBy : public Commander {
   }
 };
 
+class CommandJsonStrAppend : public Commander {
+ public:
+  Status Execute(Server *svr, Connection *conn, std::string *output) override {
+    redis::Json json(svr->storage, conn->GetNamespace());
+
+    // path default, if not provided
+    std::string path = "$";
+    if (args_.size() == 4) {
+      path = args_[2];
+    } else if (args_.size() > 4) {
+      return {Status::RedisExecErr, "The number of arguments is more than 
expected"};
+    }
+
+    std::vector<uint64_t> results;
+    auto s = json.StrAppend(args_[1], path, args_[3], results);
+    if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
+
+    *output = IntegerArray(results);
+    return Status::OK();
+  }
+
+  static std::string IntegerArray(const std::vector<uint64_t> &values) {
+    std::string result = "*" + std::to_string(values.size()) + CRLF;
+    for (const auto &value : values) {
+      if (value == std::numeric_limits<uint64_t>::max()) {
+        result += NilString();
+      } else {
+        result += Integer(value);
+      }
+    }
+    return result;
+  }
+};
+
+class CommandJsonStrLen : public Commander {
+ public:
+  Status Execute(Server *svr, Connection *conn, std::string *output) override {
+    redis::Json json(svr->storage, conn->GetNamespace());
+
+    std::string path = "$";
+    if (args_.size() == 3) {
+      path = args_[2];
+    } else if (args_.size() > 3) {
+      return {Status::RedisExecErr, "The number of arguments is more than 
expected"};
+    }
+
+    std::vector<uint64_t> results;
+    auto s = json.StrLen(args_[1], path, results);
+    if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
+
+    *output = CommandJsonStrAppend::IntegerArray(results);
+    return Status::OK();
+  }
+};
+
 REDIS_REGISTER_COMMANDS(MakeCmdAttr<CommandJsonSet>("json.set", 4, "write", 1, 
1, 1),
                         MakeCmdAttr<CommandJsonGet>("json.get", -2, 
"read-only", 1, 1, 1),
                         MakeCmdAttr<CommandJsonInfo>("json.info", 2, 
"read-only", 1, 1, 1),
@@ -586,6 +641,8 @@ 
REDIS_REGISTER_COMMANDS(MakeCmdAttr<CommandJsonSet>("json.set", 4, "write", 1, 1
                         MakeCmdAttr<CommandJsonDel>("json.forget", -2, 
"write", 1, 1, 1),
                         MakeCmdAttr<CommandJsonNumIncrBy>("json.numincrby", 4, 
"write", 1, 1, 1),
                         MakeCmdAttr<CommandJsonNumMultBy>("json.nummultby", 4, 
"write", 1, 1, 1),
-                        MakeCmdAttr<CommandJsonObjLen>("json.objlen", -2, 
"read-only", 1, 1, 1));
+                        MakeCmdAttr<CommandJsonObjLen>("json.objlen", -2, 
"read-only", 1, 1, 1),
+                        MakeCmdAttr<CommandJsonStrAppend>("json.strappend", 
-3, "write", 1, 1, 1),
+                        MakeCmdAttr<CommandJsonStrLen>("json.strlen", -2, 
"read-only", 1, 1, 1), );
 
 }  // namespace redis
diff --git a/src/types/json.h b/src/types/json.h
index 854a1d7d..0163c07a 100644
--- a/src/types/json.h
+++ b/src/types/json.h
@@ -37,6 +37,8 @@
 #include <limits>
 #include <string>
 
+#include "common/string_util.h"
+#include "jsoncons_ext/jsonpath/jsonpath_error.hpp"
 #include "status.h"
 
 constexpr ssize_t NOT_FOUND_INDEX = -1;
@@ -159,6 +161,49 @@ struct JsonValue {
     return Status::OK();
   }
 
+  Status StrAppend(std::string_view path, const std::string &append_value, 
std::vector<uint64_t> &append_cnt) {
+    try {
+      std::string append_str;
+      jsoncons::json append_json = jsoncons::json::parse(append_value);
+      if (append_json.is_string()) {
+        append_str = append_json.as_string();
+      } else {
+        return {Status::NotOK, "STRAPPEND need input a string to append"};
+      }
+
+      jsoncons::jsonpath::json_replace(
+          value, path, [&append_str, &append_cnt](const std::string & 
/*path*/, jsoncons::json &origin) {
+            if (origin.is_string()) {
+              auto origin_str = origin.as_string();
+              append_cnt.push_back(origin_str.length() + append_str.length());
+              origin = origin_str + append_str;
+            } else {
+              append_cnt.push_back(std::numeric_limits<uint64_t>::max());
+            }
+          });
+    } catch (const jsoncons::jsonpath::jsonpath_error &e) {
+      return {Status::NotOK, e.what()};
+    }
+
+    return Status::OK();
+  }
+
+  Status StrLen(std::string_view path, std::vector<uint64_t> &str_lens) const {
+    try {
+      jsoncons::jsonpath::json_query(value, path,
+                                     [&str_lens](const std::string & /*path*/, 
const jsoncons::json &origin) {
+                                       if (origin.is_string()) {
+                                         
str_lens.push_back(origin.as_string().length());
+                                       } else {
+                                         
str_lens.push_back(std::numeric_limits<uint64_t>::max());
+                                       }
+                                     });
+    } catch (const jsoncons::jsonpath::jsonpath_error &e) {
+      return {Status::NotOK, e.what()};
+    }
+    return Status::OK();
+  }
+
   StatusOr<JsonValue> Get(std::string_view path) const {
     try {
       return jsoncons::jsonpath::json_query(value, path);
diff --git a/src/types/redis_json.cc b/src/types/redis_json.cc
index 7db5c709..f3532b17 100644
--- a/src/types/redis_json.cc
+++ b/src/types/redis_json.cc
@@ -463,6 +463,44 @@ rocksdb::Status Json::numop(JsonValue::NumOpEnum op, const 
std::string &user_key
   return write(ns_key, &metadata, json_val);
 }
 
+rocksdb::Status Json::StrAppend(const std::string &user_key, const std::string 
&path, const std::string &value,
+                                std::vector<uint64_t> &append_cnt) {
+  auto ns_key = AppendNamespacePrefix(user_key);
+  JsonMetadata metadata;
+  JsonValue json_val;
+  auto s = read(ns_key, &metadata, &json_val);
+  if (!s.ok()) return s;
+
+  auto append_res = json_val.StrAppend(path, value, append_cnt);
+  if (!append_res) return rocksdb::Status::InvalidArgument(append_res.Msg());
+  // need reverse order
+  std::reverse(append_cnt.begin(), append_cnt.end());
+
+  bool need_overwrite = false;
+  for (auto append : append_cnt) {
+    if (append != std::numeric_limits<uint64_t>::max()) {
+      need_overwrite = true;
+    }
+  }
+  if (!need_overwrite) {
+    return rocksdb::Status::OK();
+  }
+
+  return write(ns_key, &metadata, json_val);
+}
+
+rocksdb::Status Json::StrLen(const std::string &user_key, const std::string 
&path, std::vector<uint64_t> &str_lens) {
+  auto ns_key = AppendNamespacePrefix(user_key);
+  JsonMetadata metadata;
+  JsonValue json_val;
+  auto s = read(ns_key, &metadata, &json_val);
+  if (!s.ok()) return s;
+
+  auto append_res = json_val.StrLen(path, str_lens);
+  if (!append_res) return rocksdb::Status::InvalidArgument(append_res.Msg());
+  return rocksdb::Status::OK();
+}
+
 rocksdb::Status Json::ObjLen(const std::string &user_key, const std::string 
&path,
                              std::vector<std::optional<uint64_t>> &obj_lens) {
   auto ns_key = AppendNamespacePrefix(user_key);
@@ -476,4 +514,5 @@ rocksdb::Status Json::ObjLen(const std::string &user_key, 
const std::string &pat
 
   return rocksdb::Status::OK();
 }
+
 }  // namespace redis
diff --git a/src/types/redis_json.h b/src/types/redis_json.h
index 732dc949..bf5e21b5 100644
--- a/src/types/redis_json.h
+++ b/src/types/redis_json.h
@@ -61,6 +61,9 @@ class Json : public Database {
   rocksdb::Status ArrTrim(const std::string &user_key, const std::string 
&path, int64_t start, int64_t stop,
                           std::vector<std::optional<uint64_t>> &results);
   rocksdb::Status Del(const std::string &user_key, const std::string &path, 
size_t *result);
+  rocksdb::Status StrAppend(const std::string &user_key, const std::string 
&path, const std::string &value,
+                            std::vector<uint64_t> &append_cnt);
+  rocksdb::Status StrLen(const std::string &user_key, const std::string &path, 
std::vector<uint64_t> &lens);
   rocksdb::Status ObjLen(const std::string &user_key, const std::string &path,
                          std::vector<std::optional<uint64_t>> &obj_lens);
 
diff --git a/tests/cppunit/types/json_test.cc b/tests/cppunit/types/json_test.cc
index 4ba08fe9..196bff44 100644
--- a/tests/cppunit/types/json_test.cc
+++ b/tests/cppunit/types/json_test.cc
@@ -36,6 +36,7 @@ class RedisJsonTest : public TestBase {
 
   std::unique_ptr<redis::Json> json_;
   JsonValue json_val_;
+  std::vector<uint64_t> append_cnt_;
 };
 
 using ::testing::MatchesRegex;
@@ -658,3 +659,36 @@ TEST_F(RedisJsonTest, NumMultBy) {
   ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok());
   ASSERT_EQ(json_val_.Dump().GetValue(), 
R"({"l1":{"l2":[0,1.6350000000001313e+308]}})");
 }
+
+TEST_F(RedisJsonTest, StrAppend) {
+  ASSERT_TRUE(json_->Set(key_, "$", R"({"a":"foo", "nested": {"a": "hello"}, 
"nested2": {"a": 31}})").ok());
+  ASSERT_TRUE(json_->StrAppend(key_, "$.a", "\"be\"", append_cnt_).ok());
+  ASSERT_EQ(append_cnt_.size(), 1);
+  ASSERT_EQ(append_cnt_[0], 5);
+
+  append_cnt_.clear();
+  ASSERT_TRUE(json_->Set(key_, "$", R"({"a":"foo", "nested": {"a": "hello"}, 
"nested2": {"a": 31}})").ok());
+  ASSERT_TRUE(json_->StrAppend(key_, "$..a", "\"be\"", append_cnt_).ok());
+  ASSERT_EQ(append_cnt_.size(), 3);
+  std::vector<int64_t> result1 = {5, 7, -1};
+  for (int i = 0; i < 3; ++i) {
+    ASSERT_EQ(append_cnt_[i], result1[i]);
+  }
+}
+
+TEST_F(RedisJsonTest, StrLen) {
+  append_cnt_.clear();
+  ASSERT_TRUE(json_->Set(key_, "$", R"({"a":"foo", "nested": {"a": "hello"}, 
"nested2": {"a": 31}})").ok());
+  ASSERT_TRUE(json_->StrLen(key_, "$.a", append_cnt_).ok());
+  ASSERT_EQ(append_cnt_.size(), 1);
+  ASSERT_EQ(append_cnt_[0], 3);
+
+  append_cnt_.clear();
+  ASSERT_TRUE(json_->Set(key_, "$", R"({"a":"foo", "nested": {"a": "hello"}, 
"nested2": {"a": 31}})").ok());
+  ASSERT_TRUE(json_->StrLen(key_, "$..a", append_cnt_).ok());
+  ASSERT_EQ(append_cnt_.size(), 3);
+  std::vector<int64_t> result1 = {3, 5, -1};
+  for (int i = 0; i < 3; ++i) {
+    ASSERT_EQ(append_cnt_[i], result1[i]);
+  }
+}
diff --git a/tests/gocase/unit/type/json/json_test.go 
b/tests/gocase/unit/type/json/json_test.go
index aee0ee69..37aa168c 100644
--- a/tests/gocase/unit/type/json/json_test.go
+++ b/tests/gocase/unit/type/json/json_test.go
@@ -158,6 +158,30 @@ func TestJson(t *testing.T) {
                require.EqualError(t, err, redis.Nil.Error())
        })
 
+       t.Run("JSON.STRAPPEND basics", func(t *testing.T) {
+               var result1 = make([]interface{}, 0)
+               result1 = append(result1, int64(5))
+               require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", 
`{"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31}}`).Err())
+               require.Equal(t, rdb.Do(ctx, "JSON.STRAPPEND", "a", "$.a", 
"\"be\"").Val(), result1)
+
+               var result2 = make([]interface{}, 0)
+               result2 = append(result2, int64(5), int64(7), interface{}(nil))
+               require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", 
`{"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31}}`).Err())
+               require.Equal(t, rdb.Do(ctx, "JSON.STRAPPEND", "a", "$..a", 
"\"be\"").Val(), result2)
+       })
+
+       t.Run("JSON.STRLEN basics", func(t *testing.T) {
+               var result1 = make([]interface{}, 0)
+               result1 = append(result1, int64(3))
+               require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", 
`{"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31}}`).Err())
+               require.Equal(t, rdb.Do(ctx, "JSON.STRLEN", "a", "$.a").Val(), 
result1)
+
+               var result2 = make([]interface{}, 0)
+               result2 = append(result2, int64(3), int64(5), interface{}(nil))
+               require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", 
`{"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31}}`).Err())
+               require.Equal(t, rdb.Do(ctx, "JSON.STRLEN", "a", "$..a").Val(), 
result2)
+       })
+
        t.Run("Merge basics", func(t *testing.T) {
                require.NoError(t, rdb.Do(ctx, "JSON.SET", "key", "$", 
`{"a":2}`).Err())
                require.NoError(t, rdb.Do(ctx, "JSON.MERGE", "key", "$.a", 
`3`).Err())

Reply via email to