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 78f87c40 Add support for the JSON.MERGE command  (#1852)
78f87c40 is described below

commit 78f87c409348bbde6839ccf4d7bb2be207efbf5e
Author: 2rueSid <[email protected]>
AuthorDate: Fri Nov 10 10:28:57 2023 -0500

    Add support for the JSON.MERGE command  (#1852)
---
 src/commands/cmd_json.cc                 | 27 +++++++++++++++++
 src/types/json.h                         | 50 ++++++++++++++++++++++++++++++++
 src/types/redis_json.cc                  | 45 ++++++++++++++++++++++++----
 src/types/redis_json.h                   |  2 ++
 tests/cppunit/types/json_test.cc         | 30 +++++++++++++++++++
 tests/gocase/unit/type/json/json_test.go | 19 ++++++++++++
 6 files changed, 168 insertions(+), 5 deletions(-)

diff --git a/src/commands/cmd_json.cc b/src/commands/cmd_json.cc
index 75e31323..50dacba2 100644
--- a/src/commands/cmd_json.cc
+++ b/src/commands/cmd_json.cc
@@ -281,6 +281,32 @@ class CommandJsonArrLen : public Commander {
   }
 };
 
+class CommandJsonMerge : public Commander {
+ public:
+  Status Execute(Server *svr, Connection *conn, std::string *output) override {
+    redis::Json json(svr->storage, conn->GetNamespace());
+
+    std::string key = args_[1];
+    std::string path = args_[2];
+    std::string value = args_[3];
+    bool result = false;
+
+    auto s = json.Merge(key, path, value, result);
+
+    if (!s.ok()) {
+      return {Status::RedisExecErr, s.ToString()};
+    }
+
+    if (!result) {
+      *output = redis::NilString();
+    } else {
+      *output = redis::SimpleString("OK");
+    }
+
+    return Status::OK();
+  }
+};
+
 class CommandJsonArrPop : public Commander {
  public:
   Status Parse(const std::vector<std::string> &args) override {
@@ -376,6 +402,7 @@ 
REDIS_REGISTER_COMMANDS(MakeCmdAttr<CommandJsonSet>("json.set", 4, "write", 1, 1
                         MakeCmdAttr<CommandJsonClear>("json.clear", -2, 
"write", 1, 1, 1),
                         MakeCmdAttr<CommandJsonToggle>("json.toggle", -2, 
"write", 1, 1, 1),
                         MakeCmdAttr<CommandJsonArrLen>("json.arrlen", -2, 
"read-only", 1, 1, 1),
+                        MakeCmdAttr<CommandJsonMerge>("json.merge", 4, 
"write", 1, 1, 1),
                         MakeCmdAttr<CommandJsonObjkeys>("json.objkeys", -2, 
"read-only", 1, 1, 1),
                         MakeCmdAttr<CommandJsonArrPop>("json.arrpop", -2, 
"write", 1, 1, 1),
                         MakeCmdAttr<CommanderJsonArrIndex>("json.arrindex", 
-4, "read-only", 1, 1, 1), );
diff --git a/src/types/json.h b/src/types/json.h
index 4aa4786b..6050e1cc 100644
--- a/src/types/json.h
+++ b/src/types/json.h
@@ -26,8 +26,10 @@
 #include <jsoncons_ext/cbor/cbor.hpp>
 #include <jsoncons_ext/cbor/cbor_encoder.hpp>
 #include <jsoncons_ext/cbor/cbor_options.hpp>
+#include <jsoncons_ext/jsonpath/flatten.hpp>
 #include <jsoncons_ext/jsonpath/json_query.hpp>
 #include <jsoncons_ext/jsonpath/jsonpath_error.hpp>
+#include <jsoncons_ext/mergepatch/mergepatch.hpp>
 #include <limits>
 #include <string>
 
@@ -316,6 +318,54 @@ struct JsonValue {
     return Status::OK();
   }
 
+  StatusOr<bool> Merge(const std::string_view path, const std::string 
&merge_value) {
+    bool is_updated = false;
+    const std::string json_root_path = "$";
+    try {
+      jsoncons::json patch_value = jsoncons::json::parse(merge_value);
+      bool not_exists = jsoncons::jsonpath::json_query(value, path).empty();
+
+      if (not_exists) {
+        // TODO:: Add ability to create an object from path.
+        return {Status::NotOK, "Path does not exist."};
+      }
+
+      if (path == json_root_path) {
+        // Merge with the root. Patch function complies with RFC7396 Json 
Merge Patch
+        jsoncons::mergepatch::apply_merge_patch(value, patch_value);
+        is_updated = true;
+      } else if (!patch_value.is_null()) {
+        // Replace value by path
+        jsoncons::jsonpath::json_replace(
+            value, path, [&patch_value, &is_updated](const std::string & 
/*path*/, jsoncons::json &target) {
+              jsoncons::mergepatch::apply_merge_patch(target, patch_value);
+              is_updated = true;
+            });
+      } else {
+        // Handle null case
+        // Unify path expression.
+        auto expr = jsoncons::jsonpath::make_expression<jsoncons::json>(path);
+        std::string converted_path;
+        expr.evaluate(
+            value, [&](const jsoncons::string_view &p, const jsoncons::json 
&val) { converted_path = p; },
+            jsoncons::jsonpath::result_options::path);
+        // Unify object state
+        jsoncons::json flattened = jsoncons::jsonpath::flatten(value);
+        if (flattened.contains(converted_path)) {
+          flattened.erase(converted_path);
+          value = jsoncons::jsonpath::unflatten(flattened);
+          is_updated = true;
+        }
+      }
+    } catch (const jsoncons::jsonpath::jsonpath_error &e) {
+      return {Status::NotOK, e.what()};
+    } catch (const jsoncons::ser_error &e) {
+      return {Status::NotOK, e.what()};
+    }
+
+    return is_updated;
+  }
+
   Status ObjKeys(std::string_view path, 
std::vector<std::optional<std::vector<std::string>>> &keys) const {
     try {
       jsoncons::jsonpath::json_query(value, path,
diff --git a/src/types/redis_json.cc b/src/types/redis_json.cc
index 3de38a88..e16fbbdd 100644
--- a/src/types/redis_json.cc
+++ b/src/types/redis_json.cc
@@ -76,6 +76,14 @@ rocksdb::Status Json::read(const Slice &ns_key, JsonMetadata 
*metadata, JsonValu
   return rocksdb::Status::OK();
 }
 
+rocksdb::Status Json::create(const std::string &ns_key, JsonMetadata 
&metadata, const std::string &value) {
+  auto json_res = JsonValue::FromString(value, 
storage_->GetConfig()->json_max_nesting_depth);
+  if (!json_res) return rocksdb::Status::InvalidArgument(json_res.Msg());
+  auto json_val = *std::move(json_res);
+
+  return write(ns_key, &metadata, json_val);
+}
+
 rocksdb::Status Json::Info(const std::string &user_key, JsonStorageFormat 
*storage_format) {
   auto ns_key = AppendNamespacePrefix(user_key);
 
@@ -103,11 +111,7 @@ rocksdb::Status Json::Set(const std::string &user_key, 
const std::string &path,
   if (s.IsNotFound()) {
     if (path != "$") return rocksdb::Status::InvalidArgument("new objects must 
be created at the root");
 
-    auto json_res = JsonValue::FromString(value, 
storage_->GetConfig()->json_max_nesting_depth);
-    if (!json_res) return rocksdb::Status::InvalidArgument(json_res.Msg());
-    auto json_val = *std::move(json_res);
-
-    return write(ns_key, &metadata, json_val);
+    return create(ns_key, metadata, value);
   }
 
   if (!s.ok()) return s;
@@ -215,6 +219,37 @@ rocksdb::Status Json::Type(const std::string &user_key, 
const std::string &path,
   return rocksdb::Status::OK();
 }
 
+rocksdb::Status Json::Merge(const std::string &user_key, const std::string 
&path, const std::string &merge_value,
+                            bool &result) {
+  auto ns_key = AppendNamespacePrefix(user_key);
+
+  LockGuard guard(storage_->GetLockManager(), ns_key);
+
+  JsonMetadata metadata;
+  JsonValue json_val;
+
+  auto s = read(ns_key, &metadata, &json_val);
+
+  if (s.IsNotFound()) {
+    if (path != "$") return rocksdb::Status::InvalidArgument("new objects must 
be created at the root");
+    result = true;
+    return create(ns_key, metadata, merge_value);
+  }
+
+  if (!s.ok()) return s;
+
+  auto res = json_val.Merge(path, merge_value);
+
+  if (!res.IsOK()) return s;
+
+  result = static_cast<bool>(res.GetValue());
+  if (!res) {
+    return rocksdb::Status::OK();
+  }
+
+  return write(ns_key, &metadata, json_val);
+}
+
 rocksdb::Status Json::Clear(const std::string &user_key, const std::string 
&path, size_t *result) {
   auto ns_key = AppendNamespacePrefix(user_key);
 
diff --git a/src/types/redis_json.h b/src/types/redis_json.h
index 94a054ae..a34ff19d 100644
--- a/src/types/redis_json.h
+++ b/src/types/redis_json.h
@@ -39,6 +39,7 @@ class Json : public Database {
   rocksdb::Status Type(const std::string &user_key, const std::string &path, 
std::vector<std::string> *results);
   rocksdb::Status ArrAppend(const std::string &user_key, const std::string 
&path,
                             const std::vector<std::string> &values, 
std::vector<size_t> *result_count);
+  rocksdb::Status Merge(const std::string &user_key, const std::string &path, 
const std::string &value, bool &result);
   rocksdb::Status Clear(const std::string &user_key, const std::string &path, 
size_t *result);
   rocksdb::Status ArrLen(const std::string &user_key, const std::string &path,
                          std::vector<std::optional<uint64_t>> &arr_lens);
@@ -54,6 +55,7 @@ class Json : public Database {
  private:
   rocksdb::Status write(Slice ns_key, JsonMetadata *metadata, const JsonValue 
&json_val);
   rocksdb::Status read(const Slice &ns_key, JsonMetadata *metadata, JsonValue 
*value);
+  rocksdb::Status create(const std::string &ns_key, JsonMetadata &metadata, 
const std::string &value);
 };
 
 }  // namespace redis
diff --git a/tests/cppunit/types/json_test.cc b/tests/cppunit/types/json_test.cc
index c371c6d6..dd2d2871 100644
--- a/tests/cppunit/types/json_test.cc
+++ b/tests/cppunit/types/json_test.cc
@@ -182,6 +182,36 @@ TEST_F(RedisJsonTest, ArrAppend) {
   res.clear();
 }
 
+TEST_F(RedisJsonTest, Merge) {
+  bool result = false;
+
+  ASSERT_TRUE(json_->Set(key_, "$", R"({"a":2})").ok());
+  ASSERT_TRUE(json_->Merge(key_, "$.a", "3", result).ok());
+  ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok());
+  ASSERT_EQ(json_val_.Dump().GetValue(), "{\"a\":3}");
+  ASSERT_EQ(result, true);
+
+  ASSERT_TRUE(json_->Set(key_, "$", R"({"v": {"b": "cc"}})").ok());
+  ASSERT_TRUE(json_->Merge(key_, "$.v.b", "null", result).ok());
+  ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok());
+  ASSERT_EQ(json_val_.Dump().GetValue(), "{}");
+  ASSERT_EQ(result, true);
+
+  ASSERT_TRUE(json_->Set(key_, "$", R"({"arr":[2,4,6,8]})").ok());
+  ASSERT_TRUE(json_->Merge(key_, "$.arr", "[10,12]", result).ok());
+  ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok());
+  ASSERT_EQ(json_val_.Dump().GetValue(), "{\"arr\":[10,12]}");
+  ASSERT_EQ(result, true);
+
+  ASSERT_TRUE(json_->Set(key_, "$", R"({"f1": {"a":1}, "f2":{"a":2}})").ok());
+  ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok());
+  ASSERT_EQ(json_val_.Dump().GetValue(), 
"{\"f1\":{\"a\":1},\"f2\":{\"a\":2}}");
+  ASSERT_TRUE(json_->Merge(key_, "$", R"({"f1": null, "f2":{"a":3, "b":4}, 
"f3":[2,4,6]})", result).ok());
+  ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok());
+  ASSERT_EQ(json_val_.Dump().GetValue(), 
"{\"f2\":{\"a\":3,\"b\":4},\"f3\":[2,4,6]}");
+  ASSERT_EQ(result, true);
+}
+
 TEST_F(RedisJsonTest, Clear) {
   size_t result = 0;
 
diff --git a/tests/gocase/unit/type/json/json_test.go 
b/tests/gocase/unit/type/json/json_test.go
index 2e97b3b8..6e68fd2e 100644
--- a/tests/gocase/unit/type/json/json_test.go
+++ b/tests/gocase/unit/type/json/json_test.go
@@ -142,6 +142,25 @@ func TestJson(t *testing.T) {
                require.EqualError(t, err, redis.Nil.Error())
        })
 
+       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())
+               require.Equal(t, `{"a":3}`, rdb.Do(ctx, "JSON.GET", 
"key").Val())
+
+               require.NoError(t, rdb.Do(ctx, "JSON.SET", "key", "$", 
`{"a":2}`).Err())
+               require.NoError(t, rdb.Do(ctx, "JSON.MERGE", "key", "$.a", 
`null`).Err())
+               require.Equal(t, `{}`, rdb.Do(ctx, "JSON.GET", "key").Val())
+
+               require.NoError(t, rdb.Do(ctx, "JSON.SET", "key", "$", 
`{"a":[2,4,6,8]}`).Err())
+               require.NoError(t, rdb.Do(ctx, "JSON.MERGE", "key", "$.a", 
`[10,12]`).Err())
+               require.Equal(t, `{"a":[10,12]}`, rdb.Do(ctx, "JSON.GET", 
"key").Val())
+
+               require.NoError(t, rdb.Do(ctx, "JSON.SET", "key", "$", `{"f1": 
{"a":1}, "f2":{"a":2}}`).Err())
+               require.Equal(t, `{"f1":{"a":1},"f2":{"a":2}}`, rdb.Do(ctx, 
"JSON.GET", "key").Val())
+               require.NoError(t, rdb.Do(ctx, "JSON.MERGE", "key", "$", 
`{"f1": null, "f2":{"a":3, "b":4}, "f3":[2,4,6]}`).Err())
+               require.Equal(t, `{"f2":{"a":3,"b":4},"f3":[2,4,6]}`, 
rdb.Do(ctx, "JSON.GET", "key").Val())
+       })
+
        t.Run("Clear JSON values", func(t *testing.T) {
                require.NoError(t, rdb.Do(ctx, "JSON.SET", "bb", "$", 
`{"obj":{"a":1, "b":2}, "arr":[1,2,3], "str": "foo", "bool": true, "int": 42, 
"float": 3.14}`).Err())
 

Reply via email to