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())